@tekmidian/pai 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +93 -9
  2. package/dist/{auto-route-B5MSUJZK.mjs → auto-route-BG6I_4B1.mjs} +2 -2
  3. package/dist/{auto-route-B5MSUJZK.mjs.map → auto-route-BG6I_4B1.mjs.map} +1 -1
  4. package/dist/cli/index.mjs +103 -18
  5. package/dist/cli/index.mjs.map +1 -1
  6. package/dist/{config-B4brrHHE.mjs → config-Cf92lGX_.mjs} +17 -3
  7. package/dist/config-Cf92lGX_.mjs.map +1 -0
  8. package/dist/daemon/index.mjs +6 -6
  9. package/dist/{daemon-s868Paua.mjs → daemon-D9evGlgR.mjs} +11 -11
  10. package/dist/{daemon-s868Paua.mjs.map → daemon-D9evGlgR.mjs.map} +1 -1
  11. package/dist/daemon-mcp/index.mjs +5 -3
  12. package/dist/daemon-mcp/index.mjs.map +1 -1
  13. package/dist/{detect-CdaA48EI.mjs → detect-BU3Nx_2L.mjs} +1 -1
  14. package/dist/{detect-CdaA48EI.mjs.map → detect-BU3Nx_2L.mjs.map} +1 -1
  15. package/dist/{factory-CeXQzlwn.mjs → factory-Bzcy70G9.mjs} +3 -3
  16. package/dist/{factory-CeXQzlwn.mjs.map → factory-Bzcy70G9.mjs.map} +1 -1
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +3 -2
  19. package/dist/{indexer-CKQcgKsz.mjs → indexer-CMPOiY1r.mjs} +22 -1
  20. package/dist/{indexer-CKQcgKsz.mjs.map → indexer-CMPOiY1r.mjs.map} +1 -1
  21. package/dist/{indexer-backend-DQO-FqAI.mjs → indexer-backend-CIMXedqk.mjs} +26 -9
  22. package/dist/indexer-backend-CIMXedqk.mjs.map +1 -0
  23. package/dist/{ipc-client-CgSpwHDC.mjs → ipc-client-Bjg_a1dc.mjs} +1 -1
  24. package/dist/{ipc-client-CgSpwHDC.mjs.map → ipc-client-Bjg_a1dc.mjs.map} +1 -1
  25. package/dist/mcp/index.mjs +7 -3
  26. package/dist/mcp/index.mjs.map +1 -1
  27. package/dist/{postgres-CIxeqf_n.mjs → postgres-FXrHDPcE.mjs} +36 -13
  28. package/dist/postgres-FXrHDPcE.mjs.map +1 -0
  29. package/dist/{sqlite-CymLKiDE.mjs → sqlite-WWBq7_2C.mjs} +18 -1
  30. package/dist/{sqlite-CymLKiDE.mjs.map → sqlite-WWBq7_2C.mjs.map} +1 -1
  31. package/dist/{tools-Dx7GjOHd.mjs → tools-DV_lsiCc.mjs} +15 -13
  32. package/dist/tools-DV_lsiCc.mjs.map +1 -0
  33. package/package.json +1 -1
  34. package/dist/config-B4brrHHE.mjs.map +0 -1
  35. package/dist/indexer-backend-DQO-FqAI.mjs.map +0 -1
  36. package/dist/postgres-CIxeqf_n.mjs.map +0 -1
  37. package/dist/tools-Dx7GjOHd.mjs.map +0 -1
package/README.md CHANGED
@@ -125,7 +125,7 @@ For the technical deep-dive — architecture, database schema, CLI reference, an
125
125
 
126
126
  ## Search Intelligence
127
127
 
128
- PAI doesn't just store your notes — it understands them. Three search modes work together, and an optional reranking step puts the best results first.
128
+ PAI doesn't just store your notes — it understands them. Three search modes work together, with reranking and recency boost on by default. All search settings are configurable.
129
129
 
130
130
  ### Search Modes
131
131
 
@@ -151,22 +151,106 @@ The reranker uses a small local model (~23 MB) that runs entirely on your machin
151
151
 
152
152
  ### Recency Boost
153
153
 
