@tekmidian/pai 0.4.0 → 0.5.0

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 (47) hide show
  1. package/ARCHITECTURE.md +16 -10
  2. package/README.md +46 -6
  3. package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
  4. package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +34 -22
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
  8. package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +7 -7
  10. package/dist/daemon-mcp/index.mjs +11 -4
  11. package/dist/daemon-mcp/index.mjs.map +1 -1
  12. package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
  13. package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
  14. package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
  15. package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  16. package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
  17. package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
  18. package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
  19. package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
  20. package/dist/index.d.mts +29 -1
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +4 -3
  23. package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
  24. package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
  25. package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
  26. package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
  27. package/dist/mcp/index.mjs +15 -5
  28. package/dist/mcp/index.mjs.map +1 -1
  29. package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
  30. package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
  31. package/dist/reranker-D7bRAHi6.mjs +71 -0
  32. package/dist/reranker-D7bRAHi6.mjs.map +1 -0
  33. package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
  34. package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
  35. package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
  36. package/dist/search-_oHfguA5.mjs.map +1 -0
  37. package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
  38. package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
  39. package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
  40. package/dist/tools-Dx7GjOHd.mjs.map +1 -0
  41. package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
  42. package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
  43. package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
  44. package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
  45. package/package.json +1 -1
  46. package/dist/search-GK0ibTJy.mjs.map +0 -1
  47. package/dist/tools-CUg0Lyg-.mjs.map +0 -1
package/ARCHITECTURE.md CHANGED
@@ -1,12 +1,8 @@
1
- ---
2
- links: "[[Ideaverse/AI/PAI/PAI|PAI]]"
3
- ---
4
-
5
1
  # PAI Knowledge OS — Architecture
6
2
 
7
3
  Technical reference for PAI's architecture, database schema, CLI commands, and development setup.
8
4
 
9
- For user-facing documentation, see [README.md](Ideaverse/AI/PAI/README.md) and [MANUAL.md](MANUAL.md).
5
+ For user-facing documentation, see [README.md](README.md) and [MANUAL.md](MANUAL.md).
10
6
 
11
7
  ---
12
8
 
@@ -174,7 +170,7 @@ Claude Code (stdio)
174
170
 
175
171
  ### Tool Reference
176
172
 
177
- **`memory_search(query, mode?, project?, limit?)`** — Search the indexed knowledge base. Returns ranked chunks with file paths and line numbers. `mode`: `keyword` (default), `semantic`, or `hybrid`.
173
+ **`memory_search(query, mode?, project?, limit?, rerank?)`** — Search the indexed knowledge base. Returns ranked chunks with file paths and line numbers. `mode`: `keyword` (default), `semantic`, or `hybrid`. Cross-encoder reranking is on by default; set `rerank: false` to skip it.
178
174
 
179
175
  **`memory_get(project, path)`** — Retrieve the complete contents of a specific file from a project's memory index.
180
176
 
@@ -243,6 +239,17 @@ Runs both keyword and semantic pipelines, normalizes each result set to a 0–1
243
239
  pai memory search --mode hybrid "rate limiting patterns"
