@tekmidian/pai 0.4.0 → 0.5.1

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 (48) hide show
  1. package/ARCHITECTURE.md +16 -10
  2. package/README.md +74 -6
  3. package/dist/{auto-route-JjW3f7pV.mjs → auto-route-BG6I_4B1.mjs} +3 -3
  4. package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-BG6I_4B1.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +121 -24
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/{config-DELNqq3Z.mjs → config-Cf92lGX_.mjs} +17 -3
  8. package/dist/config-Cf92lGX_.mjs.map +1 -0
  9. package/dist/daemon/index.mjs +7 -7
  10. package/dist/{daemon-CeTX4NpF.mjs → daemon-a1W4KgFq.mjs} +12 -12
  11. package/dist/{daemon-CeTX4NpF.mjs.map → daemon-a1W4KgFq.mjs.map} +1 -1
  12. package/dist/daemon-mcp/index.mjs +13 -4
  13. package/dist/daemon-mcp/index.mjs.map +1 -1
  14. package/dist/{detect-D7gPV3fQ.mjs → detect-BU3Nx_2L.mjs} +1 -1
  15. package/dist/{detect-D7gPV3fQ.mjs.map → detect-BU3Nx_2L.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 +5 -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-Bjg_a1dc.mjs} +1 -1
  26. package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-Bjg_a1dc.mjs.map} +1 -1
  27. package/dist/mcp/index.mjs +19 -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-DV_lsiCc.mjs} +29 -18
  40. package/dist/tools-DV_lsiCc.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/config-DELNqq3Z.mjs.map +0 -1
  47. package/dist/search-GK0ibTJy.mjs.map +0 -1
  48. 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,80 @@ 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, with reranking and recency boost on by default. All search settings are configurable.
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
+ 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%.
155
+
156
+ ```bash
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
162
+
163
+ # Disable recency boost for this search
164
+ pai memory search "notification system" --recency 0
165
+ ```
166
+
167
+ Via MCP, pass `recency_boost: 90` to the `memory_search` tool, or `recency_boost: 0` to disable.
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.
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
+ ---
199
+
130
200
  ## Zettelkasten Intelligence
131
201
 
132
202
  PAI implements Niklas Luhmann's Zettelkasten principles as six computational operations on your Obsidian vault.
@@ -176,5 +246,3 @@ PAI Knowledge OS is inspired by [Daniel Miessler](https://github.com/danielmiess
176
246
 
177
247
  MIT
178
248
 
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-BU3Nx_2L.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-BG6I_4B1.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-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"}
@@ -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 { 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-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-BG6I_4B1.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-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-DELNqq3Z.mjs").then((n) => n.r);
2344
- const { PostgresBackend } = await import("../postgres-CRBe30Ag.mjs");
2343
+ const { loadConfig } = await import("../config-Cf92lGX_.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-Cf92lGX_.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 ?? {});
@@ -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").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",
@@ -3194,8 +3204,20 @@ function registerMemoryCommands(memoryCmd, getDb) {
3194
3204
  console.log(dim(`No results found for: "${query}" (mode: ${mode})`));
3195
3205
  return;
3196
3206
  }
3207
+ if (opts.rerank !== false) {
3208
+ const { rerankResults } = await import("../reranker-D7bRAHi6.mjs").then((n) => n.r);
3209
+ console.log(dim("Reranking with cross-encoder..."));
3210
+ results = await rerankResults(query, results, { topK: maxResults });
3211
+ }
3212
+ const recencyDays = parseInt(opts.recency ?? String(searchConfig.recencyBoostDays), 10);
3213
+ if (recencyDays > 0) {
3214
+ const { applyRecencyBoost } = await import("../search-_oHfguA5.mjs").then((n) => n.o);
3215
+ console.log(dim(`Applying recency boost (half-life: ${recencyDays} days)...`));
3216
+ results = applyRecencyBoost(results, recencyDays);
3217
+ }
3197
3218
  const withSlugs = populateSlugs(results, registryDb);
3198
- const modeLabel = mode !== "keyword" ? ` [${mode}]` : "";
3219
+ const rerankLabel = opts.rerank !== false ? " +rerank" : "";
3220
+ const modeLabel = mode !== "keyword" ? ` [${mode}${rerankLabel}]` : opts.rerank !== false ? ` [rerank]` : "";
3199
3221
  console.log(`\n ${bold(`Search results for: "${query}"`)}${modeLabel} ${dim(`(${withSlugs.length} found)`)}\n`);
3200
3222
  for (const result of withSlugs) {
3201
3223
  const projectLabel = result.projectSlug ? chalk.cyan(result.projectSlug) : chalk.cyan(String(result.projectId));
@@ -3277,6 +3299,81 @@ function registerMemoryCommands(memoryCmd, getDb) {
3277
3299
  console.log();
3278
3300
  }
3279
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
+ });
3280
3377
  }
3281
3378
 
3282
3379
  //#endregion
@@ -3640,8 +3737,8 @@ function cmdLogs(opts) {
3640
3737
  }
3641
3738
  function registerDaemonCommands(daemonCmd) {
3642
3739
  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);
3740
+ const { serve } = await import("../daemon-a1W4KgFq.mjs").then((n) => n.t);
3741
+ const { loadConfig: lc, ensureConfigDir } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
3645
3742
  ensureConfigDir();
3646
3743
  await serve(lc());
3647
3744
  });
