@tekmidian/pai 0.5.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.
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,50 @@ 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
+
170
198
  ---
171
199
 
172
200
  ## 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"}
@@ -7,9 +7,9 @@ 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
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";
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
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";
@@ -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
1814
  const { createStorageBackend } = await import("../factory-CeXQzlwn.mjs").then((n) => n.n);
1815
- const { loadConfig } = await import("../config-B4brrHHE.mjs").then((n) => n.r);
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,7 +2340,7 @@ 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);
2343
+ const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2344
2344
  const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2345
2345
  const config = loadConfig();
2346
2346
  if (config.storageBackend !== "postgres") return 0;
@@ -2367,7 +2367,7 @@ 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);
2370
+ const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2371
2371
  const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2372
2372
  const config = loadConfig();
2373
2373
  if (config.storageBackend !== "postgres") return 0;
@@ -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-a1W4KgFq.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
  });