244
240
  ```
245
241
 
242
+ ### Cross-Encoder Reranking (on by default)
243
+
244
+ All search results are automatically re-scored using a cross-encoder model (`Xenova/ms-marco-MiniLM-L-6-v2`, 23 MB quantized). Cross-encoders process (query, document) pairs jointly — more accurate than BM25 or bi-encoder cosine but slower since each pair is scored independently. Use `--no-rerank` to skip this step.
245
+
246
+ ```bash
247
+ pai memory search "PAI memory search implementation" --mode hybrid
248
+ pai memory search "PAI memory search implementation" --mode hybrid --no-rerank
249
+ ```
250
+
251
+ The reranker loads lazily on first use (downloads the model once, ~23 MB). Subsequent calls reuse the cached model. The MCP tool defaults to `rerank: true`; pass `rerank: false` to skip.
252
+
246
253
  ### Mode Comparison
247
254
 
248
255
  | Mode | Speed | Requires Embeddings | Best For |
@@ -250,6 +257,7 @@ pai memory search --mode hybrid "rate limiting patterns"
250
257
  | keyword | Fast | No | Exact terms, IDs, session numbers |
251
258
  | semantic | Medium | Yes | Concepts, paraphrases, cross-language |
252
259
  | hybrid | Medium | Yes | General-purpose, best quality |
260
+ | any (rerank default) | Slower | Model auto-downloads | All modes — best relevance ordering |
253
261
 
254
262
  ---
255
263
 
@@ -675,7 +683,8 @@ src/
675
683
  ├── federation/ # Federation schema definitions
676
684
  ├── hooks/ # Lifecycle hooks (pre-compact, session-stop)
677
685
  ├── mcp/ # Direct MCP server (legacy)
678
- ├── memory/ # Indexer, chunker, embeddings, search
686
+ ├── memory/ # Indexer, chunker, embeddings, search, reranker
687
+ │ ├── reranker.ts # Cross-encoder reranking (Xenova/ms-marco-MiniLM-L-6-v2)
679
688
  │ └── vault-indexer.ts # Obsidian vault indexing into v3 vault tables
680
689
  ├── obsidian/ # Obsidian vault bridge
681
690
  │ └── vault-fixer.ts # Repairs broken wikilinks and orphaned entries
@@ -704,6 +713,3 @@ src/
704
713
  ## License
705
714
 
706
715
  MIT
707
-
708
- ---
709
- *Links:* [[Ideaverse/AI/PAI/PAI|PAI]]
package/README.md CHANGED
@@ -1,7 +1,3 @@
1
- ---
2
- links: "[[Ideaverse/AI/PAI/PAI|PAI]]"
3
- ---
4
-
5
1
  # PAI Knowledge OS
6
2
 
7
3
  Claude Code has a memory problem. Every new session starts cold — no idea what you built yesterday, what decisions you made, or where you left off. You re-explain everything, every time. PAI fixes this.
@@ -127,6 +123,52 @@ For the technical deep-dive — architecture, database schema, CLI reference, an
127
123
 
128
124
  ---
129
125
 
126
+ ## Search Intelligence
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.
129
+
130
+ ### Search Modes
131
+
132
+ | Mode | How it works | Best for |
133
+ |------|-------------|----------|
134
+ | **Keyword** | Full-text search (BM25 via SQLite FTS5) | Exact terms, function names, error messages |
135
+ | **Semantic** | Vector similarity (Snowflake Arctic embeddings) | Finding things by meaning, even with different words |
136
+ | **Hybrid** | Keyword + semantic combined, scores normalized and blended | General use — the default |
137
+
138
+ ### Cross-Encoder Reranking
139
+
140
+ Every search automatically runs a second pass: a cross-encoder model reads each (query, result) pair together and re-scores them for relevance. This catches results that keyword or vector search ranked too low.
141
+
142
+ ```bash
143
+ # Search with reranking (default)
144
+ pai memory search "how does session routing work"
145
+
146
+ # Skip reranking for faster results
147
+ pai memory search "how does session routing work" --no-rerank
148
+ ```
149
+
150
+ The reranker uses a small local model (~23 MB) that runs entirely on your machine. First use downloads it automatically. No API keys, no cloud calls.
151
+
152
+ ### Recency Boost
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%.
157
+
158
+ ```bash
159
+ # Boost recent results (score halves every 90 days)
160
+ pai memory search "notification system" --recency 90
161
+
162
+ # Combine with any mode — works with keyword, semantic, hybrid, and reranking
163
+ pai memory search "notification system" --mode hybrid --recency 90
164
+ ```
165
+
166
+ Via MCP, pass `recency_boost: 90` to the `memory_search` tool. Set to 0 (default) to disable.
167
+
168
+ 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
+
130
172
  ## Zettelkasten Intelligence
131
173
 
132
174
  PAI implements Niklas Luhmann's Zettelkasten principles as six computational operations on your Obsidian vault.
@@ -176,5 +218,3 @@ PAI Knowledge OS is inspired by [Daniel Miessler](https://github.com/danielmiess
176
218
 
177
219
  MIT
178
220
 
179
- ---
180
- *Links:* [[Ideaverse/AI/PAI/PAI|PAI]]
@@ -1,5 +1,5 @@
1
1
  import { r as readPaiMarker } from "./pai-marker-CXQPX2P6.mjs";
2
- import { t as detectProject } from "./detect-D7gPV3fQ.mjs";
2
+ import { t as detectProject } from "./detect-CdaA48EI.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
 
@@ -26,7 +26,7 @@ async function autoRoute(registryDb, federation, cwd, context) {
26
26
  const markerResult = findMarkerUpward(registryDb, target);
27
27
  if (markerResult) return markerResult;
28
28
  if (context && context.trim().length > 0) {
29
- const { detectTopicShift } = await import("./detector-cYYhK2Mi.mjs").then((n) => n.n);
29
+ const { detectTopicShift } = await import("./detector-Bp-2SM3x.mjs").then((n) => n.n);
30
30
  const topicResult = await detectTopicShift(registryDb, federation, {
31
31
  context,
32
32
  threshold: .5
@@ -83,4 +83,4 @@ function formatAutoRouteJson(result) {
83
83
 
84
84
  //#endregion
85
85
  export { autoRoute, formatAutoRouteJson };
86
- //# sourceMappingURL=auto-route-JjW3f7pV.mjs.map
86
+ //# sourceMappingURL=auto-route-B5MSUJZK.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"auto-route-JjW3f7pV.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-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"}
@@ -6,11 +6,11 @@ import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQ
6
6
  import { n as openFederation } from "../db-Dp8VXIMR.mjs";
7
7
  import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CKQcgKsz.mjs";
8
8
  import "../embeddings-DGRAPAYb.mjs";
9
- import { n as populateSlugs, r as searchMemory } from "../search-GK0ibTJy.mjs";
10
- import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-D7gPV3fQ.mjs";
11
- import { t as PaiClient } from "../ipc-client-CLt2fNlC.mjs";
12
- import { a as expandHome, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-DELNqq3Z.mjs";
13
- import { t as createStorageBackend } from "../factory-DZLvRf4m.mjs";
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";
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-JjW3f7pV.mjs");
1812
+ const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-B5MSUJZK.mjs");
1813
1813
  const { openRegistry } = await import("../db-4lSqLFb8.mjs").then((n) => n.t);
1814
- const { createStorageBackend } = await import("../factory-DZLvRf4m.mjs").then((n) => n.n);
1815
- const { loadConfig } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
1814
+ const { createStorageBackend } = await import("../factory-CeXQzlwn.mjs").then((n) => n.n);
1815
+ const { loadConfig } = await import("../config-B4brrHHE.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-DELNqq3Z.mjs").then((n) => n.r);
2344
- const { PostgresBackend } = await import("../postgres-CRBe30Ag.mjs");
2343
+ const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
2344
+ const { PostgresBackend } = await import("../postgres-CIxeqf_n.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-DELNqq3Z.mjs").then((n) => n.r);
2371
- const { PostgresBackend } = await import("../postgres-CRBe30Ag.mjs");
2370
+ const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
2371
+ const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2372
2372
  const config = loadConfig();
2373
2373
  if (config.storageBackend !== "postgres") return 0;
2374
2374
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -3110,7 +3110,7 @@ function registerMemoryCommands(memoryCmd, getDb) {
3110
3110
  await runEmbed(federation, project.id, project.slug, parseInt(opts.batchSize ?? "50", 10));
3111
3111
  } else await runEmbed(federation, void 0, void 0, parseInt(opts.batchSize ?? "50", 10));
3112
3112
  });
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").action(async (query, opts) => {
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) => {
3114
3114
  const registryDb = getDb();
3115
3115
  let federation;
3116
3116
  try {
@@ -3194,8 +3194,20 @@ function registerMemoryCommands(memoryCmd, getDb) {
3194
3194
  console.log(dim(`No results found for: "${query}" (mode: ${mode})`));
3195
3195
  return;
3196
3196
  }
3197
+ if (opts.rerank !== false) {
3198
+ const { rerankResults } = await import("../reranker-D7bRAHi6.mjs").then((n) => n.r);
3199
+ console.log(dim("Reranking with cross-encoder..."));
3200
+ results = await rerankResults(query, results, { topK: maxResults });
3201
+ }
3202
+ const recencyDays = parseInt(opts.recency ?? "0", 10);
3203
+ if (recencyDays > 0) {
3204
+ const { applyRecencyBoost } = await import("../search-_oHfguA5.mjs").then((n) => n.o);
3205
+ console.log(dim(`Applying recency boost (half-life: ${recencyDays} days)...`));
3206
+ results = applyRecencyBoost(results, recencyDays);
3207
+ }
3197
3208
  const withSlugs = populateSlugs(results, registryDb);
3198
- const modeLabel = mode !== "keyword" ? ` [${mode}]` : "";
3209
+ const rerankLabel = opts.rerank !== false ? " +rerank" : "";
3210
+ const modeLabel = mode !== "keyword" ? ` [${mode}${rerankLabel}]` : opts.rerank !== false ? ` [rerank]` : "";
3199
3211
  console.log(`\n ${bold(`Search results for: "${query}"`)}${modeLabel} ${dim(`(${withSlugs.length} found)`)}\n`);
3200
3212
  for (const result of withSlugs) {
3201
3213
  const projectLabel = result.projectSlug ? chalk.cyan(result.projectSlug) : chalk.cyan(String(result.projectId));
@@ -3640,8 +3652,8 @@ function cmdLogs(opts) {
3640
3652
  }
3641
3653
  function registerDaemonCommands(daemonCmd) {
3642
3654
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3643
- const { serve } = await import("../daemon-CeTX4NpF.mjs").then((n) => n.t);
3644
- const { loadConfig: lc, ensureConfigDir } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
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);
3645
3657
  ensureConfigDir();
3646
3658
  await serve(lc());
3647
3659
  });
@@ -6087,7 +6099,7 @@ async function cmdExplore(note, opts) {
6087
6099
  const depth = parseInt(opts.depth ?? "3", 10);
6088
6100
  const direction = opts.direction ?? "both";
6089
6101
  const mode = opts.mode ?? "all";
6090
- const { zettelExplore } = await import("../zettelkasten-Co-w0XSZ.mjs");
6102
+ const { zettelExplore } = await import("../zettelkasten-e-a4rW_6.mjs");
6091
6103
  const result = zettelExplore(getFedDb(), {
6092
6104
  startNote: note,
6093
6105
  depth,
@@ -6138,7 +6150,7 @@ async function cmdHealth(opts) {
6138
6150
  const projectPath = opts.project;
6139
6151
  const recentDays = parseInt(opts.days ?? "30", 10);
6140
6152
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
6141
- const { zettelHealth } = await import("../zettelkasten-Co-w0XSZ.mjs");
6153
+ const { zettelHealth } = await import("../zettelkasten-e-a4rW_6.mjs");
6142
6154
  const result = zettelHealth(getFedDb(), {
6143
6155
  scope,
6144
6156
  projectPath,
@@ -6194,7 +6206,7 @@ async function cmdSurprise(note, opts) {
6194
6206
  const limit = parseInt(opts.limit ?? "10", 10);
6195
6207
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
6196
6208
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
6197
- const { zettelSurprise } = await import("../zettelkasten-Co-w0XSZ.mjs");
6209
+ const { zettelSurprise } = await import("../zettelkasten-e-a4rW_6.mjs");
6198
6210
  const db = getFedDb();
6199
6211
  console.log();
6200
6212
  console.log(header(" PAI Zettel Surprise"));
@@ -6234,7 +6246,7 @@ async function cmdSuggest(note, opts) {
6234
6246
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6235
6247
  const limit = parseInt(opts.limit ?? "5", 10);
6236
6248
  const excludeLinked = opts.excludeLinked !== false;
6237
- const { zettelSuggest } = await import("../zettelkasten-Co-w0XSZ.mjs");
6249
+ const { zettelSuggest } = await import("../zettelkasten-e-a4rW_6.mjs");
6238
6250
  const db = getFedDb();
6239
6251
  console.log();
6240
6252
  console.log(header(" PAI Zettel Suggest"));
@@ -6272,7 +6284,7 @@ async function cmdConverse(question, opts) {
6272
6284
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6273
6285
  const depth = parseInt(opts.depth ?? "2", 10);
6274
6286
  const limit = parseInt(opts.limit ?? "15", 10);
6275
- const { zettelConverse } = await import("../zettelkasten-Co-w0XSZ.mjs");
6287
+ const { zettelConverse } = await import("../zettelkasten-e-a4rW_6.mjs");
6276
6288
  const db = getFedDb();
6277
6289
  console.log();
6278
6290
  console.log(header(" PAI Zettel Converse"));
@@ -6320,7 +6332,7 @@ async function cmdThemes(opts) {
6320
6332
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
6321
6333
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
6322
6334
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
6323
- const { zettelThemes } = await import("../zettelkasten-Co-w0XSZ.mjs");
6335
+ const { zettelThemes } = await import("../zettelkasten-e-a4rW_6.mjs");
6324
6336
  const db = getFedDb();
6325
6337
  console.log();
6326
6338
  console.log(header(" PAI Zettel Themes"));