154
- Optionally weight recent content higher than older content. Useful when you want what you worked on last week to rank above something from six months ago, even if both match equally well.
155
-
156
- The boost uses exponential decay with a configurable half-life. A half-life of 90 days means a 3-month-old result retains 50% of its score, a 6-month-old retains 25%, and a year-old retains ~6%.
154
+ Recent content scores higher than older content on by default with a 90-day half-life. A 3-month-old result retains 50% of its score, a 6-month-old retains 25%, and a year-old retains ~6%.
157
155
 
158
156
  ```bash
159
- # Boost recent results (score halves every 90 days)
160
- pai memory search "notification system" --recency 90
157
+ # Search uses recency boost automatically (90-day half-life from config)
158
+ pai memory search "notification system"
159
+
160
+ # Override the half-life for this search
161
+ pai memory search "notification system" --recency 30
161
162
 
162
- # Combine with any mode works with keyword, semantic, hybrid, and reranking
163
- pai memory search "notification system" --mode hybrid --recency 90
163
+ # Disable recency boost for this search
164
+ pai memory search "notification system" --recency 0
164
165
  ```
165
166
 
166
- Via MCP, pass `recency_boost: 90` to the `memory_search` tool. Set to 0 (default) to disable.
167
+ Via MCP, pass `recency_boost: 90` to the `memory_search` tool, or `recency_boost: 0` to disable.
167
168
 
168
169
  Recency boost is applied after cross-encoder reranking, so relevance is scored first, then time-weighted. Scores are normalized before decay so the math works correctly regardless of the underlying score scale.
169
170
 
171
+ ### Search Settings
172
+
173
+ All search defaults are configurable via `~/.config/pai/config.json` and can be viewed or changed from the command line.
174
+
175
+ ```bash
176
+ # View all search settings
177
+ pai memory settings
178
+
179
+ # View a single setting
180
+ pai memory settings recencyBoostDays
181
+
182
+ # Change a setting
183
+ pai memory settings recencyBoostDays 60
184
+ pai memory settings mode hybrid
185
+ pai memory settings rerank false
186
+ ```
187
+
188
+ | Setting | Default | Description |
189
+ |---------|---------|-------------|
190
+ | `mode` | `keyword` | Default search mode: `keyword`, `semantic`, or `hybrid` |
191
+ | `rerank` | `true` | Cross-encoder reranking on by default |
192
+ | `recencyBoostDays` | `90` | Recency half-life in days. `0` = off |
193
+ | `defaultLimit` | `10` | Default number of results |
194
+ | `snippetLength` | `200` | Max characters per snippet in MCP results |
195
+
196
+ Settings live in the `search` section of `~/.config/pai/config.json`. Per-call parameters (CLI flags or MCP tool arguments) always override config defaults.
197
+
198
+ ### Using Search from Within Claude
199
+
200
+ When PAI is configured as an MCP server, Claude uses the `memory_search` tool automatically. You don't need to call it yourself — just ask Claude naturally and it searches your memory behind the scenes.
201
+
202
+ **Example prompts you can give Claude:**
203
+
204
+ ```
205
+ "Search your memory for authentication"
206
+ "What do you know about the database migration?"
207
+ "Find where we discussed the notification system"
208
+ ```
209
+
210
+ Claude calls `memory_search` with the right parameters based on your config defaults. Reranking and recency boost are both active by default — you don't need to configure anything for good results.
211
+
212
+ **Overriding defaults for a specific search:**
213
+
214
+ You can ask Claude to adjust search behavior per-query:
215
+
216
+ ```
217
+ "Search for authentication using semantic mode"
218
+ → Claude passes mode: "semantic"
219
+
220
+ "Search for the old logging discussion without recency boost"
221
+ → Claude passes recency_boost: 0
222
+
223
+ "Search for database schema across all projects with no reranking"
224
+ → Claude passes all_projects: true, rerank: false
225
+ ```
226
+
227
+ **The `memory_search` MCP tool accepts these parameters:**
228
+
229
+ | Parameter | Type | Description |
230
+ |-----------|------|-------------|
231
+ | `query` | string | Free-text search query (required) |
232
+ | `project` | string | Scope to one project by slug |
233
+ | `all_projects` | boolean | Explicitly search all projects |
234
+ | `sources` | array | Restrict to `"memory"` or `"notes"` |
235
+ | `limit` | integer | Max results (1–100, default from config) |
236
+ | `mode` | string | `"keyword"`, `"semantic"`, or `"hybrid"` |
237
+ | `rerank` | boolean | Cross-encoder reranking (default: true from config) |
238
+ | `recency_boost` | integer | Recency half-life in days (0 = off, default from config) |
239
+
240
+ All parameters except `query` are optional. Omitted values fall back to your `~/.config/pai/config.json` defaults.
241
+
242
+ **Changing defaults permanently:**
243
+
244
+ Tell Claude to change your search settings:
245
+
246
+ ```
247
+ "Set my default search mode to hybrid"
248
+ "Turn off reranking by default"
249
+ "Change the recency boost to 60 days"
250
+ ```
251
+
252
+ Claude runs `pai memory settings <key> <value>` to update `~/.config/pai/config.json`. Changes take effect on the next search — no restart needed.
253
+
170
254
  ---
