@coreyuan/vector-mind 1.0.38 → 1.0.41

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/dist/index.js CHANGED
@@ -14,9 +14,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
14
14
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
15
15
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
17
- import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
17
+ import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
18
18
  const SERVER_NAME = "vector-mind";
19
- const SERVER_VERSION = "1.0.38";
19
+ const SERVER_VERSION = "1.0.41";
20
20
  const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
21
21
  const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
22
22
  const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
@@ -164,9 +164,13 @@ let getConventionByKeyStmt;
164
164
  let insertConventionStmt;
165
165
  let updateConventionByIdStmt;
166
166
  let listConventionsStmt;
167
+ let upsertDecisionStmt;
168
+ let getDecisionByKeyStmt;
169
+ let listCurrentDecisionsStmt;
167
170
  let upsertProjectSummaryStmt;
168
171
  let getProjectSummaryStmt;
169
172
  let listRecentNotesStmt;
173
+ let getLatestChangeIntentForFileStmt;
170
174
  let deleteFileChunkItemsStmt;
171
175
  let getEmbeddingMetaStmt;
172
176
  let upsertEmbeddingStmt;
@@ -181,6 +185,10 @@ let deleteOldestPendingChangesStmt = null;
181
185
  let deleteSymbolsForFileStmt;
182
186
  let upsertSymbolStmt;
183
187
  let searchSymbolsStmt;
188
+ let insertTokenSavingsStmt;
189
+ let summarizeTokenSavingsStmt;
190
+ let summarizeTokenSavingsByToolStmt;
191
+ let listRecentTokenSavingsStmt;
184
192
  let indexFileSymbolsTx = null;
185
193
  let activitySeq = 0;
186
194
  const activityLog = [];
@@ -552,6 +560,89 @@ function shouldIgnoreDbFilePath(filePath) {
552
560
  return false;
553
561
  return pathHasIgnoredSegments(filePath);
554
562
  }