@@ -6087,7 +6184,7 @@ async function cmdExplore(note, opts) {
6087
6184
  const depth = parseInt(opts.depth ?? "3", 10);
6088
6185
  const direction = opts.direction ?? "both";
6089
6186
  const mode = opts.mode ?? "all";
6090
- const { zettelExplore } = await import("../zettelkasten-Co-w0XSZ.mjs");
6187
+ const { zettelExplore } = await import("../zettelkasten-e-a4rW_6.mjs");
6091
6188
  const result = zettelExplore(getFedDb(), {
6092
6189
  startNote: note,
6093
6190
  depth,
@@ -6138,7 +6235,7 @@ async function cmdHealth(opts) {
6138
6235
  const projectPath = opts.project;
6139
6236
  const recentDays = parseInt(opts.days ?? "30", 10);
6140
6237
  const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
6141
- const { zettelHealth } = await import("../zettelkasten-Co-w0XSZ.mjs");
6238
+ const { zettelHealth } = await import("../zettelkasten-e-a4rW_6.mjs");
6142
6239
  const result = zettelHealth(getFedDb(), {
6143
6240
  scope,
6144
6241
  projectPath,
@@ -6194,7 +6291,7 @@ async function cmdSurprise(note, opts) {
6194
6291
  const limit = parseInt(opts.limit ?? "10", 10);
6195
6292
  const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
6196
6293
  const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
6197
- const { zettelSurprise } = await import("../zettelkasten-Co-w0XSZ.mjs");
6294
+ const { zettelSurprise } = await import("../zettelkasten-e-a4rW_6.mjs");
6198
6295
  const db = getFedDb();
6199
6296
  console.log();
6200
6297
  console.log(header(" PAI Zettel Surprise"));
@@ -6234,7 +6331,7 @@ async function cmdSuggest(note, opts) {
6234
6331
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6235
6332
  const limit = parseInt(opts.limit ?? "5", 10);
6236
6333
  const excludeLinked = opts.excludeLinked !== false;
6237
- const { zettelSuggest } = await import("../zettelkasten-Co-w0XSZ.mjs");
6334
+ const { zettelSuggest } = await import("../zettelkasten-e-a4rW_6.mjs");
6238
6335
  const db = getFedDb();
6239
6336
  console.log();
6240
6337
  console.log(header(" PAI Zettel Suggest"));
@@ -6272,7 +6369,7 @@ async function cmdConverse(question, opts) {
6272
6369
  const vaultProjectId = parseInt(opts.vaultProjectId, 10);
6273
6370
  const depth = parseInt(opts.depth ?? "2", 10);
6274
6371
  const limit = parseInt(opts.limit ?? "15", 10);
6275
- const { zettelConverse } = await import("../zettelkasten-Co-w0XSZ.mjs");
6372
+ const { zettelConverse } = await import("../zettelkasten-e-a4rW_6.mjs");
6276
6373
  const db = getFedDb();
6277
6374
  console.log();
6278
6375
  console.log(header(" PAI Zettel Converse"));
@@ -6320,7 +6417,7 @@ async function cmdThemes(opts) {
6320
6417
  const minClusterSize = parseInt(opts.minSize ?? "3", 10);
6321
6418
  const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
6322
6419
  const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
6323
- const { zettelThemes } = await import("../zettelkasten-Co-w0XSZ.mjs");
6420
+ const { zettelThemes } = await import("../zettelkasten-e-a4rW_6.mjs");
6324
6421
  const db = getFedDb();
6325
6422
  console.log();
6326
6423
  console.log(header(" PAI Zettel Themes"));