171
255
 
172
256
  ## Zettelkasten Intelligence
@@ -1,5 +1,5 @@
1
1
  import { r as readPaiMarker } from "./pai-marker-CXQPX2P6.mjs";
2
- import { t as detectProject } from "./detect-CdaA48EI.mjs";
2
+ import { t as detectProject } from "./detect-BU3Nx_2L.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
 
@@ -83,4 +83,4 @@ function formatAutoRouteJson(result) {
83
83
 
84
84
  //#endregion
85
85
  export { autoRoute, formatAutoRouteJson };
86
- //# sourceMappingURL=auto-route-B5MSUJZK.mjs.map
86
+ //# sourceMappingURL=auto-route-BG6I_4B1.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"auto-route-B5MSUJZK.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
1
+ {"version":3,"file":"auto-route-BG6I_4B1.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
@@ -4,13 +4,13 @@ import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shorte
4
4
  import { a as slugify$1, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "../migrate-jokLenje.mjs";
5
5
  import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQPX2P6.mjs";
6
6
  import { n as openFederation } from "../db-Dp8VXIMR.mjs";
7
- import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CKQcgKsz.mjs";
7
+ import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CMPOiY1r.mjs";
8
8
  import "../embeddings-DGRAPAYb.mjs";
9
9
  import { n as populateSlugs, r as searchMemory } from "../search-_oHfguA5.mjs";
10
- import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-CdaA48EI.mjs";
11
- import { t as PaiClient } from "../ipc-client-CgSpwHDC.mjs";
12
- import { a as expandHome, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-B4brrHHE.mjs";
13
- import { t as createStorageBackend } from "../factory-CeXQzlwn.mjs";
10
+ import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-Cf92lGX_.mjs";
11
+ import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-BU3Nx_2L.mjs";
12
+ import { t as PaiClient } from "../ipc-client-Bjg_a1dc.mjs";
13
+ import { t as createStorageBackend } from "../factory-Bzcy70G9.mjs";
14
14
  import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { basename, dirname, join, relative, resolve } from "node:path";
@@ -1809,10 +1809,10 @@ function cmdActive(db, opts) {
1809
1809
  ], rows));
1810
1810
  }
1811
1811
  async function cmdAutoRoute(opts) {
1812
- const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-B5MSUJZK.mjs");
1812
+ const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-BG6I_4B1.mjs");
1813
1813
  const { openRegistry } = await import("../db-4lSqLFb8.mjs").then((n) => n.t);
1814
- const { createStorageBackend } = await import("../factory-CeXQzlwn.mjs").then((n) => n.n);
1815
- const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
1814
+ const { createStorageBackend } = await import("../factory-Bzcy70G9.mjs").then((n) => n.n);
1815
+ const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
1816
1816
  const config = loadConfig();
1817
1817
  const registryDb = openRegistry();
1818
1818
  const federation = await createStorageBackend(config);
@@ -2340,8 +2340,8 @@ async function displayDryRun(plans) {
2340
2340
  async function countVectorDbPaths(oldPaths) {
2341
2341
  if (oldPaths.length === 0) return 0;
2342
2342
  try {
2343
- const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
2344
- const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2343
+ const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2344
+ const { PostgresBackend } = await import("../postgres-FXrHDPcE.mjs");
2345
2345
  const config = loadConfig();
2346
2346
  if (config.storageBackend !== "postgres") return 0;
2347
2347
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2367,8 +2367,8 @@ async function countVectorDbPaths(oldPaths) {
2367
2367
  async function updateVectorDbPaths(moves) {
2368
2368
  if (moves.length === 0) return 0;
2369
2369
  try {
2370
- const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
2371
- const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2370
+ const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2371
+ const { PostgresBackend } = await import("../postgres-FXrHDPcE.mjs");
2372
2372
  const config = loadConfig();
2373
2373
  if (config.storageBackend !== "postgres") return 0;
2374
2374
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -3039,6 +3039,15 @@ function registerRegistryCommands(registryCmd, getDb) {
3039
3039
 
3040
3040
  //#endregion
3041
3041
  //#region src/cli/commands/memory.ts
3042
+ /**
3043
+ * CLI commands for the PAI memory engine (Phase 2 / Phase 2.5).
3044
+ *
3045
+ * Commands:
3046
+ * pai memory index [project-slug] — index one or all projects
3047
+ * pai memory embed [project-slug] — generate embeddings for un-embedded chunks
3048
+ * pai memory search <query> — BM25/semantic/hybrid search across federation.db
3049
+ * pai memory status [project-slug] — show index stats
3050
+ */
3042
3051
  function tierColor(tier) {
3043
3052
  switch (tier) {
3044
3053
  case "evergreen": return chalk.green(tier);
@@ -3110,7 +3119,7 @@ function registerMemoryCommands(memoryCmd, getDb) {
3110
3119
  await runEmbed(federation, project.id, project.slug, parseInt(opts.batchSize ?? "50", 10));
3111
3120
  } else await runEmbed(federation, void 0, void 0, parseInt(opts.batchSize ?? "50", 10));
3112
3121
  });
3113
- memoryCmd.command("search <query>").description("Search indexed memory (BM25 keyword, semantic, or hybrid)").option("--project <slug>", "Restrict search to a specific project").option("--source <source>", "Restrict to 'memory' or 'notes'").option("--limit <n>", "Maximum results to return", "10").option("--mode <mode>", "Search mode: keyword (default), semantic, hybrid", "keyword").option("--no-rerank", "Skip cross-encoder reranking (reranking is on by default)").option("--recency <days>", "Apply recency boost: score halves every N days. 0 = off (default)", "0").action(async (query, opts) => {
3122
+ memoryCmd.command("search <query>").description("Search indexed memory (BM25 keyword, semantic, or hybrid)").option("--project <slug>", "Restrict search to a specific project").option("--source <source>", "Restrict to 'memory' or 'notes'").option("--limit <n>", "Maximum results to return").option("--mode <mode>", "Search mode: keyword (default), semantic, hybrid").option("--no-rerank", "Skip cross-encoder reranking (reranking is on by default)").option("--recency <days>", "Apply recency boost: score halves every N days. 0 = off").action(async (query, opts) => {
3114
3123
  const registryDb = getDb();
3115
3124
  let federation;
3116
3125
  try {
@@ -3119,8 +3128,9 @@ function registerMemoryCommands(memoryCmd, getDb) {
3119
3128
  console.error(err(`Failed to open federation database: ${e}`));
3120
3129
  process.exit(1);
3121
3130
  }
3122
- const maxResults = parseInt(opts.limit ?? "10", 10);
3123
- const mode = opts.mode ?? "keyword";
3131
+ const searchConfig = loadConfig$1().search;
3132
+ const maxResults = parseInt(opts.limit ?? String(searchConfig.defaultLimit), 10);
3133
+ const mode = opts.mode ?? searchConfig.mode;
3124
3134
  if (![
3125
3135
  "keyword",
3126
3136
  "semantic",
@@ -3199,7 +3209,7 @@ function registerMemoryCommands(memoryCmd, getDb) {
3199
3209
  console.log(dim("Reranking with cross-encoder..."));
3200
3210
  results = await rerankResults(query, results, { topK: maxResults });
3201
3211
  }
3202
- const recencyDays = parseInt(opts.recency ?? "0", 10);
3212
+ const recencyDays = parseInt(opts.recency ?? String(searchConfig.recencyBoostDays), 10);
3203
3213
  if (recencyDays > 0) {
3204
3214
  const { applyRecencyBoost } = await import("../search-_oHfguA5.mjs").then((n) => n.o);
3205
3215
  console.log(dim(`Applying recency boost (half-life: ${recencyDays} days)...`));
@@ -3289,6 +3299,81 @@ function registerMemoryCommands(memoryCmd, getDb) {
3289
3299
  console.log();
3290
3300
  }
3291
3301
  });
3302
+ memoryCmd.command("settings [key] [value]").description("View or modify search settings in ~/.config/pai/config.json").action((key, value) => {
3303
+ const search = loadConfig$1().search;
3304
+ if (!key) {
3305
+ console.log(`\n ${bold("PAI Memory — Search Settings")}\n`);
3306
+ console.log(` ${bold("mode:")} ${search.mode}`);
3307
+ console.log(` ${bold("rerank:")} ${search.rerank}`);
3308
+ console.log(` ${bold("recencyBoostDays:")} ${search.recencyBoostDays}`);
3309
+ console.log(` ${bold("defaultLimit:")} ${search.defaultLimit}`);
3310
+ console.log(` ${bold("snippetLength:")} ${search.snippetLength}`);
3311
+ console.log();
3312
+ console.log(dim(` Config file: ${CONFIG_FILE$2}`));
3313
+ console.log(dim(` Edit directly or use: pai memory settings <key> <value>`));
3314
+ console.log();
3315
+ return;
3316
+ }
3317
+ if (!value) {
3318
+ const val = search[key];
3319
+ if (val === void 0) {
3320
+ console.error(err(`Unknown setting: ${key}`));
3321
+ console.log(dim(` Valid keys: mode, rerank, recencyBoostDays, defaultLimit, snippetLength`));
3322
+ process.exit(1);
3323
+ }
3324
+ console.log(String(val));
3325
+ return;
3326
+ }
3327
+ const validKeys = new Set([
3328
+ "mode",
3329
+ "rerank",
3330
+ "recencyBoostDays",
3331
+ "defaultLimit",
3332
+ "snippetLength"
3333
+ ]);
3334
+ if (!validKeys.has(key)) {
3335
+ console.error(err(`Unknown setting: ${key}`));
3336
+ console.log(dim(` Valid keys: ${[...validKeys].join(", ")}`));
3337
+ process.exit(1);
3338
+ }
3339
+ let fileConfig = {};
3340
+ if (existsSync(CONFIG_FILE$2)) try {
3341
+ fileConfig = JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
3342
+ } catch {
3343
+ console.error(err(`Could not parse ${CONFIG_FILE$2}`));
3344
+ process.exit(1);
3345
+ }
3346
+ if (!fileConfig.search || typeof fileConfig.search !== "object") fileConfig.search = {};
3347
+ let parsed;
3348
+ if (key === "mode") {
3349
+ if (![
3350
+ "keyword",
3351
+ "semantic",
3352
+ "hybrid"
3353
+ ].includes(value)) {
3354
+ console.error(err(`Invalid mode: ${value}. Must be keyword, semantic, or hybrid.`));
3355
+ process.exit(1);
3356
+ }
3357
+ parsed = value;
3358
+ } else if (key === "rerank") parsed = value === "true" || value === "1" || value === "on";
3359
+ else {
3360
+ parsed = parseInt(value, 10);
3361
+ if (isNaN(parsed)) {
3362
+ console.error(err(`Invalid number: ${value}`));
3363
+ process.exit(1);
3364
+ }
3365
+ }
3366
+ fileConfig.search[key] = parsed;
3367
+ try {
3368
+ ensureConfigDir();
3369
+ writeFileSync(CONFIG_FILE$2, JSON.stringify(fileConfig, null, 2) + "\n", "utf-8");
3370
+ console.log(ok(`Set search.${key} = ${parsed}`));
3371
+ console.log(dim(` Restart daemon to apply: pai daemon restart`));
3372
+ } catch (e) {
3373
+ console.error(err(`Could not write config: ${e}`));
3374
+ process.exit(1);
3375
+ }
3376
+ });
3292
3377
  }
3293
3378
 
3294
3379
  //#endregion
@@ -3652,8 +3737,8 @@ function cmdLogs(opts) {
3652
3737
  }
3653
3738
  function registerDaemonCommands(daemonCmd) {
3654
3739
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3655
- const { serve } = await import("../daemon-s868Paua.mjs").then((n) => n.t);
3656
- const { loadConfig: lc, ensureConfigDir } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
3740
+ const { serve } = await import("../daemon-D9evGlgR.mjs").then((n) => n.t);
3741
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
3657
3742
  ensureConfigDir();
3658
3743
  await serve(lc());
3659
3744
  });