# Semantic search (QMD)

> Opt-in QMD indexing via Bun bundle, collection creation, embedding generation, search IPC, project preferences, and setup-qmd dev/build integration.

- Repository: Parcha-ai/build
- GitHub: https://github.com/Parcha-ai/build
- Human docs: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b
- Complete Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/llms-full.txt

## Source Files

- `src/main/services/qmd.service.ts`
- `src/main/ipc/qmd.ipc.ts`
- `scripts/setup-qmd.ts`
- `src/renderer/components/qmd/QMDPrompt.tsx`
- `src/main/services/settings.service.ts`
- `package.json`

---

---
title: "Semantic search (QMD)"
description: "Opt-in QMD indexing via Bun bundle, collection creation, embedding generation, search IPC, project preferences, and setup-qmd dev/build integration."
---

Build integrates [QMD](https://github.com/tobi/qmd) as an optional, local semantic codebase search layer. The main process resolves a Bun-wrapped QMD binary, indexes project folders into named collections, generates embeddings, and—when both global and per-project opt-in are satisfied—registers QMD as a stdio MCP server for local Claude Agent SDK sessions. Indexing, install, and search are exposed through typed IPC; the renderer can prompt per project and surface progress in settings or `QMDPrompt`.

## Opt-in model

QMD is off by default at every layer until the user enables it.

| Layer | Store / surface | Values | Default |
| --- | --- | --- | --- |
| Global | `claudette-settings` → `settings.qmdEnabled` (`AppSettings`) | `true` / `false` | `false` |
| Per project | `claudette-qmd` → `projectPreferences.<md5(projectPath)>` | `enabled` / `disabled` / unset (`unknown`) | unset |
| Runtime binary | Resolved by `QMDService.checkInstalled()` | bundled, user-local, or PATH | none until setup |

`QMDService.isEnabledForProject(projectPath, globalEnabled)` requires **both** `globalEnabled === true` and per-project preference `enabled`. SSH sessions skip QMD entirely because indexing runs on the local machine.

<Note>
`ClaudeService` reads the global flag with `store.get('qmdEnabled', false)` on the `claudette-settings` store root, while `SettingsService` persists `qmdEnabled` under the nested `settings` object. If semantic search does not attach after toggling **Enable QMD Search**, confirm the value ClaudeService reads matches what the settings UI wrote.
</Note>

## Resolution order for the QMD binary

`QMDService` caches `qmdPath` after the first successful resolve.

```mermaid
flowchart TD
  subgraph dev [Development - app not packaged]
    R1[resources/qmd/platformKey/qmd]
    R2[userData/qmd/platformKey/qmd]
    R1 -->|exists| USE[Use wrapper]
    R2 -->|fallback| USE
  end
  subgraph pkg [Packaged app]
    P1[App Resources/qmd/platformKey/qmd]
    P1 --> USE
  end
  subgraph fallback [Any mode]
    PATH[which qmd + common paths]
    PATH --> USE
  end
  USE --> CLI[Spawn qmd via wrapper or system binary]
```

Platform key format: `{platform}-{arch}` (for example `darwin-arm64`). Wrapper scripts invoke bundled Bun against `qmd-package/node_modules/qmd/src/qmd.ts`.

| Location | When used |
| --- | --- |
| `resources/qmd/<platformKey>/qmd` | Dev after `npm run setup-qmd`; packaged copy from forge hook |
| `<userData>/qmd/<platformKey>/qmd` | After `autoInstall` or manual user-local install |
| PATH / Homebrew / `~/.bun/bin/qmd` | Fallback when no bundle present |

Bun version pinned in repo: **1.1.38**. QMD package source: `https://github.com/tobi/qmd`.

## Bundle layout

:::files
resources/qmd/
└── <platform>-<arch>/
    ├── bun/                 # Bun runtime (extracted from GitHub release)
    ├── qmd-package/         # bun add https://github.com/tobi/qmd
    └── qmd or qmd.cmd       # Wrapper → bun --cwd=... src/qmd.ts
:::

Packaging (`forge.config.ts`) copies `resources/qmd/<platformKey>/` into the app `Resources/qmd/` tree and strips `.bun-cache` and `__MACOSX` before signing. Missing platform bundles log a warning and skip copy.

## Setup commands

| Command | Purpose |
| --- | --- |
| `npm run setup-qmd` | Bundle QMD for **current** platform into `resources/qmd/` |
| `npm run setup-qmd:all` | Run `scripts/setup-qmd.ts all` for every supported platform |
| `npx ts-node scripts/setup-qmd.ts <platformKey>` | Explicit platform (e.g. `darwin-arm64`) |

Supported `platformKey` values: `darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`.

`./scripts/dev.sh` runs `npm run setup-qmd` before starting the dev server so local Electron can find `resources/qmd/<platformKey>/qmd`. Production builds expect the same directory populated before `npm run make` (or accept runtime `autoInstall`).

## Indexing lifecycle

```mermaid
sequenceDiagram
  participant UI as Renderer
  participant IPC as qmd.ipc
  participant Svc as QMDService
  participant QMD as QMD CLI

  UI->>IPC: QMD_ENSURE_INDEXED(projectPath)
  IPC->>Svc: ensureProjectIndexed()
  Svc->>Svc: checkInstalled()
  alt no collection
    Svc->>QMD: collection add --name --mask
  end
  Svc->>QMD: embed --collection <name>
  QMD-->>Svc: stdout progress
  Svc-->>IPC: onProgress(message)
  IPC-->>UI: QMD_INDEXING_PROGRESS event
```

### Collection naming and file mask

- **Name:** `<sanitized-basename>-<6-char-md5-prefix>` from `path.basename(projectPath)` and path hash.
- **Default mask:** `**/*.{ts,tsx,js,jsx,py,go,rs,java,md,json,yaml,yml,toml}` (overridable via `QMD_CREATE_COLLECTION`).

`ensureProjectIndexed` is idempotent for an existing collection but still runs `embed` for that collection. A re-entrant guard (`isChecking`) drops overlapping calls.

### Embeddings and status

- `generateEmbeddings(collectionName?)` spawns `qmd embed [--collection name]` and streams stdout to progress callbacks.
- `getStatus()` runs `qmd status --json` and sets `embeddingsReady` from `embeddingsReady` or `hasEmbeddings`.

Collection metadata (last indexed timestamp) is also stored in `claudette-qmd` under `collections.<name>`.

## Agent integration (MCP)

For **local** sessions when global + project enablement pass and `getMcpServerConfig()` returns a path:

```text
mcpServersConfig['qmd'] = { type: 'stdio', command: <qmdPath>, args: ['mcp'] }
```

Background indexing: `ensureProjectIndexed(projectPath)` without blocking the agent turn.

When global QMD is on but per-project preference is still `unknown`, `shouldPromptForProject` may emit `QMD_PROMPT_RESPONSE` with `{ sessionId, projectPath }` so `QMDPrompt` can ask the user.

<Warning>
QMD does not run for SSH-backed sessions (`session.sshConfig` set). Remote workspaces are not indexed by this path.
</Warning>

## IPC surface

Handlers live in `registerQmdHandlers` (`src/main/ipc/qmd.ipc.ts`). Channel constants are in `IPC_CHANNELS` under the `qmd:*` prefix.

| Channel | Pattern | Role |
| --- | --- | --- |
| `qmd:get-status` | invoke | `QMDStatus`: installed, version, collections, embeddingsReady, bundled |
| `qmd:ensure-indexed` | invoke | Full index pipeline for `projectPath` |
| `qmd:create-collection` | invoke | `collection add` with optional `mask` |
| `qmd:generate-embeddings` | invoke | `embed` for optional collection name |
| `qmd:search` | invoke | Direct CLI search (`search` / `vsearch` / `query`, JSON results) |
| `qmd:get-project-preference` | invoke | `enabled` \| `disabled` \| `unknown` |
| `qmd:set-project-preference` | invoke | Persist per-project choice |
| `qmd:should-prompt` | invoke | Whether to show first-run prompt |
| `qmd:auto-install` | invoke | Download Bun + install QMD under `userData/qmd/` |
| `qmd:indexing-progress` | **event** (main → renderer) | Progress during index, embed, or install |
| `qmd:prompt-response` | **event** (main → renderer) | Open `QMDPrompt` modal |

### Preload API (`window.electronAPI.qmd`)

- **Invoke:** `getStatus`, `ensureIndexed`, `createCollection`, `generateEmbeddings`, `search`, `getProjectPreference`, `setProjectPreference`, `shouldPrompt`, `autoInstall`
- **Subscribe:** `onIndexingProgress`, `onPromptRequest`

### Search invoke parameters

<ParamField body="query" type="string" required>
Natural-language or keyword query passed to the QMD CLI.
</ParamField>

<ParamField body="options.collection" type="string">
Limit search to a collection name (typically the project-derived name).
</ParamField>

<ParamField body="options.mode" type="'search' | 'vsearch' | 'query'">
CLI mode; default `query`.
</ParamField>

<ParamField body="options.limit" type="number">
Result cap; default `10`.
</ParamField>

<ResponseField name="results" type="Array<{ file: string; score: number; content: string }>">
Parsed JSON from the QMD CLI stdout.
</ResponseField>

## Persistence

| File | Contents |
| --- | --- |
| `claudette-qmd.json` | `collections`, optional `qmdPath`, `projectPreferences` (MD5 keys) |
| `claudette-settings.json` | `settings.qmdEnabled` and other `AppSettings` |

Dev uses a separate user data root (`GREP_DEV_USER_DATA`, typically `/tmp/grep-build-dev`) so QMD indexes and preferences do not overwrite production data.

## User workflows

<Steps>
<Step title="Enable globally">
Open **Settings → General → Semantic Codebase Search**, turn on **Enable QMD Search**. Optionally use **Install** if status reports QMD missing; that calls `autoInstall` and listens for `QMD_INDEXING_PROGRESS`.
</Step>
<Step title="Accept per project">
On first local session with global QMD on, `QMDPrompt` offers **Enable** or **Not Now**. **Enable** may run `autoInstall`, sets preference `enabled`, then `ensureIndexed`. **Not Now** sets `disabled`.
</Step>
<Step title="Verify">
Settings should show `QMD (bundled) ready` or `(installed) ready`. Agent logs may include `QMD MCP server enabled for semantic search`. First embed can download models and take noticeable time on large repos.
</Step>
</Steps>

## Auto-install behavior

`QMDService.autoInstall` mirrors `setup-qmd.ts` for a single platform:

1. Download Bun zip from `oven-sh/bun` releases.
2. `bun add https://github.com/tobi/qmd` into `<userData>/qmd/<platformKey>/qmd-package`.
3. Write wrapper at `<userData>/qmd/<platformKey>/qmd` (or `qmd.cmd` on Windows).

Progress messages are forwarded on `QMD_INDEXING_PROGRESS` (install path omits `projectPath` in the payload).

## Troubleshooting

| Symptom | Likely cause | Action |
| --- | --- | --- |
| `QMD not installed` in settings | No bundle and no PATH binary | Run `npm run setup-qmd` (dev) or **Install** / `autoInstall` |
| Packaging warning for platform | `resources/qmd/<platformKey>` missing before `make` | `npm run setup-qmd` or `setup-qmd:all` for target arch |
| Prompt never appears | QMD not installed, global off, or preference already set | Enable global toggle; check `claudette-qmd` preferences |
| MCP not in session | SSH session, project disabled, or binary unresolved | Use local session; enable project; check `getStatus().installed` |
| Indexing stuck / silent | Concurrent `ensureProjectIndexed` (`isChecking`) | Wait and retry; inspect main process logs `[QMD Service]` |
| Dev vs prod data mixed | Wrong userData path | Confirm dev uses `GREP_DEV_USER_DATA` |

<Info>
First `embed` may download embedding models locally. No cloud API key is required for QMD itself; the feature is BYOC/BYOK-neutral at the search layer.
</Info>

## Related pages

<CardGroup>
<Card title="Settings reference" href="/settings-reference">
`qmdEnabled` and other `AppSettings` keys in `claudette-settings`.
</Card>
<Card title="IPC channels reference" href="/ipc-channels-reference">
Full `qmd:*` channel list with invoke vs push semantics.
</Card>
<Card title="MCP servers" href="/mcp-servers">
How stdio MCP servers are loaded alongside QMD in agent sessions.
</Card>
<Card title="npm scripts reference" href="/npm-scripts-reference">
`setup-qmd`, `setup-qmd:all`, and `./scripts/dev.sh` vs `npm run start`.
</Card>
<Card title="Build and release" href="/build-and-release">
Packaging QMD into distributables and forge resource copy.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Broader dev instance, PATH, and logging guidance.
</Card>
</CardGroup>