563
+ function isProbablyGitRepository() {
564
+ try {
565
+ return fs.existsSync(path.join(projectRoot, ".git"));
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ }
571
+ function normalizeGitStatusPath(raw) {
572
+ const first = raw.split("\0")[0] ?? "";
573
+ return first.trim().replace(/\\/g, "/").replace(/^"(.*)"$/, "$1");
574
+ }
575
+ function collectGitPendingChanges(limit) {
576
+ if (limit <= 0 || !isProbablyGitRepository())
577
+ return [];
578
+ const git = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=normal"], {
579
+ cwd: projectRoot,
580
+ encoding: "utf8",
581
+ timeout: 5000,
582
+ windowsHide: true,
583
+ maxBuffer: 2_000_000,
584
+ });
585
+ if (git.error || git.status !== 0 || !git.stdout)
586
+ return [];
587
+ const parts = git.stdout.split("\0").filter(Boolean);
588
+ const rows = [];
589
+ for (let i = 0; i < parts.length && rows.length < limit; i++) {
590
+ const rec = parts[i] ?? "";
591
+ const status = rec.slice(0, 2);
592
+ let rawPath = rec.slice(3);
593
+ if (status.startsWith("R") || status.startsWith("C")) {
594
+ // Porcelain -z rename/copy records include the destination in the next NUL field.
595
+ rawPath = parts[i + 1] ?? rawPath;
596
+ i += 1;
597
+ }
598
+ const filePath = normalizeGitStatusPath(rawPath);
599
+ if (!filePath || filePath === ".vectormind" || filePath.startsWith(".vectormind/"))
600
+ continue;
601
+ rows.push({
602
+ file_path: filePath,
603
+ last_event: status.includes("D") ? "unlink" : status === "??" ? "add" : "change",
604
+ updated_at: new Date().toISOString(),
605
+ source: "git",
606
+ git_status: status.trim() || "modified",
607
+ file_state_hash: getFileStateHash(filePath) ?? undefined,
608
+ });
609
+ }
610
+ return rows;
611
+ }
612
+ function mergePendingWithGit(pending, opts) {
613
+ const byPath = new Map();
614
+ for (const p of pending) {
615
+ if (shouldIgnoreDbFilePath(p.file_path))
616
+ continue;
617
+ byPath.set(p.file_path, { ...p, source: p.source ?? "watcher" });
618
+ }
619
+ const gitRows = collectGitPendingChanges(Math.max(500, opts.offset + opts.limit * 4));
620
+ for (const g of gitRows) {
621
+ const latestSyncedHash = getLatestSyncedFileHash(g.file_path);
622
+ if (latestSyncedHash && g.file_state_hash && latestSyncedHash === g.file_state_hash)
623
+ continue;
624
+ const existing = byPath.get(g.file_path);
625
+ if (!existing) {
626
+ byPath.set(g.file_path, g);
627
+ continue;
628
+ }
629
+ byPath.set(g.file_path, {
630
+ ...existing,
631
+ source: existing.source === "watcher" ? "watcher" : g.source,
632
+ git_status: g.git_status,
633
+ file_state_hash: g.file_state_hash,
634
+ });
635
+ }
636
+ const all = Array.from(byPath.values()).sort((a, b) => {
637
+ const at = Date.parse(a.updated_at) || 0;
638
+ const bt = Date.parse(b.updated_at) || 0;
639
+ if (bt !== at)
640
+ return bt - at;
641
+ return a.file_path.localeCompare(b.file_path);
642
+ });
643
+ const page = all.slice(opts.offset, opts.offset + opts.limit);
644
+ return { total: all.length, page, truncated: all.length > opts.offset + opts.limit };
645
+ }
555
646
  function pruneIgnoredPendingChanges() {
556
647
  if (!db)
557
648
  return;
@@ -1149,6 +1240,9 @@ function removeFileIndexes(absPath) {
1149
1240
  const ProjectRootArgSchema = z.object({
1150
1241
  project_root: z.string().optional(),
1151
1242
  });
1243
+ const OutputFormatSchema = z.object({
1244
+ format: z.enum(["compact", "json"]).optional().default("compact"),
1245
+ });
1152
1246
  const StartRequirementArgsSchema = ProjectRootArgSchema.merge(z.object({
1153
1247
  title: z.string().min(1),
1154
1248
  background: z.string().optional().default(""),
@@ -1159,10 +1253,10 @@ const SyncChangeIntentArgsSchema = ProjectRootArgSchema.merge(z.object({
1159
1253
  files: z.array(z.string().min(1)).optional(),
1160
1254
  affected_files: z.array(z.string().min(1)).optional(),
1161
1255
  }));
1162
- const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(z.object({
1256
+ const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1163
1257
  query: z.string().min(1),
1164
1258
  }));
1165
- const GrepArgsSchema = ProjectRootArgSchema.merge(z.object({
1259
+ const GrepArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1166
1260
  // Pattern to search for. Defaults to regex mode for parity with tools like ripgrep.
1167
1261
  query: z.string().min(1),
1168
1262
  mode: z.enum(["regex", "literal"]).optional().default("regex"),
@@ -1179,7 +1273,7 @@ const GrepArgsSchema = ProjectRootArgSchema.merge(z.object({
1179
1273
  // Compatibility knob for the indexed fallback when ripgrep is unavailable.
1180
1274
  max_candidates: z.number().int().min(1).max(50_000).optional(),
1181
1275
  }));
1182
- const ReadFileLinesArgsSchema = ProjectRootArgSchema.merge(z.object({
1276
+ const ReadFileLinesArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1183
1277
  // Relative to project_root, or an absolute path under project_root.
1184
1278
  path: z.string().min(1),
1185
1279
  from_line: z.number().int().min(1).optional().default(1),
@@ -1190,7 +1284,7 @@ const ReadFileLinesArgsSchema = ProjectRootArgSchema.merge(z.object({
1190
1284
  max_lines: z.number().int().min(1).max(2000).optional().default(400),
1191
1285
  max_chars: z.number().int().min(200).max(200_000).optional().default(20_000),
1192
1286
  }));
1193
- const ReadFileTextArgsSchema = ProjectRootArgSchema.merge(z.object({
1287
+ const ReadFileTextArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1194
1288
  // Relative to project_root, or an absolute path under project_root.
1195
1289
  path: z.string().min(1),
1196
1290
  // Character offset in the decoded UTF-8 text.
@@ -1200,14 +1294,14 @@ const ReadFileTextArgsSchema = ProjectRootArgSchema.merge(z.object({
1200
1294
  // Safety guard for raw reads; use read_file_lines on larger files.
1201
1295
  max_file_bytes: z.number().int().min(1_000).max(5_000_000).optional().default(1_000_000),
1202
1296
  }));
1203
- const ReadCodexTextFileArgsSchema = ProjectRootArgSchema.merge(z.object({
1297
+ const ReadCodexTextFileArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1204
1298
  // Absolute path, file:// URI, or a path under CODEX_HOME / AGENTS_HOME allowed roots.
1205
1299
  path: z.string().min(1),
1206
1300
  offset: z.number().int().min(0).optional().default(0),
1207
1301
  max_chars: z.number().int().min(1).max(200_000).optional().default(20_000),
1208
1302
  max_file_bytes: z.number().int().min(1_000).max(5_000_000).optional().default(1_000_000),
1209
1303
  }));
1210
- const ListProjectFilesArgsSchema = ProjectRootArgSchema.merge(z.object({
1304
+ const ListProjectFilesArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1211
1305
  // Relative directory/file path under project_root. "." means the project root.
1212
1306
  path: z.string().optional().default("."),
1213
1307
  recursive: z.boolean().optional().default(false),
@@ -1242,29 +1336,48 @@ const UpsertConventionArgsSchema = ProjectRootArgSchema.merge(z.object({
1242
1336
  content: z.string().min(1),
1243
1337
  tags: z.array(z.string().min(1)).optional(),
1244
1338
  }));
1245
- const DEFAULT_PENDING_LIMIT = 50;
1339
+ const UpsertDecisionArgsSchema = ProjectRootArgSchema.merge(z.object({
1340
+ key: z.string().min(1),
1341
+ title: z.string().optional().default(""),
1342
+ content: z.string().min(1),
1343
+ tags: z.array(z.string().min(1)).optional(),
1344
+ supersedes_req_ids: z.array(z.number().int().positive()).optional(),
1345
+ supersedes_memory_ids: z.array(z.number().int().positive()).optional(),
1346
+ related_files: z.array(z.string().min(1)).optional(),
1347
+ }));
1348
+ const SupersedeMemoryArgsSchema = ProjectRootArgSchema.merge(z.object({
1349
+ superseded_req_ids: z.array(z.number().int().positive()).optional(),
1350
+ superseded_memory_ids: z.array(z.number().int().positive()).optional(),
1351
+ replacement_req_id: z.number().int().positive().optional(),
1352
+ replacement_memory_id: z.number().int().positive().optional(),
1353
+ reason: z.string().min(1),
1354
+ }));
1355
+ const DEFAULT_PENDING_LIMIT = 10;
1246
1356
  const MAX_PENDING_LIMIT = 2000;
1247
1357
  const PendingPagingSchema = z.object({
1248
1358
  pending_offset: z.number().int().min(0).optional().default(0),
1249
1359
  pending_limit: z.number().int().min(1).max(MAX_PENDING_LIMIT).optional().default(DEFAULT_PENDING_LIMIT),
1250
1360
  });
1251
- const DEFAULT_PREVIEW_CHARS = 200;
1361
+ const DEFAULT_PREVIEW_CHARS = 120;
1252
1362
  const PreviewSchema = z.object({
1253
1363
  preview_chars: z.number().int().min(50).max(10_000).optional().default(DEFAULT_PREVIEW_CHARS),
1254
1364
  });
1255
- const DEFAULT_CONTENT_MAX_CHARS = 2000;
1365
+ const DEFAULT_CONTENT_MAX_CHARS = 1200;
1256
1366
  const ContentMaxSchema = z.object({
1257
1367
  content_max_chars: z.number().int().min(0).max(200_000).optional().default(DEFAULT_CONTENT_MAX_CHARS),
1258
1368
  });
1259
- const DEFAULT_RECENT_REQUIREMENTS = 3;
1260
- const DEFAULT_RECENT_CHANGES_PER_REQ = 5;
1261
- const DEFAULT_RECENT_NOTES = 5;
1262
- const DEFAULT_CONVENTIONS_LIMIT = 20;
1369
+ const DEFAULT_RECENT_REQUIREMENTS = 2;
1370
+ const DEFAULT_RECENT_CHANGES_PER_REQ = 3;
1371
+ const DEFAULT_RECENT_NOTES = 3;
1372
+ const DEFAULT_CONVENTIONS_LIMIT = 0;
1373
+ const DEFAULT_DECISIONS_LIMIT = 5;
1374
+ const MAX_DECISIONS_LIMIT = 50;
1263
1375
  const BrainDumpLimitsSchema = z.object({
1264
1376
  requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
1265
1377
  changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
1266
1378
  notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
1267
1379
  conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
1380
+ decisions_limit: z.number().int().min(0).max(MAX_DECISIONS_LIMIT).optional().default(DEFAULT_DECISIONS_LIMIT),
1268
1381
  });
1269
1382
  const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
1270
1383
  offset: z.number().int().min(0).optional().default(0),
@@ -1285,6 +1398,7 @@ const GetActivitySummaryArgsSchema = ProjectRootArgSchema.merge(z.object({
1285
1398
  }));
1286
1399
  const ClearActivityLogArgsSchema = ProjectRootArgSchema;
1287
1400
  const GetBrainDumpArgsSchema = ProjectRootArgSchema.merge(PendingPagingSchema)
1401
+ .merge(OutputFormatSchema)
1288
1402
  .merge(PreviewSchema)
1289
1403
  .merge(ContentMaxSchema)
1290
1404
  .merge(BrainDumpLimitsSchema)
@@ -1293,16 +1407,17 @@ const GetBrainDumpArgsSchema = ProjectRootArgSchema.merge(PendingPagingSchema)
1293
1407
  }));
1294
1408
  const BootstrapContextArgsSchema = ProjectRootArgSchema.merge(z.object({
1295
1409
  query: z.string().optional(),
1296
- top_k: z.number().int().min(1).max(50).optional().default(5),
1410
+ top_k: z.number().int().min(1).max(50).optional().default(3),
1297
1411
  kinds: z.array(z.string().min(1)).optional(),
1298
1412
  include_content: z.boolean().optional().default(false),
1299
1413
  pending_offset: z.number().int().min(0).optional().default(0),
1300
1414
  pending_limit: z.number().int().min(1).max(MAX_PENDING_LIMIT).optional().default(DEFAULT_PENDING_LIMIT),
1301
1415
  })
1416
+ .merge(OutputFormatSchema)
1302
1417
  .merge(PreviewSchema)
1303
1418
  .merge(ContentMaxSchema)
1304
1419
  .merge(BrainDumpLimitsSchema));
1305
- const SemanticSearchArgsSchema = ProjectRootArgSchema.merge(z.object({
1420
+ const SemanticSearchArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1306
1421
  query: z.string().min(1),
1307
1422
  top_k: z.number().int().min(1).max(50).optional().default(8),
1308
1423
  kinds: z.array(z.string().min(1)).optional(),
@@ -1311,6 +1426,21 @@ const SemanticSearchArgsSchema = ProjectRootArgSchema.merge(z.object({
1311
1426
  content_max_chars: z.number().int().min(0).max(200_000).optional().default(DEFAULT_CONTENT_MAX_CHARS),
1312
1427
  }));
1313
1428
  const ProjectRootOnlyArgsSchema = ProjectRootArgSchema;
1429
+ const GetTokenSavingsArgsSchema = ProjectRootArgSchema.merge(z.object({
1430
+ limit: z.number().int().min(1).max(100).optional().default(10),
1431
+ format: z.enum(["compact", "json"]).optional().default("compact"),
1432
+ }));
1433
+ const DetectRtkArgsSchema = ProjectRootArgSchema;
1434
+ const InstallRtkArgsSchema = ProjectRootArgSchema.merge(z.object({
1435
+ dry_run: z.boolean().optional().default(true),
1436
+ method: z.enum(["auto", "cargo", "brew", "shell_script"]).optional().default("auto"),
1437
+ init: z
1438
+ .enum(["none", "global_no_patch", "global_auto_patch", "global_hook_only", "local", "codex_global", "codex_local"])
1439
+ .optional()
1440
+ .default("none"),
1441
+ uninstall_wrong_cargo_rtk: z.boolean().optional().default(false),
1442
+ timeout_ms: z.number().int().min(10_000).max(1_800_000).optional().default(600_000),
1443
+ }));
1314
1444
  const ReadMemoryItemArgsSchema = ProjectRootArgSchema.merge(z.object({
1315
1445
  id: z.number().int().positive(),
1316
1446
  offset: z.number().int().min(0).optional().default(0),
@@ -1322,6 +1452,28 @@ function escapeLike(pattern) {
1322
1452
  function sha256Hex(input) {
1323
1453
  return crypto.createHash("sha256").update(input).digest("hex");
1324
1454
  }
1455
+ function getFileStateHash(dbOrAbsPath) {
1456
+ try {
1457
+ const abs = path.isAbsolute(dbOrAbsPath) ? dbOrAbsPath : path.join(projectRoot, dbOrAbsPath);
1458
+ const st = fs.statSync(abs);
1459
+ if (!st.isFile())
1460
+ return sha256Hex(`non-file:${st.mtimeMs}:${st.size}`);
1461
+ if (st.size <= 5_000_000) {
1462
+ return crypto.createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
1463
+ }
1464
+ return sha256Hex(`large:${st.size}:${Math.floor(st.mtimeMs)}`);
1465
+ }
1466
+ catch {
1467
+ return sha256Hex("missing");
1468
+ }
1469
+ }
1470
+ function getLatestSyncedFileHash(dbFilePath) {
1471
+ const row = getLatestChangeIntentForFileStmt?.get(dbFilePath);
1472
+ if (!row)
1473
+ return null;
1474
+ const meta = parseMetadataJson(row.metadata_json);
1475
+ return typeof meta.file_state_hash === "string" ? meta.file_state_hash : null;
1476
+ }
1325
1477
  function safeJson(value) {
1326
1478
  if (value === undefined)
1327
1479
  return null;
@@ -1335,6 +1487,511 @@ function safeJson(value) {
1335
1487
  function toolJson(value) {
1336
1488
  return JSON.stringify(value, null, prettyJsonOutput ? 2 : undefined);
1337
1489
  }
1490
+ function estimateTokens(text) {
1491
+ if (!text)
1492
+ return 0;
1493
+ return Math.ceil(text.length / 4);
1494
+ }
1495
+ function recordTokenSavings(tool, rawText, outputText) {
1496
+ if (!db || !insertTokenSavingsStmt)
1497
+ return;
1498
+ const rawTokens = estimateTokens(rawText);
1499
+ const outputTokens = estimateTokens(outputText);
1500
+ const savedTokens = Math.max(0, rawTokens - outputTokens);
1501
+ const savingsPct = rawTokens > 0 ? (savedTokens / rawTokens) * 100 : 0;
1502
+ try {
1503
+ insertTokenSavingsStmt.run(tool, rawTokens, outputTokens, savedTokens, savingsPct);
1504
+ }
1505
+ catch (err) {
1506
+ console.error("[vectormind] token savings record failed:", err);
1507
+ }
1508
+ }
1509
+ function toolText(tool, rawValue, compactText, format = "compact") {
1510
+ const rawText = toolJson(rawValue);
1511
+ if (format === "json")
1512
+ return rawText;
1513
+ recordTokenSavings(tool, rawText, compactText);
1514
+ return compactText;
1515
+ }
1516
+ function toolCompactOrJson(tool, rawValue, compactText, format) {
1517
+ return toolText(tool, rawValue, compactText, format);
1518
+ }
1519
+ function oneLine(input, max = 120) {
1520
+ const text = (input ?? "").replace(/\s+/g, " ").trim();
1521
+ if (text.length <= max)
1522
+ return text;
1523
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
1524
+ }
1525
+ function compactMemoryLabel(item, max = 120) {
1526
+ const title = item.title ? ` ${oneLine(item.title, 48)}` : "";
1527
+ const loc = item.file_path ? ` ${item.file_path}${item.start_line != null ? `:${item.start_line}` : ""}` : "";
1528
+ const body = item.preview ? ` — ${oneLine(item.preview, max)}` : "";
1529
+ return `#${item.id} ${item.kind}${title}${loc}${body}`;
1530
+ }
1531
+ function compactRequirementLabel(req) {
1532
+ const ctx = req.context_preview ? ` — ${oneLine(req.context_preview, 100)}` : "";
1533
+ const mem = req.memory_item_id ? ` mem#${req.memory_item_id}` : "";
1534
+ return `req#${req.id}${mem} [${req.status}] ${oneLine(req.title, 80)}${ctx}`;
1535
+ }
1536
+ function compactChangeLabel(change) {
1537
+ return `change#${change.id} ${change.file_path}: ${oneLine(change.intent_preview, 120)}`;
1538
+ }
1539
+ function compactPendingLabel(p) {
1540
+ const source = "source" in p && p.source === "git" ? " git" : "";
1541
+ const status = "git_status" in p && p.git_status ? ` ${p.git_status}` : "";
1542
+ return `${p.last_event}${source}${status} ${p.file_path}`;
1543
+ }
1544
+ function compactSemanticSearchText(data) {
1545
+ const lines = [
1546
+ `semantic ${data.mode} ${data.matches.length}/${data.top_k} q="${oneLine(data.query, 100)}"`,
1547
+ ];
1548
+ for (const m of data.matches.slice(0, data.top_k)) {
1549
+ lines.push(`- score=${m.score.toFixed(3)} ${compactMemoryLabel(m.item, 160)}`);
1550
+ }
1551
+ if (!data.matches.length)
1552
+ lines.push("- no matches");
1553
+ lines.push("hint: use format=json for full metadata; read_memory_item(id) for full content");
1554
+ return lines.join("\n");
1555
+ }
1556
+ function compactGrepText(data) {
1557
+ const total = data.total_matches ?? data.matches.length;
1558
+ const fallback = data.fallback_reason ? ` fallback=${data.fallback_reason}` : "";
1559
+ const candidateText = data.candidates ? ` candidates=${data.candidates.scanned}/${data.candidates.total}` : "";
1560
+ const lines = [
1561
+ `grep ${data.backend}${fallback} mode=${data.mode} matches=${data.matches.length}/${total} truncated=${data.truncated}${candidateText} q="${oneLine(data.query, 100)}"`,
1562
+ ];
1563
+ if (data.ripgrep_error)
1564
+ lines.push(`ripgrep_error ${oneLine(data.ripgrep_error, 180)}`);
1565
+ for (const m of data.matches.slice(0, 80)) {
1566
+ lines.push(`${m.file_path}:${m.line}:${m.col}: ${oneLine(m.preview, 220)}`);
1567
+ }
1568
+ if (!data.matches.length)
1569
+ lines.push("- no matches");
1570
+ if (data.truncated)
1571
+ lines.push("hint: refine query/include_paths or raise max_results; use format=json for full match objects");
1572
+ return lines.join("\n");
1573
+ }
1574
+ function compactListProjectFilesText(data) {
1575
+ const lines = [
1576
+ `files path=${data.path} kind=${data.path_kind} returned=${data.returned} scanned=${data.scanned} recursive=${data.recursive} depth=${data.max_depth} truncated=${data.truncated}`,
1577
+ ];
1578
+ for (const e of data.entries.slice(0, 200)) {
1579
+ const stat = e.size != null ? ` ${e.size}B` : "";
1580
+ lines.push(`${e.kind === "dir" ? "d" : "f"} ${e.path}${stat}`);
1581
+ }
1582
+ if (!data.entries.length)
1583
+ lines.push("- empty");
1584
+ if (data.truncated)
1585
+ lines.push("hint: narrow path/filters or raise max_results; use format=json for full entry metadata");
1586
+ return lines.join("\n");
1587
+ }
1588
+ function compactReadTextFileText(data) {
1589
+ const offset = data.offset != null ? ` offset=${data.offset}` : "";
1590
+ const header = `file ${data.file_path}${offset} chars=${data.returned_chars}/${data.total_chars} truncated=${data.truncated}`;
1591
+ const hint = data.truncated ? "\nhint: continue with offset or read_file_lines; use format=json for metadata fields" : "";
1592
+ return `${header}\n${data.text}${hint}`;
1593
+ }
1594
+ function compactReadFileLinesText(data) {
1595
+ const header = `lines ${data.file_path}:${data.from_line}-${data.to_line} returned=${data.returned} truncated=${data.truncated}`;
1596
+ const hint = data.truncated ? "\nhint: narrow range or raise max_lines/max_chars; use format=json for metadata fields" : "";
1597
+ return `${header}\n${data.text}${hint}`;
1598
+ }
1599
+ function compactQueryCodebaseText(data) {
1600
+ const lines = [`query_codebase matches=${data.matches.length} q="${oneLine(data.query, 100)}"`];
1601
+ for (const m of data.matches.slice(0, 50)) {
1602
+ lines.push(`${m.file_path}: ${m.type} ${m.name}${m.signature ? ` — ${oneLine(m.signature, 160)}` : ""}`);
1603
+ }
1604
+ if (!data.matches.length)
1605
+ lines.push("- no matches");
1606
+ return lines.join("\n");
1607
+ }
1608
+ function compactBootstrapText(data) {
1609
+ const lines = [];
1610
+ lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
1611
+ if (data.project_summary)
1612
+ lines.push(`summary ${compactMemoryLabel(data.project_summary, 140)}`);
1613
+ if (data.decisions.length) {
1614
+ lines.push("current decisions:");
1615
+ for (const d of data.decisions.slice(0, 5))
1616
+ lines.push(`- ${compactMemoryLabel(d, 160)}`);
1617
+ }
1618
+ if (data.pending_total) {
1619
+ lines.push(`pending ${data.pending_changes.length}/${data.pending_total}${data.pending_truncated ? " truncated" : ""}: ${data.pending_changes
1620
+ .slice(0, 8)
1621
+ .map(compactPendingLabel)
1622
+ .join("; ")}`);
1623
+ }
1624
+ else {
1625
+ lines.push("pending 0");
1626
+ }
1627
+ if (data.items.length) {
1628
+ lines.push("requirements:");
1629
+ for (const item of data.items) {
1630
+ lines.push(`- ${compactRequirementLabel(item.requirement)}`);
1631
+ for (const c of item.recent_changes.slice(0, 3))
1632
+ lines.push(` - ${compactChangeLabel(c)}`);
1633
+ }
1634
+ }
1635
+ else {
1636
+ lines.push("requirements: none");
1637
+ }
1638
+ if (data.recent_notes.length) {
1639
+ lines.push("notes:");
1640
+ for (const n of data.recent_notes.slice(0, 3))
1641
+ lines.push(`- ${compactMemoryLabel(n, 120)}`);
1642
+ }
1643
+ if (data.conventions.length) {
1644
+ lines.push(`conventions ${data.conventions.length}: ${data.conventions
1645
+ .slice(0, 5)
1646
+ .map((c) => c.title ?? `#${c.id}`)
1647
+ .join(", ")}`);
1648
+ }
1649
+ if (data.semantic) {
1650
+ lines.push(`semantic ${data.semantic.mode} ${data.semantic.matches.length}/${data.semantic.top_k} for "${oneLine(data.semantic.query, 80)}":`);
1651
+ for (const m of data.semantic.matches.slice(0, 5)) {
1652
+ lines.push(`- score=${m.score.toFixed(3)} ${compactMemoryLabel(m.item, 120)}`);
1653
+ }
1654
+ }
1655
+ lines.push("hint: use format=json for full structured output; read_memory_item(id) for full content");
1656
+ return lines.join("\n");
1657
+ }
1658
+ function compactBrainDumpText(data) {
1659
+ return compactBootstrapText(data);
1660
+ }
1661
+ function shellQuoteArg(arg) {
1662
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg))
1663
+ return arg;
1664
+ if (process.platform === "win32")
1665
+ return `"${arg.replace(/"/g, '\\"')}"`;
1666
+ return `'${arg.replace(/'/g, "'\\''")}'`;
1667
+ }
1668
+ function getPackageRtkShimPath() {
1669
+ try {
1670
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
1671
+ const candidate = path.join(currentDir, "rtk-shim.js");
1672
+ if (fs.existsSync(candidate))
1673
+ return candidate;
1674
+ }
1675
+ catch {
1676
+ // import.meta.url may be unavailable only in unexpected runtimes.
1677
+ }
1678
+ return null;
1679
+ }
1680
+ function runRtkProbe(spec) {
1681
+ const argsPrefix = spec.execArgsPrefix ?? [];
1682
+ const result = spawnSync(spec.execCommand, [...argsPrefix, "--version"], {
1683
+ encoding: "utf8",
1684
+ timeout: 120_000,
1685
+ windowsHide: true,
1686
+ shell: spec.execShell ?? false,
1687
+ });
1688
+ if (result.status === 0) {
1689
+ const gain = spawnSync(spec.execCommand, [...argsPrefix, "gain"], {
1690
+ encoding: "utf8",
1691
+ timeout: 120_000,
1692
+ windowsHide: true,
1693
+ shell: spec.execShell ?? false,
1694
+ });
1695
+ let resolvedPath = spec.path;
1696
+ if (spec.source === "path") {
1697
+ const whereCommand = process.platform === "win32" ? "where.exe" : "which";
1698
+ const whereResult = spawnSync(whereCommand, ["rtk"], {
1699
+ encoding: "utf8",
1700
+ timeout: 2000,
1701
+ windowsHide: true,
1702
+ });
1703
+ resolvedPath = whereResult.status === 0 ? oneLine(whereResult.stdout, 240) : resolvedPath;
1704
+ }
1705
+ const gainText = `${gain.stdout}${gain.stderr}`.trim();
1706
+ return {
1707
+ available: gain.status === 0,
1708
+ command: spec.displayCommand,
1709
+ version: `${result.stdout}${result.stderr}`.trim(),
1710
+ gain_ok: gain.status === 0,
1711
+ gain_preview: oneLine(gainText, 240),
1712
+ path: resolvedPath,
1713
+ source: spec.source,
1714
+ exec_command: spec.execCommand,
1715
+ exec_args_prefix: argsPrefix,
1716
+ exec_shell: spec.execShell ?? false,
1717
+ note: gain.status === 0
1718
+ ? spec.source === "package_shim"
1719
+ ? `Prefer prefixing shell commands with ${spec.displayCommand} for compact outputs. This is VectorMind's bundled RTK shim; first run auto-installs/caches rtk-ai/rtk if needed.`
1720
+ : "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
1721
+ : spec.source === "package_shim"
1722
+ ? "VectorMind's bundled RTK shim exists, but `gain` failed. Check network/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
1723
+ : "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only after confirming it is safe.",
1724
+ };
1725
+ }
1726
+ return null;
1727
+ }
1728
+ function detectRtk() {
1729
+ const pathProbe = runRtkProbe({
1730
+ source: "path",
1731
+ displayCommand: "rtk",
1732
+ execCommand: "rtk",
1733
+ execShell: process.platform === "win32",
1734
+ });
1735
+ if (pathProbe?.available)
1736
+ return pathProbe;
1737
+ const shimPath = getPackageRtkShimPath();
1738
+ if (shimPath) {
1739
+ const displayCommand = `node ${shellQuoteArg(shimPath)}`;
1740
+ const shimProbe = runRtkProbe({
1741
+ source: "package_shim",
1742
+ displayCommand,
1743
+ execCommand: process.execPath,
1744
+ execArgsPrefix: [shimPath],
1745
+ path: shimPath,
1746
+ });
1747
+ if (shimProbe)
1748
+ return shimProbe;
1749
+ }
1750
+ if (pathProbe)
1751
+ return pathProbe;
1752
+ return {
1753
+ available: false,
1754
+ command: shimPath ? `node ${shellQuoteArg(shimPath)}` : "rtk",
1755
+ path: shimPath ?? undefined,
1756
+ source: shimPath ? "package_shim" : undefined,
1757
+ note: shimPath
1758
+ ? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check network/cache or set VECTORMIND_RTK_REAL."
1759
+ : "rtk was not found on PATH and the package RTK shim is unavailable. VectorMind compact MCP output still works; install rtk to compact shell command output too.",
1760
+ };
1761
+ }
1762
+ function commandExists(command) {
1763
+ const probe = process.platform === "win32" ? "where.exe" : "which";
1764
+ const result = spawnSync(probe, [command], { encoding: "utf8", timeout: 2000, windowsHide: true });
1765
+ return result.status === 0;
1766
+ }
1767
+ function runInstallStep(command, args, timeoutMs) {
1768
+ const result = spawnSync(command, args, {
1769
+ encoding: "utf8",
1770
+ timeout: timeoutMs,
1771
+ windowsHide: true,
1772
+ shell: false,
1773
+ });
1774
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
1775
+ return {
1776
+ command: [command, ...args].join(" "),
1777
+ status: result.status,
1778
+ ok: result.status === 0,
1779
+ output: oneLine(output, 1200),
1780
+ };
1781
+ }
1782
+ function runDetectedRtkStep(detected, args, timeoutMs) {
1783
+ const execCommand = detected.exec_command ?? "rtk";
1784
+ const argsPrefix = detected.exec_args_prefix ?? [];
1785
+ const result = spawnSync(execCommand, [...argsPrefix, ...args], {
1786
+ encoding: "utf8",
1787
+ timeout: timeoutMs,
1788
+ windowsHide: true,
1789
+ shell: detected.exec_shell ?? (execCommand === "rtk" && process.platform === "win32"),
1790
+ });
1791
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
1792
+ return {
1793
+ command: [detected.command, ...args].join(" "),
1794
+ status: result.status,
1795
+ ok: result.status === 0,
1796
+ output: oneLine(output, 1200),
1797
+ };
1798
+ }
1799
+ function appendRtkInitStep(steps, detected, init, timeoutMs) {
1800
+ if (init === "none")
1801
+ return;
1802
+ if (init === "global_no_patch")
1803
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--no-patch"], timeoutMs));
1804
+ if (init === "global_auto_patch")
1805
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--auto-patch"], timeoutMs));
1806
+ if (init === "global_hook_only") {
1807
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--hook-only", "--no-patch"], timeoutMs));
1808
+ }
1809
+ if (init === "local")
1810
+ steps.push(runDetectedRtkStep(detected, ["init"], timeoutMs));
1811
+ if (init === "codex_global")
1812
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--codex"], timeoutMs));
1813
+ if (init === "codex_local")
1814
+ steps.push(runDetectedRtkStep(detected, ["init", "--codex"], timeoutMs));
1815
+ }
1816
+ function chooseRtkInstallMethod(method) {
1817
+ if (method !== "auto")
1818
+ return method;
1819
+ if (process.platform === "darwin" && commandExists("brew"))
1820
+ return "brew";
1821
+ if (commandExists("cargo"))
1822
+ return "cargo";
1823
+ return "shell_script";
1824
+ }
1825
+ function buildRtkInstallPlan(args) {
1826
+ const method = chooseRtkInstallMethod(args.method);
1827
+ const commands = [];
1828
+ const notes = [];
1829
+ if (args.uninstall_wrong_cargo_rtk) {
1830
+ commands.push("cargo uninstall rtk");
1831
+ notes.push("Only use uninstall_wrong_cargo_rtk after verifying the existing rtk is the wrong Cargo package.");
1832
+ }
1833
+ if (method === "brew") {
1834
+ commands.push("brew install rtk");
1835
+ }
1836
+ else if (method === "cargo") {
1837
+ commands.push("cargo install --git https://github.com/rtk-ai/rtk");
1838
+ }
1839
+ else {
1840
+ if (process.platform === "win32") {
1841
+ notes.push("shell_script install is Linux/macOS-oriented; on Windows prefer method=cargo after installing Rust/Cargo.");
1842
+ commands.push("cargo install --git https://github.com/rtk-ai/rtk");
1843
+ }
1844
+ else {
1845
+ commands.push("curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh");
1846
+ }
1847
+ }
1848
+ commands.push("rtk --version");
1849
+ commands.push("rtk gain");
1850
+ if (args.init === "global_no_patch")
1851
+ commands.push("rtk init -g --no-patch");
1852
+ if (args.init === "global_auto_patch")
1853
+ commands.push("rtk init -g --auto-patch");
1854
+ if (args.init === "global_hook_only")
1855
+ commands.push("rtk init -g --hook-only --no-patch");
1856
+ if (args.init === "local")
1857
+ commands.push("rtk init");
1858
+ if (args.init === "codex_global")
1859
+ commands.push("rtk init -g --codex");
1860
+ if (args.init === "codex_local")
1861
+ commands.push("rtk init --codex");
1862
+ if (args.init !== "none") {
1863
+ notes.push("rtk init may modify Claude/RTK configuration. Use init=none for binary-only installation.");
1864
+ }
1865
+ return { method, commands, notes };
1866
+ }
1867
+ function installRtk(args) {
1868
+ const detectedBefore = detectRtk();
1869
+ const plan = buildRtkInstallPlan(args);
1870
+ const steps = [];
1871
+ const notes = [...plan.notes];
1872
+ if (detectedBefore.available) {
1873
+ notes.push("rtk is already installed and verified with `rtk gain`; installation skipped.");
1874
+ if (!args.dry_run && args.init !== "none") {
1875
+ appendRtkInitStep(steps, detectedBefore, args.init, args.timeout_ms);
1876
+ }
1877
+ return {
1878
+ ok: true,
1879
+ dry_run: args.dry_run,
1880
+ already_available: true,
1881
+ method: plan.method,
1882
+ commands: plan.commands,
1883
+ notes,
1884
+ steps,
1885
+ detected_before: detectedBefore,
1886
+ detected_after: detectedBefore,
1887
+ };
1888
+ }
1889
+ if (args.dry_run) {
1890
+ notes.push("dry_run=true: no command was executed. Call install_rtk with dry_run=false to install.");
1891
+ return {
1892
+ ok: true,
1893
+ dry_run: true,
1894
+ already_available: false,
1895
+ method: plan.method,
1896
+ commands: plan.commands,
1897
+ notes,
1898
+ steps,
1899
+ detected_before: detectedBefore,
1900
+ };
1901
+ }
1902
+ if (plan.method === "brew") {
1903
+ steps.push(runInstallStep("brew", ["install", "rtk"], args.timeout_ms));
1904
+ }
1905
+ else if (plan.method === "cargo") {
1906
+ if (args.uninstall_wrong_cargo_rtk) {
1907
+ steps.push(runInstallStep("cargo", ["uninstall", "rtk"], args.timeout_ms));
1908
+ }
1909
+ steps.push(runInstallStep("cargo", ["install", "--git", "https://github.com/rtk-ai/rtk"], args.timeout_ms));
1910
+ }
1911
+ else if (process.platform === "win32") {
1912
+ notes.push("Windows fallback uses Cargo because the upstream shell installer targets POSIX shells.");
1913
+ if (args.uninstall_wrong_cargo_rtk) {
1914
+ steps.push(runInstallStep("cargo", ["uninstall", "rtk"], args.timeout_ms));
1915
+ }
1916
+ steps.push(runInstallStep("cargo", ["install", "--git", "https://github.com/rtk-ai/rtk"], args.timeout_ms));
1917
+ }
1918
+ else {
1919
+ const script = "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh";
1920
+ steps.push(runInstallStep("sh", ["-c", script], args.timeout_ms));
1921
+ }
1922
+ const detectedAfterInstall = detectRtk();
1923
+ if (detectedAfterInstall.available && args.init !== "none") {
1924
+ appendRtkInitStep(steps, detectedAfterInstall, args.init, args.timeout_ms);
1925
+ }
1926
+ const detectedAfter = detectRtk();
1927
+ return {
1928
+ ok: detectedAfter.available,
1929
+ dry_run: false,
1930
+ already_available: false,
1931
+ method: plan.method,
1932
+ commands: plan.commands,
1933
+ notes,
1934
+ steps,
1935
+ detected_before: detectedBefore,
1936
+ detected_after: detectedAfter,
1937
+ };
1938
+ }
1939
+ function compactInstallRtkText(data) {
1940
+ const lines = [];
1941
+ lines.push(`install_rtk ok=${data.ok} dry_run=${data.dry_run} already_available=${data.already_available} method=${data.method}`);
1942
+ lines.push(`before available=${data.detected_before.available} version=${data.detected_before.version ?? "none"} gain_ok=${data.detected_before.gain_ok ?? false}`);
1943
+ if (data.detected_after) {
1944
+ lines.push(`after available=${data.detected_after.available} version=${data.detected_after.version ?? "none"} gain_ok=${data.detected_after.gain_ok ?? false}`);
1945
+ }
1946
+ if (data.commands.length) {
1947
+ lines.push("commands:");
1948
+ for (const command of data.commands)
1949
+ lines.push(`- ${command}`);
1950
+ }
1951
+ if (data.steps.length) {
1952
+ lines.push("steps:");
1953
+ for (const step of data.steps) {
1954
+ lines.push(`- ${step.ok ? "ok" : "fail"} [${step.status ?? "null"}] ${step.command}: ${oneLine(step.output, 240)}`);
1955
+ }
1956
+ }
1957
+ if (data.notes.length) {
1958
+ lines.push("notes:");
1959
+ for (const note of data.notes)
1960
+ lines.push(`- ${note}`);
1961
+ }
1962
+ return lines.join("\n");
1963
+ }
1964
+ function tokenSavingsSummary(limit) {
1965
+ const summary = summarizeTokenSavingsStmt.get();
1966
+ const by_tool = summarizeTokenSavingsByToolStmt.all(limit);
1967
+ const recent = listRecentTokenSavingsStmt.all(limit);
1968
+ return {
1969
+ ok: true,
1970
+ summary: summary ?? { calls: 0, raw_tokens: 0, output_tokens: 0, saved_tokens: 0, avg_savings_pct: 0 },
1971
+ by_tool,
1972
+ recent,
1973
+ };
1974
+ }
1975
+ function compactTokenSavingsText(data) {
1976
+ const s = data.summary;
1977
+ const pct = Number(s.raw_tokens) > 0 ? (Number(s.saved_tokens) / Number(s.raw_tokens)) * 100 : 0;
1978
+ const lines = [
1979
+ `token_savings calls=${s.calls} raw=${s.raw_tokens} out=${s.output_tokens} saved=${s.saved_tokens} (${pct.toFixed(1)}%)`,
1980
+ ];
1981
+ if (data.by_tool.length) {
1982
+ lines.push("by_tool:");
1983
+ for (const t of data.by_tool.slice(0, 10)) {
1984
+ lines.push(`- ${t.tool}: calls=${t.calls} saved=${t.saved_tokens} raw=${t.raw_tokens} out=${t.output_tokens} avg=${Number(t.avg_savings_pct).toFixed(1)}%`);
1985
+ }
1986
+ }
1987
+ if (data.recent.length) {
1988
+ lines.push("recent:");
1989
+ for (const r of data.recent.slice(0, 10)) {
1990
+ lines.push(`- #${r.id} ${r.tool}: ${r.raw_tokens}->${r.output_tokens} saved=${r.saved_tokens}`);
1991
+ }
1992
+ }
1993
+ return lines.join("\n");
1994
+ }
1338
1995
  function sliceTextForOutput(input, maxChars) {
1339
1996
  const total = input.length;
1340
1997
  if (maxChars <= 0)
@@ -1472,6 +2129,88 @@ function dotProduct(a, b) {
1472
2129
  s += a[i] * b[i];
1473
2130
  return s;
1474
2131
  }
2132
+ function parseMetadataJson(metadata) {
2133
+ if (!metadata)
2134
+ return {};
2135
+ try {
2136
+ const parsed = JSON.parse(metadata);
2137
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
2138
+ ? parsed
2139
+ : {};
2140
+ }
2141
+ catch {
2142
+ return {};
2143
+ }
2144
+ }
2145
+ function metadataStatus(row) {
2146
+ const meta = parseMetadataJson(row.metadata_json);
2147
+ return typeof meta.status === "string" ? meta.status : "";
2148
+ }
2149
+ function isSupersededMemory(row) {
2150
+ const meta = parseMetadataJson(row.metadata_json);
2151
+ return meta.superseded === true || meta.status === "superseded";
2152
+ }
2153
+ function semanticRecencyWeight(updatedAt) {
2154
+ if (!updatedAt)
2155
+ return 0;
2156
+ const t = Date.parse(updatedAt.endsWith("Z") ? updatedAt : `${updatedAt}Z`);
2157
+ if (!Number.isFinite(t))
2158
+ return 0;
2159
+ const ageDays = Math.max(0, (Date.now() - t) / 86_400_000);
2160
+ if (ageDays <= 1)
2161
+ return 0.8;
2162
+ if (ageDays <= 7)
2163
+ return 0.45;
2164
+ if (ageDays <= 30)
2165
+ return 0.2;
2166
+ return 0;
2167
+ }
2168
+ function semanticKindWeight(kind) {
2169
+ switch (kind) {
2170
+ case "decision":
2171
+ return 3.5;
2172
+ case "convention":
2173
+ return 2.6;
2174
+ case "project_summary":
2175
+ return 2.2;
2176
+ case "note":
2177
+ return 1.1;
2178
+ case "requirement":
2179
+ return 0.4;
2180
+ case "change_intent":
2181
+ return 0.2;
2182
+ default:
2183
+ return 0;
2184
+ }
2185
+ }
2186
+ function adjustSemanticScore(row, rawScore) {
2187
+ if (isSupersededMemory(row))
2188
+ return rawScore - 1000;
2189
+ let score = rawScore + semanticKindWeight(row.kind) + semanticRecencyWeight(row.updated_at);
2190
+ const status = metadataStatus(row);
2191
+ if (status === "active" || status === "current")
2192
+ score += 1.2;
2193
+ if (row.kind === "change_intent" && row.file_path && shouldIgnoreDbFilePath(row.file_path)) {
2194
+ // Human-synced intent for generated/build/runtime files is often the only durable
2195
+ // "why" for that change. Do not let built-in path ignores hide the decision trail.
2196
+ score += 0.4;
2197
+ }
2198
+ return score;
2199
+ }
2200
+ function filterAndRankSemanticRows(rows, scoreOf, opts) {
2201
+ return rows
2202
+ .map((r) => ({ row: r, score: adjustSemanticScore(r, scoreOf(r)) }))
2203
+ .filter(({ row }) => {
2204
+ if (isSupersededMemory(row))
2205
+ return false;
2206
+ if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
2207
+ return false;
2208
+ return true;
2209
+ })
2210
+ .sort((a, b) => b.score - a.score)
2211
+ .slice(0, opts.topK)
2212
+ .map(({ row, score }) => toSemanticMatch(row, score, opts.includeContent, opts.previewChars, opts.contentMaxChars));
2213
+ }
1475
2214
  function makePreviewText(content, max) {
1476
2215
  if (max <= 0)
1477
2216
  return "";
@@ -1546,6 +2285,15 @@ function getConventionPreviews(conventionsLimit, previewChars, contentMaxChars)
1546
2285
  const stored = listConventionsStmt.all(remaining).map((c) => toMemoryItemPreview(c, false, previewChars, contentMaxChars));
1547
2286
  return [...builtin, ...stored];
1548
2287
  }
2288
+ function getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars) {
2289
+ if (decisionsLimit <= 0)
2290
+ return [];
2291
+ const rows = listCurrentDecisionsStmt.all(Math.min(MAX_DECISIONS_LIMIT * 4, Math.max(decisionsLimit, decisionsLimit * 4)));
2292
+ return rows
2293
+ .filter((d) => !isSupersededMemory(d))
2294
+ .slice(0, decisionsLimit)
2295
+ .map((d) => toMemoryItemPreview(d, false, previewChars, contentMaxChars));
2296
+ }
1549
2297
  function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
1550
2298
  const context = req.context_data ?? null;
1551
2299
  const contextPreview = context ? makePreviewText(context, previewChars) : null;
@@ -1590,6 +2338,46 @@ function completeAllActiveRequirementMemoryItems() {
1590
2338
  console.error("[vectormind] failed to complete all active requirement memory items:", err);
1591
2339
  }
1592
2340
  }
2341
+ function patchMemoryItemMetadata(id, patch) {
2342
+ const row = getMemoryItemByIdStmt.get(id);
2343
+ if (!row)
2344
+ return;
2345
+ const meta = { ...parseMetadataJson(row.metadata_json), ...patch };
2346
+ db.prepare(`UPDATE memory_items SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(safeJson(meta), id);
2347
+ }
2348
+ function supersedeMemoryItemIds(ids, replacement) {
2349
+ const updated = [];
2350
+ for (const id of Array.from(new Set(ids)).filter((n) => Number.isFinite(n) && n > 0)) {
2351
+ const row = getMemoryItemByIdStmt.get(id);
2352
+ if (!row)
2353
+ continue;
2354
+ patchMemoryItemMetadata(id, {
2355
+ ...parseMetadataJson(row.metadata_json),
2356
+ status: "superseded",
2357
+ superseded: true,
2358
+ superseded_at: new Date().toISOString(),
2359
+ superseded_reason: replacement.reason,
2360
+ superseded_by_req_id: replacement.req_id ?? null,
2361
+ superseded_by_memory_id: replacement.memory_id ?? null,
2362
+ superseded_by_decision_id: replacement.decision_id ?? null,
2363
+ });
2364
+ updated.push(id);
2365
+ }
2366
+ return updated;
2367
+ }
2368
+ function supersedeRequirementIds(reqIds, replacement) {
2369
+ const updatedReqs = [];
2370
+ for (const reqId of Array.from(new Set(reqIds)).filter((n) => Number.isFinite(n) && n > 0)) {
2371
+ const info = db.prepare(`UPDATE requirements SET status = 'superseded' WHERE id = ?`).run(reqId);
2372
+ if (info.changes > 0)
2373
+ updatedReqs.push(reqId);
2374
+ const rows = db
2375
+ .prepare(`SELECT id FROM memory_items WHERE req_id = ? OR (kind = 'requirement' AND req_id = ?)`)
2376
+ .all(reqId, reqId);
2377
+ supersedeMemoryItemIds(rows.map((r) => r.id), replacement);
2378
+ }
2379
+ return updatedReqs;
2380
+ }
1593
2381
  async function semanticSearchInternal(opts) {
1594
2382
  if (!embeddingsEnabled) {
1595
2383
  throw new Error("Embeddings are disabled");
@@ -1641,7 +2429,31 @@ async function semanticSearchInternal(opts) {
1641
2429
  return toSemanticMatch(item, t.score, opts.includeContent, opts.previewChars, opts.contentMaxChars);
1642
2430
  })
1643
2431
  .filter(Boolean);
1644
- const filtered = matches.filter((m) => !shouldIgnoreDbFilePath(m.item.file_path)).slice(0, opts.topK);
2432
+ const filtered = matches
2433
+ .filter((m) => {
2434
+ if (isSupersededMemory({ metadata_json: m.item.metadata_json }))
2435
+ return false;
2436
+ if (shouldIgnoreDbFilePath(m.item.file_path) && m.item.kind !== "change_intent")
2437
+ return false;
2438
+ return true;
2439
+ })
2440
+ .map((m) => ({
2441
+ ...m,
2442
+ score: adjustSemanticScore({
2443
+ id: m.item.id,
2444
+ kind: m.item.kind,
2445
+ title: m.item.title,
2446
+ content: m.item.content ?? m.item.preview,
2447
+ file_path: m.item.file_path,
2448
+ start_line: m.item.start_line,
2449
+ end_line: m.item.end_line,
2450
+ req_id: m.item.req_id,
2451
+ metadata_json: m.item.metadata_json,
2452
+ updated_at: m.item.updated_at,
2453
+ }, m.score),
2454
+ }))
2455
+ .sort((a, b) => b.score - a.score)
2456
+ .slice(0, opts.topK);
1645
2457
  return { query: q, top_k: opts.topK, mode: "embeddings", matches: filtered };
1646
2458
  }
1647
2459
  function buildFtsMatchQuery(raw) {
@@ -1710,10 +2522,7 @@ function ftsSearchInternal(opts) {
1710
2522
  `);
1711
2523
  return stmt.all(matchQuery, rawLimit);
1712
2524
  })();
1713
- const matches = rows
1714
- .map((r) => toSemanticMatch(r, -Number(r.rank), opts.includeContent, opts.previewChars, opts.contentMaxChars))
1715
- .filter((m) => !shouldIgnoreDbFilePath(m.item.file_path))
1716
- .slice(0, opts.topK);
2525
+ const matches = filterAndRankSemanticRows(rows, (r) => -Number(r.rank), opts);
1717
2526
  return { query: q, top_k: opts.topK, mode: "fts", matches };
1718
2527
  }
1719
2528
  function likeSearchInternal(opts) {
@@ -1779,10 +2588,7 @@ function likeSearchInternal(opts) {
1779
2588
  `);
1780
2589
  return stmt.all(like, like, like, like, like, rawLimit);
1781
2590
  })();
1782
- const matches = rows
1783
- .map((r) => toSemanticMatch(r, Number(r.score), opts.includeContent, opts.previewChars, opts.contentMaxChars))
1784
- .filter((m) => !shouldIgnoreDbFilePath(m.item.file_path))
1785
- .slice(0, opts.topK);
2591
+ const matches = filterAndRankSemanticRows(rows, (r) => Number(r.score), opts);
1786
2592
  return { query: q, top_k: opts.topK, mode: "like", matches };
1787
2593
  }
1788
2594
  async function semanticSearchHybridInternal(opts) {
@@ -2527,6 +3333,9 @@ function buildServerInstructions() {
2527
3333
  "Built-in frontend output-purity policy:",
2528
3334
  BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
2529
3335
  "",
3336
+ "Built-in git commit summary policy:",
3337
+ BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS,
3338
+ "",
2530
3339
  "Built-in low-overhead execution and heavy-thread policy:",
2531
3340
  BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS,
2532
3341
  "",
@@ -2537,11 +3346,14 @@ function buildServerInstructions() {
2537
3346
  BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS,
2538
3347
  "",
2539
3348
  "Required workflow:",
2540
- "- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
3349
+ "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
3350
+ "- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore compact context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
2541
3351
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
2542
- " - Tune output size with: requirements_limit/changes_limit/notes_limit, preview_chars, pending_limit/pending_offset.",
3352
+ " - Tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit, preview_chars, pending_limit/pending_offset.",
2543
3353
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
2544
3354
  "- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
3355
+ "- If rtk is installed or VectorMind's bundled RTK shim is verified (detect_rtk with gain_ok=true), prefix shell commands with the command returned by detect_rtk. Usually this is rtk (rtk git status, rtk npm run build, rtk rg ...); in npx/MCP-only installs it may be a package shim command such as node <...>/rtk-shim.js.",
3356
+ "- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only after the user clearly approves installation/init choices.",
2545
3357
  "- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming a filesystem MCP resource server exists.",
2546
3358
  "- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
2547
3359
  "- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
@@ -2555,11 +3367,13 @@ function buildServerInstructions() {
2555
3367
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
2556
3368
  "- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
2557
3369
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
3370
+ "- When a requirement or user decision changes/reverses an older behavior, call upsert_decision(key, title, content, supersedes_req_ids?/supersedes_memory_ids?) and/or supersede_memory(...). Current decisions are shown in bootstrap_context/get_brain_dump and superseded memories are hidden from default semantic recall so stale requirements do not override newer facts.",
2558
3371
  "- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
2559
3372
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
2560
3373
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
2561
3374
  "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
2562
3375
  "- If the current thread is already heavy or the user reports it has become slow, switch to a lighter workflow: avoid redundant retrieval, keep outputs compact, and if the user refuses thread switching, continue in light mode without repeating the switch reminder in that same session.",
3376
+ "- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
2563
3377
  "",
2564
3378
  "If tool output conflicts with assumptions, trust the tool output.",
2565
3379
  ].join("\n");
@@ -2778,6 +3592,9 @@ function initDatabase() {
2778
3592
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_convention_key
2779
3593
  ON memory_items(kind, title) WHERE kind = 'convention';
2780
3594
 
3595
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_decision_key
3596
+ ON memory_items(kind, title) WHERE kind = 'decision';
3597
+
2781
3598
  CREATE INDEX IF NOT EXISTS idx_memory_items_kind_updated_at
2782
3599
  ON memory_items(kind, updated_at DESC);
2783
3600
 
@@ -2808,6 +3625,22 @@ function initDatabase() {
2808
3625
 
2809
3626
  CREATE INDEX IF NOT EXISTS idx_pending_changes_updated_at
2810
3627
  ON pending_changes(updated_at DESC);
3628
+
3629
+ CREATE TABLE IF NOT EXISTS token_savings (
3630
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3631
+ tool TEXT NOT NULL,
3632
+ raw_tokens INTEGER NOT NULL,
3633
+ output_tokens INTEGER NOT NULL,
3634
+ saved_tokens INTEGER NOT NULL,
3635
+ savings_pct REAL NOT NULL,
3636
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
3637
+ );
3638
+
3639
+ CREATE INDEX IF NOT EXISTS idx_token_savings_created_at
3640
+ ON token_savings(created_at DESC);
3641
+
3642
+ CREATE INDEX IF NOT EXISTS idx_token_savings_tool
3643
+ ON token_savings(tool);
2811
3644
  `);
2812
3645
  initMemoryItemsFts();
2813
3646
  insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
@@ -2858,6 +3691,23 @@ function initDatabase() {
2858
3691
  WHERE kind = 'convention'
2859
3692
  ORDER BY updated_at DESC, id DESC
2860
3693
  LIMIT ?`);
3694
+ upsertDecisionStmt = db.prepare(`INSERT INTO memory_items (kind, title, content, metadata_json, content_hash)
3695
+ VALUES ('decision', ?, ?, ?, ?)
3696
+ ON CONFLICT DO UPDATE SET
3697
+ content = excluded.content,
3698
+ metadata_json = excluded.metadata_json,
3699
+ content_hash = excluded.content_hash,
3700
+ updated_at = CURRENT_TIMESTAMP`);
3701
+ getDecisionByKeyStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3702
+ FROM memory_items
3703
+ WHERE kind = 'decision' AND title = ?
3704
+ ORDER BY updated_at DESC, id DESC
3705
+ LIMIT 1`);
3706
+ listCurrentDecisionsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3707
+ FROM memory_items
3708
+ WHERE kind = 'decision'
3709
+ ORDER BY updated_at DESC, id DESC
3710
+ LIMIT ?`);
2861
3711
  getRequirementMemoryItemIdStmt = db.prepare(`SELECT id
2862
3712
  FROM memory_items
2863
3713
  WHERE kind = 'requirement' AND req_id = ?
@@ -2880,6 +3730,11 @@ function initDatabase() {
2880
3730
  WHERE kind = 'note'
2881
3731
  ORDER BY updated_at DESC, id DESC
2882
3732
  LIMIT ?`);
3733
+ getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3734
+ FROM memory_items
3735
+ WHERE kind = 'change_intent' AND file_path = ?
3736
+ ORDER BY updated_at DESC, id DESC
3737
+ LIMIT 1`);
2883
3738
  deleteFileChunkItemsStmt = db.prepare(`DELETE FROM memory_items
2884
3739
  WHERE file_path = ?
2885
3740
  AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
@@ -2929,6 +3784,30 @@ function initDatabase() {
2929
3784
  END,
2930
3785
  name
2931
3786
  LIMIT ?`);
3787
+ insertTokenSavingsStmt = db.prepare(`INSERT INTO token_savings (tool, raw_tokens, output_tokens, saved_tokens, savings_pct)
3788
+ VALUES (?, ?, ?, ?, ?)`);
3789
+ summarizeTokenSavingsStmt = db.prepare(`SELECT
3790
+ COUNT(*) as calls,
3791
+ COALESCE(SUM(raw_tokens), 0) as raw_tokens,
3792
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
3793
+ COALESCE(SUM(saved_tokens), 0) as saved_tokens,
3794
+ COALESCE(AVG(savings_pct), 0) as avg_savings_pct
3795
+ FROM token_savings`);
3796
+ summarizeTokenSavingsByToolStmt = db.prepare(`SELECT
3797
+ tool,
3798
+ COUNT(*) as calls,
3799
+ COALESCE(SUM(raw_tokens), 0) as raw_tokens,
3800
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
3801
+ COALESCE(SUM(saved_tokens), 0) as saved_tokens,
3802
+ COALESCE(AVG(savings_pct), 0) as avg_savings_pct
3803
+ FROM token_savings
3804
+ GROUP BY tool
3805
+ ORDER BY saved_tokens DESC, calls DESC
3806
+ LIMIT ?`);
3807
+ listRecentTokenSavingsStmt = db.prepare(`SELECT id, tool, raw_tokens, output_tokens, saved_tokens, savings_pct, created_at
3808
+ FROM token_savings
3809
+ ORDER BY created_at DESC, id DESC
3810
+ LIMIT ?`);
2932
3811
  indexFileSymbolsTx = db.transaction((filePath, symbols) => {
2933
3812
  deleteSymbolsForFileStmt.run(filePath);
2934
3813
  for (const s of symbols) {
@@ -3123,6 +4002,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3123
4002
  description: "Clear the in-memory debug activity log. Enable logging with VECTORMIND_DEBUG_LOG=1.",
3124
4003
  inputSchema: toJsonSchemaCompat(ClearActivityLogArgsSchema),
3125
4004
  },
4005
+ {
4006
+ name: "detect_rtk",
4007
+ description: "Detect whether rtk is available on PATH or via VectorMind's bundled RTK shim. When available, prefer the returned command as a shell prefix to reduce command-output tokens.",
4008
+ inputSchema: toJsonSchemaCompat(DetectRtkArgsSchema),
4009
+ },
4010
+ {
4011
+ name: "install_rtk",
4012
+ description: "Install the rtk-ai/rtk Rust Token Killer binary when it is missing. Defaults to dry_run=true and never patches hooks unless init is explicitly requested.",
4013
+ inputSchema: toJsonSchemaCompat(InstallRtkArgsSchema),
4014
+ },
4015
+ {
4016
+ name: "get_token_savings",
4017
+ description: "Show VectorMind compact-output token savings recorded by MCP tools. Use this to verify raw-vs-compact output reduction.",
4018
+ inputSchema: toJsonSchemaCompat(GetTokenSavingsArgsSchema),
4019
+ },
3126
4020
  {
3127
4021
  name: "grep",
3128
4022
  description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable.",
@@ -3163,6 +4057,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3163
4057
  description: "Save a durable project note (decision, constraint, TODO, architecture detail). Use this to persist important context locally instead of relying on chat memory.",
3164
4058
  inputSchema: toJsonSchemaCompat(AddNoteArgsSchema),
3165
4059
  },
4060
+ {
4061
+ name: "upsert_decision",
4062
+ description: "Save/update the current authoritative project decision for a key. Use it when requirements change, reverse, or supersede older behavior so future sessions prefer the latest decision over old history.",
4063
+ inputSchema: toJsonSchemaCompat(UpsertDecisionArgsSchema),
4064
+ },
4065
+ {
4066
+ name: "supersede_memory",
4067
+ description: "Mark old requirements or memory items as superseded by a newer requirement/decision. Superseded items are hidden from default semantic recall to avoid reverting to stale behavior.",
4068
+ inputSchema: toJsonSchemaCompat(SupersedeMemoryArgsSchema),
4069
+ },
3166
4070
  {
3167
4071
  name: "upsert_convention",
3168
4072
  description: "Save/update a project convention (framework choice, build command, naming rules, etc). Conventions are durable and should be applied automatically in future sessions.",
@@ -3394,24 +4298,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3394
4298
  }
3395
4299
  else {
3396
4300
  const pendingAll = listPendingChangesStmt.all();
3397
- if (pendingAll.length) {
3398
- const pending = pendingAll.filter((p) => !shouldIgnoreDbFilePath(p.file_path));
3399
- if (pending.length) {
3400
- for (const p of pending) {
3401
- targets.push({
3402
- rawFile: p.file_path,
3403
- dbFilePath: p.file_path,
3404
- event: p.last_event,
3405
- source: "pending",
3406
- });
3407
- }
3408
- }
3409
- else {
4301
+ const merged = mergePendingWithGit(pendingAll, { offset: 0, limit: MAX_PENDING_LIMIT });
4302
+ if (merged.page.length) {
4303
+ for (const p of merged.page) {
3410
4304
  targets.push({
3411
- rawFile: "(unspecified)",
3412
- dbFilePath: "(unspecified)",
3413
- event: "manual",
3414
- source: "unspecified",
4305
+ rawFile: p.file_path,
4306
+ dbFilePath: p.file_path,
4307
+ event: p.last_event,
4308
+ source: p.source === "git" ? "pending" : "pending",
3415
4309
  });
3416
4310
  }
3417
4311
  deleteAllPendingChangesStmt.run();
@@ -3429,7 +4323,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3429
4323
  const isUnspecified = t.dbFilePath === "(unspecified)";
3430
4324
  const changeInfo = insertChangeLogStmt.run(active.id, t.dbFilePath, args.intent);
3431
4325
  const change_log_id = Number(changeInfo.lastInsertRowid);
3432
- const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({ change_log_id, event: t.event, source: t.source }), sha256Hex(args.intent));
4326
+ const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({
4327
+ change_log_id,
4328
+ event: t.event,
4329
+ source: t.source,
4330
+ file_state_hash: isUnspecified ? null : getFileStateHash(t.rawFile),
4331
+ }), sha256Hex(args.intent));
3433
4332
  const memory_item_id = Number(memoryInfo.lastInsertRowid);
3434
4333
  enqueueEmbedding(memory_item_id);
3435
4334
  synced_files.push({ file_path: t.dbFilePath, event: t.event, source: t.source });
@@ -3480,6 +4379,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3480
4379
  const changesLimit = args.changes_limit;
3481
4380
  const notesLimit = args.notes_limit;
3482
4381
  const conventionsLimit = args.conventions_limit;
4382
+ const decisionsLimit = args.decisions_limit;
3483
4383
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
3484
4384
  const items = recent.map((req) => {
3485
4385
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -3493,12 +4393,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3493
4393
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
3494
4394
  : null;
3495
4395
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4396
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
3496
4397
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
3497
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
3498
4398
  const pending_offset = args.pending_offset;
3499
4399
  const pending_limit = args.pending_limit;
3500
- const pending_truncated = pending_total > pending_offset + pending_limit;
3501
- const pending_changes = listPendingChangesPageStmt.all(pending_limit, pending_offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4400
+ const pendingDbRows = listPendingChangesStmt.all();
4401
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4402
+ const pending_total = mergedPending.total;
4403
+ const pending_truncated = mergedPending.truncated;
4404
+ const pending_changes = mergedPending.page;
3502
4405
  const q = args.query?.trim() ?? "";
3503
4406
  const semantic = q
3504
4407
  ? await Promise.race([
@@ -3521,47 +4424,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3521
4424
  pending_total,
3522
4425
  pending_returned: pending_changes.length,
3523
4426
  requirements_returned: items.length,
4427
+ decisions_returned: decisions.length,
3524
4428
  conventions_returned: conventions.length,
3525
4429
  semantic_mode: semantic?.mode ?? null,
3526
4430
  semantic_matches: semantic?.matches?.length ?? 0,
3527
4431
  });
4432
+ const outputValue = {
4433
+ ok: true,
4434
+ generated_at: new Date().toISOString(),
4435
+ project_root: projectRoot,
4436
+ root_source: rootSource,
4437
+ db_path: dbPath,
4438
+ watcher_enabled: !!watcher,
4439
+ watcher_ready: watcherReady,
4440
+ embeddings: {
4441
+ enabled: embeddingsEnabled,
4442
+ model: embedModelName,
4443
+ embed_files: embedFilesMode,
4444
+ },
4445
+ output: {
4446
+ format: args.format,
4447
+ include_content: includeContent,
4448
+ preview_chars: previewChars,
4449
+ content_max_chars: contentMaxChars,
4450
+ requirements_limit: requirementsLimit,
4451
+ changes_limit: changesLimit,
4452
+ notes_limit: notesLimit,
4453
+ decisions_limit: decisionsLimit,
4454
+ conventions_limit: conventionsLimit,
4455
+ },
4456
+ project_summary,
4457
+ decisions,
4458
+ conventions,
4459
+ recent_notes,
4460
+ pending_total,
4461
+ pending_offset,
4462
+ pending_limit,
4463
+ pending_truncated,
4464
+ pending_changes,
4465
+ items,
4466
+ semantic,
4467
+ };
3528
4468
  return {
3529
4469
  content: [
3530
4470
  {
3531
4471
  type: "text",
3532
- text: toolJson({
3533
- ok: true,
3534
- generated_at: new Date().toISOString(),
3535
- project_root: projectRoot,
3536
- root_source: rootSource,
3537
- db_path: dbPath,
3538
- watcher_enabled: !!watcher,
3539
- watcher_ready: watcherReady,
3540
- embeddings: {
3541
- enabled: embeddingsEnabled,
3542
- model: embedModelName,
3543
- embed_files: embedFilesMode,
3544
- },
3545
- output: {
3546
- include_content: includeContent,
3547
- preview_chars: previewChars,
3548
- content_max_chars: contentMaxChars,
3549
- requirements_limit: requirementsLimit,
3550
- changes_limit: changesLimit,
3551
- notes_limit: notesLimit,
3552
- conventions_limit: conventionsLimit,
3553
- },
3554
- project_summary,
3555
- conventions,
3556
- recent_notes,
3557
- pending_total,
3558
- pending_offset,
3559
- pending_limit,
3560
- pending_truncated,
3561
- pending_changes,
3562
- items,
3563
- semantic,
3564
- }),
4472
+ text: toolText("bootstrap_context", outputValue, compactBootstrapText(outputValue), args.format),
3565
4473
  },
3566
4474
  ],
3567
4475
  };
@@ -3576,6 +4484,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3576
4484
  const changesLimit = args.changes_limit;
3577
4485
  const notesLimit = args.notes_limit;
3578
4486
  const conventionsLimit = args.conventions_limit;
4487
+ const decisionsLimit = args.decisions_limit;
3579
4488
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
3580
4489
  const items = recent.map((req) => {
3581
4490
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -3589,55 +4498,64 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3589
4498
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
3590
4499
  : null;
3591
4500
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4501
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
3592
4502
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
3593
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
3594
4503
  const pending_offset = args.pending_offset;
3595
4504
  const pending_limit = args.pending_limit;
3596
- const pending_truncated = pending_total > pending_offset + pending_limit;
3597
- const pending_changes = listPendingChangesPageStmt.all(pending_limit, pending_offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4505
+ const pendingDbRows = listPendingChangesStmt.all();
4506
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4507
+ const pending_total = mergedPending.total;
4508
+ const pending_truncated = mergedPending.truncated;
4509
+ const pending_changes = mergedPending.page;
3598
4510
  logActivity("get_brain_dump", {
3599
4511
  pending_total,
3600
4512
  pending_returned: pending_changes.length,
3601
4513
  requirements_returned: items.length,
3602
4514
  notes_returned: recent_notes.length,
4515
+ decisions_returned: decisions.length,
3603
4516
  conventions_returned: conventions.length,
3604
4517
  });
4518
+ const outputValue = {
4519
+ ok: true,
4520
+ generated_at: new Date().toISOString(),
4521
+ project_root: projectRoot,
4522
+ root_source: rootSource,
4523
+ db_path: dbPath,
4524
+ watcher_enabled: !!watcher,
4525
+ watcher_ready: watcherReady,
4526
+ embeddings: {
4527
+ enabled: embeddingsEnabled,
4528
+ model: embedModelName,
4529
+ embed_files: embedFilesMode,
4530
+ },
4531
+ output: {
4532
+ format: args.format,
4533
+ include_content: includeContent,
4534
+ preview_chars: previewChars,
4535
+ content_max_chars: contentMaxChars,
4536
+ requirements_limit: requirementsLimit,
4537
+ changes_limit: changesLimit,
4538
+ notes_limit: notesLimit,
4539
+ decisions_limit: decisionsLimit,
4540
+ conventions_limit: conventionsLimit,
4541
+ },
4542
+ project_summary,
4543
+ decisions,
4544
+ conventions,
4545
+ recent_notes,
4546
+ pending_total,
4547
+ pending_offset,
4548
+ pending_limit,
4549
+ pending_truncated,
4550
+ pending_changes,
4551
+ items,
4552
+ semantic: null,
4553
+ };
3605
4554
  return {
3606
4555
  content: [
3607
4556
  {
3608
4557
  type: "text",
3609
- text: toolJson({
3610
- ok: true,
3611
- generated_at: new Date().toISOString(),
3612
- project_root: projectRoot,
3613
- root_source: rootSource,
3614
- db_path: dbPath,
3615
- watcher_enabled: !!watcher,
3616
- watcher_ready: watcherReady,
3617
- embeddings: {
3618
- enabled: embeddingsEnabled,
3619
- model: embedModelName,
3620
- embed_files: embedFilesMode,
3621
- },
3622
- output: {
3623
- include_content: includeContent,
3624
- preview_chars: previewChars,
3625
- content_max_chars: contentMaxChars,
3626
- requirements_limit: requirementsLimit,
3627
- changes_limit: changesLimit,
3628
- notes_limit: notesLimit,
3629
- conventions_limit: conventionsLimit,
3630
- },
3631
- project_summary,
3632
- conventions,
3633
- recent_notes,
3634
- pending_total,
3635
- pending_offset,
3636
- pending_limit,
3637
- pending_truncated,
3638
- pending_changes,
3639
- items,
3640
- }),
4558
+ text: toolText("get_brain_dump", outputValue, compactBrainDumpText(outputValue), args.format),
3641
4559
  },
3642
4560
  ],
3643
4561
  };
@@ -3645,11 +4563,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3645
4563
  if (toolName === "get_pending_changes") {
3646
4564
  const args = GetPendingChangesArgsSchema.parse(rawArgs);
3647
4565
  flushPendingChangeBuffer();
3648
- const total = Number(countPendingChangesStmt.get()?.total ?? 0);
3649
4566
  const offset = args.offset;
3650
4567
  const limit = args.limit;
3651
- const truncated = total > offset + limit;
3652
- const pending = listPendingChangesPageStmt.all(limit, offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4568
+ const pendingDbRows = listPendingChangesStmt.all();
4569
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset, limit });
4570
+ const total = mergedPending.total;
4571
+ const truncated = mergedPending.truncated;
4572
+ const pending = mergedPending.page;
3653
4573
  logActivity("get_pending_changes", {
3654
4574
  total,
3655
4575
  offset,
@@ -3818,6 +4738,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3818
4738
  clearActivityLog();
3819
4739
  return { content: [{ type: "text", text: toolJson({ ok: true }) }] };
3820
4740
  }
4741
+ if (toolName === "detect_rtk") {
4742
+ DetectRtkArgsSchema.parse(rawArgs);
4743
+ const result = detectRtk();
4744
+ const text = result.available
4745
+ ? `rtk available: ${result.version ?? result.command}\ncommand=${result.command} source=${result.source ?? "unknown"} gain_ok=${result.gain_ok ?? false}${result.path ? ` path=${result.path}` : ""}\n${result.note}`
4746
+ : `rtk unavailable: ${result.command}\nsource=${result.source ?? "none"} gain_ok=${result.gain_ok ?? false}${result.version ? ` version=${result.version}` : ""}${result.path ? ` path=${result.path}` : ""}\n${result.note}`;
4747
+ return { content: [{ type: "text", text }] };
4748
+ }
4749
+ if (toolName === "install_rtk") {
4750
+ const args = InstallRtkArgsSchema.parse(rawArgs);
4751
+ const result = installRtk(args);
4752
+ return { content: [{ type: "text", text: compactInstallRtkText(result) }] };
4753
+ }
4754
+ if (toolName === "get_token_savings") {
4755
+ const args = GetTokenSavingsArgsSchema.parse(rawArgs);
4756
+ const result = tokenSavingsSummary(args.limit);
4757
+ return {
4758
+ content: [
4759
+ {
4760
+ type: "text",
4761
+ text: args.format === "json" ? toolJson(result) : compactTokenSavingsText(result),
4762
+ },
4763
+ ],
4764
+ };
4765
+ }
3821
4766
  if (toolName === "grep") {
3822
4767
  const args = GrepArgsSchema.parse(rawArgs);
3823
4768
  const q = args.query;
@@ -3851,24 +4796,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3851
4796
  total_matches: ripgrepResult.total_matches,
3852
4797
  truncated: ripgrepResult.truncated,
3853
4798
  });
4799
+ const outputValue = {
4800
+ ok: true,
4801
+ backend: ripgrepResult.backend,
4802
+ rg_command: ripgrepResult.rg_command,
4803
+ query: q,
4804
+ mode,
4805
+ case_sensitive: caseSensitive,
4806
+ smart_case: smartCase,
4807
+ include_paths: includePaths ?? [],
4808
+ exclude_paths: excludePaths ?? [],
4809
+ matches: ripgrepResult.matches,
4810
+ total_matches: ripgrepResult.total_matches,
4811
+ truncated: ripgrepResult.truncated,
4812
+ };
3854
4813
  return {
3855
4814
  content: [
3856
4815
  {
3857
4816
  type: "text",
3858
- text: toolJson({
3859
- ok: true,
3860
- backend: ripgrepResult.backend,
3861
- rg_command: ripgrepResult.rg_command,
3862
- query: q,
3863
- mode,
3864
- case_sensitive: caseSensitive,
3865
- smart_case: smartCase,
3866
- include_paths: includePaths ?? [],
3867
- exclude_paths: excludePaths ?? [],
3868
- matches: ripgrepResult.matches,
3869
- total_matches: ripgrepResult.total_matches,
3870
- truncated: ripgrepResult.truncated,
3871
- }),
4817
+ text: toolCompactOrJson("grep", outputValue, compactGrepText(outputValue), args.format),
3872
4818
  },
3873
4819
  ],
3874
4820
  };
@@ -3945,28 +4891,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3945
4891
  matches: indexedResult.matches.length,
3946
4892
  truncated: indexedResult.truncated,
3947
4893
  });
4894
+ const outputValue = {
4895
+ ok: true,
4896
+ backend: indexedResult.backend,
4897
+ fallback_reason: "ripgrep_unavailable",
4898
+ ripgrep_error: ripgrepResult.error,
4899
+ ripgrep_attempts: ripgrepResult.attempts,
4900
+ query: q,
4901
+ mode,
4902
+ case_sensitive: caseSensitive,
4903
+ smart_case: smartCase,
4904
+ hint: indexedResult.hint,
4905
+ kinds,
4906
+ include_paths: includePaths ?? [],
4907
+ exclude_paths: excludePaths ?? [],
4908
+ candidates: indexedResult.candidates,
4909
+ matches: indexedResult.matches,
4910
+ truncated: indexedResult.truncated,
4911
+ };
3948
4912
  return {
3949
4913
  content: [
3950
4914
  {
3951
4915
  type: "text",
3952
- text: toolJson({
3953
- ok: true,
3954
- backend: indexedResult.backend,
3955
- fallback_reason: "ripgrep_unavailable",
3956
- ripgrep_error: ripgrepResult.error,
3957
- ripgrep_attempts: ripgrepResult.attempts,
3958
- query: q,
3959
- mode,
3960
- case_sensitive: caseSensitive,
3961
- smart_case: smartCase,
3962
- hint: indexedResult.hint,
3963
- kinds,
3964
- include_paths: includePaths ?? [],
3965
- exclude_paths: excludePaths ?? [],
3966
- candidates: indexedResult.candidates,
3967
- matches: indexedResult.matches,
3968
- truncated: indexedResult.truncated,
3969
- }),
4916
+ text: toolCompactOrJson("grep", outputValue, compactGrepText(outputValue), args.format),
3970
4917
  },
3971
4918
  ],
3972
4919
  };
@@ -4018,28 +4965,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4018
4965
  truncated: result.truncated,
4019
4966
  path_kind: st.isFile() ? "file" : st.isDirectory() ? "dir" : "other",
4020
4967
  });
4968
+ const outputValue = {
4969
+ ok: true,
4970
+ path: resolved.dbFilePath,
4971
+ path_kind: st.isFile() ? "file" : st.isDirectory() ? "dir" : "other",
4972
+ recursive: args.recursive,
4973
+ max_depth: args.recursive ? args.max_depth : 1,
4974
+ include_files: args.include_files,
4975
+ include_dirs: args.include_dirs,
4976
+ include_hidden: args.include_hidden,
4977
+ respect_ignore: args.respect_ignore,
4978
+ include_paths: includePaths ?? [],
4979
+ exclude_paths: excludePaths ?? [],
4980
+ extensions: extensions ?? [],
4981
+ returned: result.returned,
4982
+ scanned: result.scanned,
4983
+ truncated: result.truncated,
4984
+ entries: result.entries,
4985
+ };
4021
4986
  return {
4022
4987
  content: [
4023
4988
  {
4024
4989
  type: "text",
4025
- text: toolJson({
4026
- ok: true,
4027
- path: resolved.dbFilePath,
4028
- path_kind: st.isFile() ? "file" : st.isDirectory() ? "dir" : "other",
4029
- recursive: args.recursive,
4030
- max_depth: args.recursive ? args.max_depth : 1,
4031
- include_files: args.include_files,
4032
- include_dirs: args.include_dirs,
4033
- include_hidden: args.include_hidden,
4034
- respect_ignore: args.respect_ignore,
4035
- include_paths: includePaths ?? [],
4036
- exclude_paths: excludePaths ?? [],
4037
- extensions: extensions ?? [],
4038
- returned: result.returned,
4039
- scanned: result.scanned,
4040
- truncated: result.truncated,
4041
- entries: result.entries,
4042
- }),
4990
+ text: toolCompactOrJson("list_project_files", outputValue, compactListProjectFilesText(outputValue), args.format),
4043
4991
  },
4044
4992
  ],
4045
4993
  };
@@ -4079,19 +5027,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4079
5027
  total_chars: result.totalChars,
4080
5028
  truncated: result.truncated,
4081
5029
  });
5030
+ const outputValue = {
5031
+ ok: true,
5032
+ file_path: resolved.dbFilePath,
5033
+ offset: args.offset,
5034
+ returned_chars: result.returnedChars,
5035
+ total_chars: result.totalChars,
5036
+ truncated: result.truncated,
5037
+ text: result.text,
5038
+ };
4082
5039
  return {
4083
5040
  content: [
4084
5041
  {
4085
5042
  type: "text",
4086
- text: toolJson({
4087
- ok: true,
4088
- file_path: resolved.dbFilePath,
4089
- offset: args.offset,
4090
- returned_chars: result.returnedChars,
4091
- total_chars: result.totalChars,
4092
- truncated: result.truncated,
4093
- text: result.text,
4094
- }),
5043
+ text: toolCompactOrJson("read_file_text", outputValue, compactReadTextFileText(outputValue), args.format),
4095
5044
  },
4096
5045
  ],
4097
5046
  };
@@ -4138,20 +5087,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4138
5087
  total_chars: result.totalChars,
4139
5088
  truncated: result.truncated,
4140
5089
  });
5090
+ const outputValue = {
5091
+ ok: true,
5092
+ file_path: resolved.displayPath,
5093
+ allowed_root: resolved.allowedRoot,
5094
+ offset: args.offset,
5095
+ returned_chars: result.returnedChars,
5096
+ total_chars: result.totalChars,
5097
+ truncated: result.truncated,
5098
+ text: result.text,
5099
+ };
4141
5100
  return {
4142
5101
  content: [
4143
5102
  {
4144
5103
  type: "text",
4145
- text: toolJson({
4146
- ok: true,
4147
- file_path: resolved.displayPath,
4148
- allowed_root: resolved.allowedRoot,
4149
- offset: args.offset,
4150
- returned_chars: result.returnedChars,
4151
- total_chars: result.totalChars,
4152
- truncated: result.truncated,
4153
- text: result.text,
4154
- }),
5104
+ text: toolCompactOrJson("read_codex_text_file", outputValue, compactReadTextFileText(outputValue), args.format),
4155
5105
  },
4156
5106
  ],
4157
5107
  };
@@ -4213,19 +5163,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4213
5163
  returned: result.returned,
4214
5164
  truncated: result.truncated,
4215
5165
  });
5166
+ const outputValue = {
5167
+ ok: true,
5168
+ file_path: resolved.dbFilePath,
5169
+ from_line: fromLine,
5170
+ to_line: toLine,
5171
+ returned: result.returned,
5172
+ truncated: result.truncated,
5173
+ text: result.text,
5174
+ };
4216
5175
  return {
4217
5176
  content: [
4218
5177
  {
4219
5178
  type: "text",
4220
- text: toolJson({
4221
- ok: true,
4222
- file_path: resolved.dbFilePath,
4223
- from_line: fromLine,
4224
- to_line: toLine,
4225
- returned: result.returned,
4226
- truncated: result.truncated,
4227
- text: result.text,
4228
- }),
5179
+ text: toolCompactOrJson("read_file_lines", outputValue, compactReadFileLinesText(outputValue), args.format),
4229
5180
  },
4230
5181
  ],
4231
5182
  };
@@ -4242,11 +5193,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4242
5193
  matches: filtered.length,
4243
5194
  sample: filtered.slice(0, 10).map((m) => ({ name: m.name, type: m.type, file_path: m.file_path })),
4244
5195
  });
5196
+ const outputValue = { ok: true, query: q, matches: filtered };
4245
5197
  return {
4246
5198
  content: [
4247
5199
  {
4248
5200
  type: "text",
4249
- text: toolJson({ ok: true, query: q, matches: filtered }),
5201
+ text: toolCompactOrJson("query_codebase", outputValue, compactQueryCodebaseText(outputValue), args.format),
4250
5202
  },
4251
5203
  ],
4252
5204
  };
@@ -4287,6 +5239,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4287
5239
  ],
4288
5240
  };
4289
5241
  }
5242
+ if (toolName === "upsert_decision") {
5243
+ const args = UpsertDecisionArgsSchema.parse(rawArgs);
5244
+ const key = args.key.trim();
5245
+ const title = args.title.trim() || key;
5246
+ const content = args.content.trim();
5247
+ const meta = {
5248
+ status: "current",
5249
+ key,
5250
+ title,
5251
+ tags: args.tags ?? [],
5252
+ supersedes_req_ids: args.supersedes_req_ids ?? [],
5253
+ supersedes_memory_ids: args.supersedes_memory_ids ?? [],
5254
+ related_files: (args.related_files ?? []).map((f) => normalizeToDbPath(f)),
5255
+ };
5256
+ upsertDecisionStmt.run(key, `${title}\n\n${content}`, safeJson(meta), sha256Hex(`${title}\n\n${content}`));
5257
+ const row = getDecisionByKeyStmt.get(key);
5258
+ if (row)
5259
+ enqueueEmbedding(row.id);
5260
+ const superseded_requirements = supersedeRequirementIds(args.supersedes_req_ids ?? [], {
5261
+ decision_id: row?.id,
5262
+ reason: `Superseded by decision ${key}: ${title}`,
5263
+ });
5264
+ const superseded_memory_items = supersedeMemoryItemIds(args.supersedes_memory_ids ?? [], {
5265
+ decision_id: row?.id,
5266
+ reason: `Superseded by decision ${key}: ${title}`,
5267
+ });
5268
+ logActivity("upsert_decision", {
5269
+ key,
5270
+ decision_id: row?.id ?? null,
5271
+ superseded_requirements,
5272
+ superseded_memory_items,
5273
+ });
5274
+ return {
5275
+ content: [
5276
+ {
5277
+ type: "text",
5278
+ text: toolJson({
5279
+ ok: true,
5280
+ decision: row ? { id: row.id, key, updated_at: row.updated_at } : null,
5281
+ superseded_requirements,
5282
+ superseded_memory_items,
5283
+ }),
5284
+ },
5285
+ ],
5286
+ };
5287
+ }
5288
+ if (toolName === "supersede_memory") {
5289
+ const args = SupersedeMemoryArgsSchema.parse(rawArgs);
5290
+ const supersededReqIds = args.superseded_req_ids ?? [];
5291
+ const supersededMemoryIds = args.superseded_memory_ids ?? [];
5292
+ if (!supersededReqIds.length && !supersededMemoryIds.length) {
5293
+ return {
5294
+ isError: true,
5295
+ content: [
5296
+ {
5297
+ type: "text",
5298
+ text: toolJson({
5299
+ ok: false,
5300
+ error: "Provide superseded_req_ids and/or superseded_memory_ids.",
5301
+ }),
5302
+ },
5303
+ ],
5304
+ };
5305
+ }
5306
+ const superseded_requirements = supersedeRequirementIds(supersededReqIds, {
5307
+ req_id: args.replacement_req_id,
5308
+ memory_id: args.replacement_memory_id,
5309
+ reason: args.reason,
5310
+ });
5311
+ const superseded_memory_items = supersedeMemoryItemIds(supersededMemoryIds, {
5312
+ req_id: args.replacement_req_id,
5313
+ memory_id: args.replacement_memory_id,
5314
+ reason: args.reason,
5315
+ });
5316
+ logActivity("supersede_memory", {
5317
+ superseded_requirements,
5318
+ superseded_memory_items,
5319
+ replacement_req_id: args.replacement_req_id ?? null,
5320
+ replacement_memory_id: args.replacement_memory_id ?? null,
5321
+ });
5322
+ return {
5323
+ content: [
5324
+ {
5325
+ type: "text",
5326
+ text: toolJson({ ok: true, superseded_requirements, superseded_memory_items }),
5327
+ },
5328
+ ],
5329
+ };
5330
+ }
4290
5331
  if (toolName === "upsert_convention") {
4291
5332
  const args = UpsertConventionArgsSchema.parse(rawArgs);
4292
5333
  const key = args.key.trim();
@@ -4345,11 +5386,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4345
5386
  score: m.score,
4346
5387
  })),
4347
5388
  });
5389
+ const outputValue = { ok: true, ...result };
4348
5390
  return {
4349
5391
  content: [
4350
5392
  {
4351
5393
  type: "text",
4352
- text: toolJson({ ok: true, ...result }),
5394
+ text: toolCompactOrJson("semantic_search", outputValue, compactSemanticSearchText(outputValue), args.format),
4353
5395
  },
4354
5396
  ],
4355
5397
  };