@hiveai/mcp 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
-
6
3
  // src/server.ts
7
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
6
 
9
7
  // src/context.ts
10
8
  import { findProjectRoot, resolveHaivePaths } from "@hiveai/core";
@@ -142,7 +140,9 @@ var MemSaveInputSchema = {
142
140
  ),
143
141
  slug: z4.string().min(1).describe("Short human-readable identifier \u2014 becomes part of the filename"),
144
142
  body: z4.string().describe("Markdown body of the memory"),
145
- scope: z4.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope: personal | team | module"),
143
+ scope: z4.enum(["personal", "team", "module"]).optional().describe(
144
+ "Visibility scope: personal | team | module. When omitted, falls back to defaultScope in haive.config.json (default: personal)."
145
+ ),
146
146
  module: z4.string().optional().describe("Module name (required when scope=module)"),
147
147
  tags: z4.array(z4.string()).default([]).describe("Tags for filtering"),
148
148
  domain: z4.string().optional().describe("Domain (e.g. transactions, billing)"),
@@ -164,12 +164,14 @@ async function memSave(input, ctx) {
164
164
  );
165
165
  }
166
166
  const existing = existsSync4(ctx.paths.memoriesDir) ? await loadMemoriesFromDir2(ctx.paths.memoriesDir) : [];
167
+ const haiveConfig = await loadConfig(ctx.paths);
168
+ const resolvedScope = input.scope ?? haiveConfig.defaultScope ?? "personal";
167
169
  const invalidPaths = input.paths.filter(
168
170
  (p) => !existsSync4(path3.resolve(ctx.paths.root, p))
169
171
  );
170
172
  const incomingHash = bodyHash(input.body);
171
173
  const hashDuplicate = existing.find(
172
- ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === input.scope
174
+ ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === resolvedScope
173
175
  );
174
176
  if (hashDuplicate) {
175
177
  throw new Error(
@@ -178,7 +180,7 @@ async function memSave(input, ctx) {
178
180
  }
179
181
  if (input.topic) {
180
182
  const topicMatch = existing.find(
181
- ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
183
+ ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === resolvedScope && (!input.module || memory.frontmatter.module === input.module)
182
184
  );
183
185
  if (topicMatch) {
184
186
  const fm = topicMatch.memory.frontmatter;
@@ -208,8 +210,6 @@ async function memSave(input, ctx) {
208
210
  };
209
211
  }
210
212
  }
211
- const haiveConfig = await loadConfig(ctx.paths);
212
- const resolvedScope = input.scope !== "personal" ? input.scope : haiveConfig.defaultScope ?? "personal";
213
213
  const frontmatter = buildFrontmatter({
214
214
  type: input.type,
215
215
  slug: input.slug,
@@ -1040,9 +1040,9 @@ async function memObserve(input, ctx) {
1040
1040
  }
1041
1041
 
1042
1042
  // src/tools/mem-session-end.ts
1043
- import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
1044
- import { existsSync as existsSync16 } from "fs";
1045
- import path7 from "path";
1043
+ import { writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
1044
+ import { existsSync as existsSync17 } from "fs";
1045
+ import path8 from "path";
1046
1046
  import {
1047
1047
  buildFrontmatter as buildFrontmatter4,
1048
1048
  loadMemoriesFromDir as loadMemoriesFromDir12,
@@ -1050,6 +1050,134 @@ import {
1050
1050
  serializeMemory as serializeMemory8
1051
1051
  } from "@hiveai/core";
1052
1052
  import { z as z16 } from "zod";
1053
+
1054
+ // src/session-tracker.ts
1055
+ import { appendUsageEvent, loadConfig as loadConfig2 } from "@hiveai/core";
1056
+ import { mkdir as mkdir5, writeFile as writeFile9, rm } from "fs/promises";
1057
+ import { existsSync as existsSync16 } from "fs";
1058
+ import path7 from "path";
1059
+ import { execSync } from "child_process";
1060
+ function pendingDistillPath(ctx) {
1061
+ return path7.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1062
+ }
1063
+ var SessionTracker = class {
1064
+ events = [];
1065
+ startedAt = (/* @__PURE__ */ new Date()).toISOString();
1066
+ config = null;
1067
+ ctx;
1068
+ shutdownRegistered = false;
1069
+ constructor(ctx) {
1070
+ this.ctx = ctx;
1071
+ }
1072
+ async init() {
1073
+ this.config = await loadConfig2(this.ctx.paths);
1074
+ if (this.config.autoSessionEnd) {
1075
+ this.registerShutdownHandler();
1076
+ }
1077
+ }
1078
+ record(tool, summary) {
1079
+ const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
1080
+ this.events.push(event);
1081
+ void appendUsageEvent(this.ctx.paths, event);
1082
+ }
1083
+ registerShutdownHandler() {
1084
+ if (this.shutdownRegistered) return;
1085
+ this.shutdownRegistered = true;
1086
+ const save = async () => {
1087
+ const writingTools = this.events.filter(
1088
+ (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
1089
+ );
1090
+ const totalCalls = this.events.length;
1091
+ if (totalCalls === 0) return;
1092
+ const toolSummary = summarizeTools(this.events);
1093
+ const filesSet = /* @__PURE__ */ new Set();
1094
+ for (const e of this.events) {
1095
+ if (e.summary) {
1096
+ const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
1097
+ for (const m of matches) filesSet.add(m);
1098
+ }
1099
+ }
1100
+ let gitDiff;
1101
+ try {
1102
+ const raw = execSync("git diff HEAD", {
1103
+ cwd: this.ctx.paths.root,
1104
+ timeout: 5e3,
1105
+ encoding: "utf8",
1106
+ stdio: ["ignore", "pipe", "ignore"]
1107
+ });
1108
+ gitDiff = raw.slice(0, 8192) || void 0;
1109
+ } catch {
1110
+ }
1111
+ let recapId;
1112
+ try {
1113
+ const result = await memSessionEnd(
1114
+ {
1115
+ goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
1116
+ accomplished: toolSummary,
1117
+ discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
1118
+ files_touched: [...filesSet].slice(0, 10),
1119
+ next_steps: "",
1120
+ scope: this.config?.defaultScope ?? "personal",
1121
+ module: void 0
1122
+ },
1123
+ this.ctx
1124
+ );
1125
+ recapId = result.id;
1126
+ } catch {
1127
+ }
1128
+ const ranPostTask = this.events.some(
1129
+ (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1130
+ );
1131
+ if (!ranPostTask && existsSync16(this.ctx.paths.haiveDir)) {
1132
+ try {
1133
+ const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
1134
+ const payload = {
1135
+ session_start: this.startedAt,
1136
+ session_end: (/* @__PURE__ */ new Date()).toISOString(),
1137
+ total_tool_calls: totalCalls,
1138
+ tool_summary: toolSummary,
1139
+ memories_saved: memoriesSaved,
1140
+ git_diff_available: !!gitDiff,
1141
+ ...gitDiff ? { git_diff: gitDiff } : {},
1142
+ ...recapId ? { recap_id: recapId } : {}
1143
+ };
1144
+ const cacheDir = path7.join(this.ctx.paths.haiveDir, ".cache");
1145
+ await mkdir5(cacheDir, { recursive: true });
1146
+ await writeFile9(
1147
+ pendingDistillPath(this.ctx),
1148
+ JSON.stringify(payload, null, 2) + "\n",
1149
+ "utf8"
1150
+ );
1151
+ } catch {
1152
+ }
1153
+ }
1154
+ };
1155
+ process.once("SIGTERM", () => {
1156
+ void save().finally(() => process.exit(0));
1157
+ });
1158
+ process.once("SIGINT", () => {
1159
+ void save().finally(() => process.exit(0));
1160
+ });
1161
+ }
1162
+ };
1163
+ async function clearPendingDistill(ctx) {
1164
+ const p = pendingDistillPath(ctx);
1165
+ if (existsSync16(p)) {
1166
+ try {
1167
+ await rm(p);
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ }
1172
+ function summarizeTools(events) {
1173
+ const counts = /* @__PURE__ */ new Map();
1174
+ for (const e of events) {
1175
+ counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
1176
+ }
1177
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
1178
+ }
1179
+
1180
+ // src/tools/mem-session-end.ts
1053
1181
  var MemSessionEndInputSchema = {
1054
1182
  goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1055
1183
  accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
@@ -1089,18 +1217,18 @@ ${input.next_steps}`);
1089
1217
  return lines.join("\n");
1090
1218
  }
1091
1219
  async function memSessionEnd(input, ctx) {
1092
- if (!existsSync16(ctx.paths.haiveDir)) {
1220
+ if (!existsSync17(ctx.paths.haiveDir)) {
1093
1221
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1094
1222
  }
1095
1223
  const body = buildBody(input);
1096
1224
  const topic = recapTopic(input.scope, input.module);
1097
1225
  const invalidPaths = input.files_touched.filter(
1098
- (p) => !existsSync16(path7.resolve(ctx.paths.root, p))
1226
+ (p) => !existsSync17(path8.resolve(ctx.paths.root, p))
1099
1227
  );
1100
1228
  if (invalidPaths.length > 0) {
1101
1229
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
1102
1230
  }
1103
- const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1231
+ const existing = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1104
1232
  const topicMatch = existing.find(
1105
1233
  ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1106
1234
  );
@@ -1115,11 +1243,12 @@ async function memSessionEnd(input, ctx) {
1115
1243
  paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1116
1244
  }
1117
1245
  };
1118
- await writeFile9(
1246
+ await writeFile10(
1119
1247
  topicMatch.filePath,
1120
1248
  serializeMemory8({ frontmatter: newFrontmatter, body }),
1121
1249
  "utf8"
1122
1250
  );
1251
+ await clearPendingDistill(ctx);
1123
1252
  return {
1124
1253
  id: fm.id,
1125
1254
  scope: fm.scope,
@@ -1144,8 +1273,9 @@ async function memSessionEnd(input, ctx) {
1144
1273
  frontmatter.id,
1145
1274
  frontmatter.module
1146
1275
  );
1147
- await mkdir5(path7.dirname(file), { recursive: true });
1148
- await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1276
+ await mkdir6(path8.dirname(file), { recursive: true });
1277
+ await writeFile10(file, serializeMemory8({ frontmatter, body }), "utf8");
1278
+ await clearPendingDistill(ctx);
1149
1279
  return {
1150
1280
  id: frontmatter.id,
1151
1281
  scope: frontmatter.scope,
@@ -1156,24 +1286,27 @@ async function memSessionEnd(input, ctx) {
1156
1286
  }
1157
1287
 
1158
1288
  // src/tools/get-briefing.ts
1159
- import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1160
- import { existsSync as existsSync17 } from "fs";
1161
- import path8 from "path";
1289
+ import { readFile as readFile3, readdir as readdir3, writeFile as writeFile11 } from "fs/promises";
1290
+ import { existsSync as existsSync18 } from "fs";
1291
+ import path9 from "path";
1162
1292
  import {
1163
1293
  allocateBudget,
1294
+ DEFAULT_AUTO_PROMOTE_RULE,
1164
1295
  deriveConfidence as deriveConfidence4,
1165
1296
  estimateTokens,
1166
1297
  getUsage as getUsage5,
1167
1298
  inferModulesFromPaths as inferModulesFromPaths2,
1299
+ isAutoPromoteEligible,
1168
1300
  isDecaying,
1169
1301
  literalMatchesAllTokens as literalMatchesAllTokens2,
1170
1302
  literalMatchesAnyToken as literalMatchesAnyToken2,
1171
1303
  loadCodeMap,
1172
- loadConfig as loadConfig2,
1304
+ loadConfig as loadConfig3,
1173
1305
  loadMemoriesFromDir as loadMemoriesFromDir13,
1174
1306
  loadUsageIndex as loadUsageIndex7,
1175
1307
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
1176
1308
  queryCodeMap,
1309
+ serializeMemory as serializeMemory9,
1177
1310
  tokenizeQuery as tokenizeQuery2,
1178
1311
  trackReads as trackReads3,
1179
1312
  truncateToTokens
@@ -1212,7 +1345,7 @@ async function getBriefing(input, ctx) {
1212
1345
  let usage = { version: 1, updated_at: "", by_id: {} };
1213
1346
  let byId = /* @__PURE__ */ new Map();
1214
1347
  let lastSession;
1215
- if (existsSync17(ctx.paths.memoriesDir)) {
1348
+ if (existsSync18(ctx.paths.memoriesDir)) {
1216
1349
  const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1217
1350
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1218
1351
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -1330,15 +1463,38 @@ async function getBriefing(input, ctx) {
1330
1463
  memories.push(...ranked.slice(0, input.max_memories));
1331
1464
  if (input.track && memories.length > 0) {
1332
1465
  await trackReads3(ctx.paths, memories.map((m) => m.id));
1466
+ const freshUsage = await loadUsageIndex7(ctx.paths);
1467
+ const cfg = await loadConfig3(ctx.paths);
1468
+ const rule = {
1469
+ minReads: cfg.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE.minReads,
1470
+ maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
1471
+ };
1472
+ for (const m of memories) {
1473
+ const loaded = byId.get(m.id);
1474
+ if (!loaded) continue;
1475
+ const u = getUsage5(freshUsage, m.id);
1476
+ if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
1477
+ const newFm = { ...loaded.memory.frontmatter, status: "validated" };
1478
+ try {
1479
+ await writeFile11(
1480
+ loaded.filePath,
1481
+ serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
1482
+ "utf8"
1483
+ );
1484
+ m.status = "validated";
1485
+ m.confidence = "trusted";
1486
+ } catch {
1487
+ }
1488
+ }
1333
1489
  }
1334
1490
  }
1335
- const projectContextRaw = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1491
+ const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1336
1492
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1337
1493
  const setupWarnings = [];
1338
1494
  let autoContextGenerated = false;
1339
1495
  let projectContext = isTemplateContext ? "" : projectContextRaw;
1340
- if ((isTemplateContext || !existsSync17(ctx.paths.projectContext)) && input.include_project_context) {
1341
- const haiveConfig = await loadConfig2(ctx.paths);
1496
+ if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
1497
+ const haiveConfig = await loadConfig3(ctx.paths);
1342
1498
  if (haiveConfig.autoContext) {
1343
1499
  const codeMap = await loadCodeMap(ctx.paths);
1344
1500
  if (codeMap) {
@@ -1490,7 +1646,7 @@ ${m.content}`).join("\n\n---\n\n"),
1490
1646
  developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${m.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
1491
1647
  });
1492
1648
  }
1493
- if (existsSync17(ctx.paths.memoriesDir)) {
1649
+ if (existsSync18(ctx.paths.memoriesDir)) {
1494
1650
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1495
1651
  for (const { memory } of allMems) {
1496
1652
  const fm = memory.frontmatter;
@@ -1508,8 +1664,37 @@ ${m.content}`).join("\n\n---\n\n"),
1508
1664
  });
1509
1665
  }
1510
1666
  }
1667
+ const pendingDistillFile = pendingDistillPath(ctx);
1668
+ if (existsSync18(pendingDistillFile)) {
1669
+ try {
1670
+ const raw = await readFile3(pendingDistillFile, "utf8");
1671
+ const pd = JSON.parse(raw);
1672
+ const ageMs = Date.now() - new Date(pd.session_end).getTime();
1673
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
1674
+ if (ageMs < SEVEN_DAYS) {
1675
+ const savedNote = pd.memories_saved.length > 0 ? ` ${pd.memories_saved.length} memor${pd.memories_saved.length === 1 ? "y was" : "ies were"} saved.` : " No memories were saved.";
1676
+ const diffNote = pd.git_diff_available ? " A git diff snapshot is available in the pending-distill file for context." : "";
1677
+ actionRequired.push({
1678
+ id: "__pending_distill__",
1679
+ summary: "Previous session has undistilled learnings \u2014 invoke post_task to capture them",
1680
+ developer_message: `The previous session (${pd.total_tool_calls} tool calls, ${pd.tool_summary}) was closed by autopilot without a full post_task distillation.${savedNote}${diffNote}
1681
+
1682
+ **Before starting your task:** invoke the MCP prompt \`post_task\` to capture any decisions, gotchas, or conventions from that session. This takes ~30 seconds and prevents institutional knowledge from being lost.
1683
+
1684
+ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pending distill marker.`
1685
+ });
1686
+ } else {
1687
+ try {
1688
+ const { rm: rm2 } = await import("fs/promises");
1689
+ await rm2(pendingDistillFile);
1690
+ } catch {
1691
+ }
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ }
1511
1696
  const memoriesEmpty = outputMemories.length === 0;
1512
- const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
1697
+ const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
1513
1698
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
1514
1699
  const hints = [];
1515
1700
  if (isColdStart) {
@@ -1588,15 +1773,15 @@ async function trySemanticHits(ctx, task, limit) {
1588
1773
  }
1589
1774
  async function loadModuleContexts2(ctx, modules) {
1590
1775
  if (modules.length === 0) return [];
1591
- if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1776
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
1592
1777
  const available = new Set(
1593
1778
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1594
1779
  );
1595
1780
  const out = [];
1596
1781
  for (const m of modules) {
1597
1782
  if (!available.has(m)) continue;
1598
- const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1599
- if (existsSync17(file)) {
1783
+ const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
1784
+ if (existsSync18(file)) {
1600
1785
  out.push({ name: m, content: await readFile3(file, "utf8") });
1601
1786
  }
1602
1787
  }
@@ -1685,7 +1870,7 @@ function estimateFileEntryTokens(f) {
1685
1870
  }
1686
1871
 
1687
1872
  // src/tools/mem-diff.ts
1688
- import { existsSync as existsSync18 } from "fs";
1873
+ import { existsSync as existsSync19 } from "fs";
1689
1874
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1690
1875
  import { z as z19 } from "zod";
1691
1876
  var MemDiffInputSchema = {
@@ -1693,7 +1878,7 @@ var MemDiffInputSchema = {
1693
1878
  id_b: z19.string().min(1).describe("Second memory id")
1694
1879
  };
1695
1880
  async function memDiff(input, ctx) {
1696
- if (!existsSync18(ctx.paths.memoriesDir)) {
1881
+ if (!existsSync19(ctx.paths.memoriesDir)) {
1697
1882
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1698
1883
  }
1699
1884
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -1730,7 +1915,7 @@ async function memDiff(input, ctx) {
1730
1915
  }
1731
1916
 
1732
1917
  // src/tools/get-recap.ts
1733
- import { existsSync as existsSync19 } from "fs";
1918
+ import { existsSync as existsSync20 } from "fs";
1734
1919
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
1735
1920
  import { z as z20 } from "zod";
1736
1921
  var GetRecapInputSchema = {
@@ -1739,7 +1924,7 @@ var GetRecapInputSchema = {
1739
1924
  )
1740
1925
  };
1741
1926
  async function getRecap(input, ctx) {
1742
- if (!existsSync19(ctx.paths.memoriesDir)) {
1927
+ if (!existsSync20(ctx.paths.memoriesDir)) {
1743
1928
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
1744
1929
  }
1745
1930
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -1837,9 +2022,9 @@ async function codeSearch(input, ctx) {
1837
2022
  }
1838
2023
 
1839
2024
  // src/tools/why-this-file.ts
1840
- import { existsSync as existsSync20 } from "fs";
2025
+ import { existsSync as existsSync21 } from "fs";
1841
2026
  import { spawn } from "child_process";
1842
- import path9 from "path";
2027
+ import path10 from "path";
1843
2028
  import {
1844
2029
  deriveConfidence as deriveConfidence5,
1845
2030
  getUsage as getUsage6,
@@ -1857,7 +2042,7 @@ var WhyThisFileInputSchema = {
1857
2042
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
1858
2043
  };
1859
2044
  async function whyThisFile(input, ctx) {
1860
- const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
2045
+ const fileExists = existsSync21(path10.join(ctx.paths.root, input.path));
1861
2046
  const [commits, memories, codeMap] = await Promise.all([
1862
2047
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
1863
2048
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -1898,7 +2083,7 @@ async function whyThisFile(input, ctx) {
1898
2083
  };
1899
2084
  }
1900
2085
  async function collectAnchoredMemories(ctx, filePath, limit) {
1901
- if (!existsSync20(ctx.paths.memoriesDir)) return [];
2086
+ if (!existsSync21(ctx.paths.memoriesDir)) return [];
1902
2087
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
1903
2088
  const usage = await loadUsageIndex8(ctx.paths);
1904
2089
  const out = [];
@@ -1953,7 +2138,7 @@ function runCommand(cmd, args, cwd) {
1953
2138
  }
1954
2139
 
1955
2140
  // src/tools/anti-patterns-check.ts
1956
- import { existsSync as existsSync21 } from "fs";
2141
+ import { existsSync as existsSync22 } from "fs";
1957
2142
  import {
1958
2143
  deriveConfidence as deriveConfidence6,
1959
2144
  getUsage as getUsage7,
@@ -1984,7 +2169,7 @@ async function antiPatternsCheck(input, ctx) {
1984
2169
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
1985
2170
  };
1986
2171
  }
1987
- if (!existsSync21(ctx.paths.memoriesDir)) {
2172
+ if (!existsSync22(ctx.paths.memoriesDir)) {
1988
2173
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
1989
2174
  }
1990
2175
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -2066,7 +2251,7 @@ async function antiPatternsCheck(input, ctx) {
2066
2251
  }
2067
2252
 
2068
2253
  // src/tools/mem-distill.ts
2069
- import { existsSync as existsSync22 } from "fs";
2254
+ import { existsSync as existsSync23 } from "fs";
2070
2255
  import {
2071
2256
  loadMemoriesFromDir as loadMemoriesFromDir18,
2072
2257
  tokenizeQuery as tokenizeQuery4
@@ -2118,7 +2303,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2118
2303
  "error"
2119
2304
  ]);
2120
2305
  async function memDistill(input, ctx) {
2121
- if (!existsSync22(ctx.paths.memoriesDir)) {
2306
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2122
2307
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2123
2308
  }
2124
2309
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -2226,7 +2411,7 @@ function firstHeading(body) {
2226
2411
  }
2227
2412
 
2228
2413
  // src/tools/why-this-decision.ts
2229
- import { existsSync as existsSync23 } from "fs";
2414
+ import { existsSync as existsSync24 } from "fs";
2230
2415
  import { spawn as spawn2 } from "child_process";
2231
2416
  import {
2232
2417
  deriveConfidence as deriveConfidence7,
@@ -2241,7 +2426,7 @@ var WhyThisDecisionInputSchema = {
2241
2426
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2242
2427
  };
2243
2428
  async function whyThisDecision(input, ctx) {
2244
- if (!existsSync23(ctx.paths.memoriesDir)) {
2429
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2245
2430
  return {
2246
2431
  found: false,
2247
2432
  related: [],
@@ -2373,7 +2558,7 @@ function runCommand2(cmd, args, cwd) {
2373
2558
  }
2374
2559
 
2375
2560
  // src/tools/mem-conflicts.ts
2376
- import { existsSync as existsSync24 } from "fs";
2561
+ import { existsSync as existsSync25 } from "fs";
2377
2562
  import {
2378
2563
  deriveConfidence as deriveConfidence8,
2379
2564
  getUsage as getUsage9,
@@ -2391,7 +2576,7 @@ var MemConflictsInputSchema = {
2391
2576
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2392
2577
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2393
2578
  async function memConflicts(input, ctx) {
2394
- if (!existsSync24(ctx.paths.memoriesDir)) {
2579
+ if (!existsSync25(ctx.paths.memoriesDir)) {
2395
2580
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2396
2581
  }
2397
2582
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -2572,13 +2757,226 @@ async function preCommitCheck(input, ctx) {
2572
2757
  };
2573
2758
  }
2574
2759
 
2575
- // src/prompts/bootstrap-project.ts
2760
+ // src/tools/pattern-detect.ts
2761
+ import { mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2762
+ import { existsSync as existsSync26 } from "fs";
2763
+ import path11 from "path";
2764
+ import { execSync as execSync2 } from "child_process";
2765
+ import {
2766
+ buildFrontmatter as buildFrontmatter5,
2767
+ memoryFilePath as memoryFilePath5,
2768
+ readUsageEvents,
2769
+ serializeMemory as serializeMemory10
2770
+ } from "@hiveai/core";
2576
2771
  import { z as z29 } from "zod";
2772
+ var CONFIG_PATTERNS = [
2773
+ ".eslintrc",
2774
+ "eslint.config",
2775
+ "prettier.config",
2776
+ ".prettierrc",
2777
+ "tsconfig",
2778
+ "jsconfig",
2779
+ "vitest.config",
2780
+ "jest.config",
2781
+ ".env.example",
2782
+ ".env.defaults",
2783
+ "tailwind.config",
2784
+ "vite.config",
2785
+ "next.config",
2786
+ "babel.config",
2787
+ "postcss.config",
2788
+ "renovate.json",
2789
+ "dependabot.yml"
2790
+ ];
2791
+ var MAX_DIFF_BYTES = 4096;
2792
+ var HOT_FILE_MIN = 3;
2793
+ var PatternDetectInputSchema = {
2794
+ since_days: z29.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
2795
+ dry_run: z29.boolean().default(false).describe("When true, report matches without writing any memory files."),
2796
+ scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
2797
+ };
2798
+ async function patternDetect(input, ctx) {
2799
+ if (!existsSync26(ctx.paths.haiveDir)) {
2800
+ return {
2801
+ scanned_events: 0,
2802
+ matches: [],
2803
+ saved: 0,
2804
+ saved_ids: [],
2805
+ notice: "No .ai/ directory found. Run 'haive init' first."
2806
+ };
2807
+ }
2808
+ const matches = [];
2809
+ try {
2810
+ const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
2811
+ const configFiles = changedFiles.filter(
2812
+ (f) => CONFIG_PATTERNS.some((p) => path11.basename(f.toLowerCase()).includes(p))
2813
+ );
2814
+ for (const file of configFiles.slice(0, 5)) {
2815
+ const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
2816
+ if (!diff) continue;
2817
+ const parentDir = path11.basename(path11.dirname(file));
2818
+ const baseName = path11.basename(file).replace(/\.[^.]+$/, "");
2819
+ const slug = `${parentDir}-${baseName}`.replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
2820
+ matches.push({
2821
+ kind: "config_change",
2822
+ signal: `Config file modified: ${file}`,
2823
+ proposed_type: "convention",
2824
+ proposed_slug: `config-change-${slug}`,
2825
+ proposed_body: [
2826
+ `# Config change: \`${file}\``,
2827
+ "",
2828
+ "This configuration file was recently modified. The diff below captures the intent.",
2829
+ "Review and update this memory with the **reason** for the change if known.",
2830
+ "",
2831
+ "```diff",
2832
+ diff.slice(0, MAX_DIFF_BYTES),
2833
+ "```"
2834
+ ].join("\n"),
2835
+ anchor_paths: [file]
2836
+ });
2837
+ }
2838
+ } catch {
2839
+ }
2840
+ const events = await readUsageEvents(ctx.paths);
2841
+ const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
2842
+ const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
2843
+ const pathCounts = /* @__PURE__ */ new Map();
2844
+ for (const e of recent) {
2845
+ if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
2846
+ if (!e.summary) continue;
2847
+ const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
2848
+ for (const t of tokens) {
2849
+ const key = t.toLowerCase();
2850
+ const existing = pathCounts.get(key);
2851
+ if (existing) {
2852
+ existing.count++;
2853
+ existing.tools.add(e.tool);
2854
+ } else {
2855
+ pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
2856
+ }
2857
+ }
2858
+ }
2859
+ for (const [p, { count, tools }] of pathCounts) {
2860
+ if (count < HOT_FILE_MIN) continue;
2861
+ const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
2862
+ if (!isGotchaSignal) continue;
2863
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2864
+ matches.push({
2865
+ kind: "repeated_path",
2866
+ signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
2867
+ proposed_type: "gotcha",
2868
+ proposed_slug: `repeated-issue-${slug}`,
2869
+ proposed_body: [
2870
+ `# Recurring issue near \`${p}\``,
2871
+ "",
2872
+ `This file appeared ${count} times in failed-approach or observation events over the last ${input.since_days} days. Review the related attempt/gotcha memories and consolidate them into a single authoritative gotcha.`,
2873
+ "",
2874
+ `**Source signals:** ${[...tools].join(", ")} (${count} events)`
2875
+ ].join("\n"),
2876
+ anchor_paths: [p]
2877
+ });
2878
+ }
2879
+ for (const [p, { count, tools }] of pathCounts) {
2880
+ if (count < HOT_FILE_MIN) continue;
2881
+ if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
2882
+ if (CONFIG_PATTERNS.some((cp) => path11.basename(p).includes(cp))) continue;
2883
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2884
+ matches.push({
2885
+ kind: "hot_file",
2886
+ signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
2887
+ proposed_type: "convention",
2888
+ proposed_slug: `hot-file-${slug}`,
2889
+ proposed_body: [
2890
+ `# Frequent edits to \`${p}\``,
2891
+ "",
2892
+ `This file was referenced ${count} times in memory-saving events over the last ${input.since_days} days \u2014 a signal that a recurring pattern or convention applies here.`,
2893
+ "",
2894
+ "**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
2895
+ ].join("\n"),
2896
+ anchor_paths: [p]
2897
+ });
2898
+ }
2899
+ if (matches.length === 0) {
2900
+ return {
2901
+ scanned_events: recent.length,
2902
+ matches: [],
2903
+ saved: 0,
2904
+ saved_ids: [],
2905
+ notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
2906
+ };
2907
+ }
2908
+ if (input.dry_run) {
2909
+ return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
2910
+ }
2911
+ const savedIds = [];
2912
+ for (const match of matches) {
2913
+ try {
2914
+ const fm = buildFrontmatter5({
2915
+ type: match.proposed_type,
2916
+ slug: match.proposed_slug,
2917
+ scope: input.scope,
2918
+ tags: ["pattern-detect", match.kind],
2919
+ paths: match.anchor_paths,
2920
+ status: "proposed"
2921
+ });
2922
+ const file = memoryFilePath5(
2923
+ ctx.paths,
2924
+ fm.scope === "shared" ? "team" : fm.scope,
2925
+ fm.id,
2926
+ void 0
2927
+ );
2928
+ if (existsSync26(file)) continue;
2929
+ await mkdir7(path11.dirname(file), { recursive: true });
2930
+ await writeFile12(
2931
+ file,
2932
+ serializeMemory10({ frontmatter: fm, body: match.proposed_body }),
2933
+ "utf8"
2934
+ );
2935
+ savedIds.push(fm.id);
2936
+ } catch {
2937
+ }
2938
+ }
2939
+ return {
2940
+ scanned_events: recent.length,
2941
+ matches,
2942
+ saved: savedIds.length,
2943
+ saved_ids: savedIds
2944
+ };
2945
+ }
2946
+ function gitChangedFiles(root, sinceDays) {
2947
+ try {
2948
+ const out = execSync2(
2949
+ `git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
2950
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2951
+ );
2952
+ return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
2953
+ } catch {
2954
+ return [];
2955
+ }
2956
+ }
2957
+ function gitFileDiff(root, file, sinceDays) {
2958
+ try {
2959
+ const out = execSync2(
2960
+ `git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
2961
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2962
+ );
2963
+ if (!out.trim()) return null;
2964
+ const diffLines = out.split("\n").filter(
2965
+ (l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
2966
+ );
2967
+ return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
2968
+ } catch {
2969
+ return null;
2970
+ }
2971
+ }
2972
+
2973
+ // src/prompts/bootstrap-project.ts
2974
+ import { z as z30 } from "zod";
2577
2975
  var BootstrapProjectArgsSchema = {
2578
- module: z29.string().optional().describe(
2976
+ module: z30.string().optional().describe(
2579
2977
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
2580
2978
  ),
2581
- focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2979
+ focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2582
2980
  };
2583
2981
  var ROOT_TEMPLATE = `# Project context
2584
2982
 
@@ -2660,10 +3058,10 @@ ${template}\`\`\`
2660
3058
  }
2661
3059
 
2662
3060
  // src/prompts/post-task.ts
2663
- import { z as z30 } from "zod";
3061
+ import { z as z31 } from "zod";
2664
3062
  var PostTaskArgsSchema = {
2665
- task_summary: z30.string().optional().describe("One sentence describing what you just did"),
2666
- files_touched: z30.array(z30.string()).optional().describe("Files you created or modified during the task")
3063
+ task_summary: z31.string().optional().describe("One sentence describing what you just did"),
3064
+ files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
2667
3065
  };
2668
3066
  function postTaskPrompt(args, ctx) {
2669
3067
  const taskLine = args.task_summary ? `
@@ -2731,6 +3129,8 @@ Call **\`mem_session_end\`** with:
2731
3129
 
2732
3130
  This creates/updates a single rolling recap that **get_briefing automatically surfaces** at the start of every subsequent session \u2014 no token waste re-explaining what happened.
2733
3131
 
3132
+ Calling \`mem_session_end\` also **clears the pending-distill marker** (if any), confirming that this session's learnings have been properly captured rather than left as an auto-recap skeleton.
3133
+
2734
3134
  When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
2735
3135
  `;
2736
3136
  return {
@@ -2745,12 +3145,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
2745
3145
  }
2746
3146
 
2747
3147
  // src/prompts/import-docs.ts
2748
- import { z as z31 } from "zod";
3148
+ import { z as z32 } from "zod";
2749
3149
  var ImportDocsArgsSchema = {
2750
- content: z31.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
2751
- source: z31.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
2752
- scope: z31.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
2753
- dry_run: z31.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
3150
+ content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3151
+ source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3152
+ scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3153
+ dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2754
3154
  };
2755
3155
  function importDocsPrompt(args, ctx) {
2756
3156
  const sourceLine = args.source ? `
@@ -2813,80 +3213,9 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
2813
3213
  };
2814
3214
  }
2815
3215
 
2816
- // src/session-tracker.ts
2817
- import { appendUsageEvent, loadConfig as loadConfig3 } from "@hiveai/core";
2818
- var SessionTracker = class {
2819
- events = [];
2820
- startedAt = (/* @__PURE__ */ new Date()).toISOString();
2821
- config = null;
2822
- ctx;
2823
- shutdownRegistered = false;
2824
- constructor(ctx) {
2825
- this.ctx = ctx;
2826
- }
2827
- async init() {
2828
- this.config = await loadConfig3(this.ctx.paths);
2829
- if (this.config.autoSessionEnd) {
2830
- this.registerShutdownHandler();
2831
- }
2832
- }
2833
- record(tool, summary) {
2834
- const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
2835
- this.events.push(event);
2836
- void appendUsageEvent(this.ctx.paths, event);
2837
- }
2838
- registerShutdownHandler() {
2839
- if (this.shutdownRegistered) return;
2840
- this.shutdownRegistered = true;
2841
- const save = async () => {
2842
- const writingTools = this.events.filter(
2843
- (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
2844
- );
2845
- const totalCalls = this.events.length;
2846
- if (totalCalls === 0) return;
2847
- const toolSummary = summarizeTools(this.events);
2848
- const filesSet = /* @__PURE__ */ new Set();
2849
- for (const e of this.events) {
2850
- if (e.summary) {
2851
- const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
2852
- for (const m of matches) filesSet.add(m);
2853
- }
2854
- }
2855
- try {
2856
- await memSessionEnd(
2857
- {
2858
- goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
2859
- accomplished: toolSummary,
2860
- discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
2861
- files_touched: [...filesSet].slice(0, 10),
2862
- next_steps: "",
2863
- scope: this.config?.defaultScope ?? "personal",
2864
- module: void 0
2865
- },
2866
- this.ctx
2867
- );
2868
- } catch {
2869
- }
2870
- };
2871
- process.once("SIGTERM", () => {
2872
- void save().finally(() => process.exit(0));
2873
- });
2874
- process.once("SIGINT", () => {
2875
- void save().finally(() => process.exit(0));
2876
- });
2877
- }
2878
- };
2879
- function summarizeTools(events) {
2880
- const counts = /* @__PURE__ */ new Map();
2881
- for (const e of events) {
2882
- counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
2883
- }
2884
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
2885
- }
2886
-
2887
3216
  // src/server.ts
2888
3217
  var SERVER_NAME = "haive";
2889
- var SERVER_VERSION = "0.8.0";
3218
+ var SERVER_VERSION = "0.9.2";
2890
3219
  function jsonResult(data) {
2891
3220
  return {
2892
3221
  content: [
@@ -3531,6 +3860,37 @@ function createHaiveServer(options = {}) {
3531
3860
  return jsonResult(await preCommitCheck(input, context));
3532
3861
  }
3533
3862
  );
3863
+ server.tool(
3864
+ "pattern_detect",
3865
+ [
3866
+ "Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
3867
+ "",
3868
+ "Runs three signals over local git history and the tool-usage log:",
3869
+ " 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
3870
+ " \u2192 proposes a convention memory with the git diff as body.",
3871
+ " 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
3872
+ " \u2192 proposes a gotcha memory anchored to that path.",
3873
+ " 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
3874
+ " \u2192 proposes a convention memory (frequent edits = pattern emerging).",
3875
+ "",
3876
+ "Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
3877
+ "or are surfaced in the next post_task distillation for LLM review.",
3878
+ "",
3879
+ "USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
3880
+ "",
3881
+ "PARAMETERS:",
3882
+ " since_days \u2014 look-back window in days (default 7)",
3883
+ " dry_run \u2014 report matches without saving (default false)",
3884
+ " scope \u2014 'team' (default) | 'personal'",
3885
+ "",
3886
+ "RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
3887
+ ].join("\n"),
3888
+ PatternDetectInputSchema,
3889
+ async (input) => {
3890
+ tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
3891
+ return jsonResult(await patternDetect(input, context));
3892
+ }
3893
+ );
3534
3894
  server.tool(
3535
3895
  "mem_diff",
3536
3896
  [
@@ -3584,9 +3944,13 @@ function createHaiveServer(options = {}) {
3584
3944
  );
3585
3945
  return { server, context, tracker };
3586
3946
  }
3587
-
3588
- // src/index.ts
3589
- function parseArgs(argv) {
3947
+ function parseMcpCliArgs(argv) {
3948
+ for (let i = 2; i < argv.length; i++) {
3949
+ const arg = argv[i];
3950
+ if (arg === "--version" || arg === "-V") {
3951
+ return { versionOnly: true };
3952
+ }
3953
+ }
3590
3954
  const out = {};
3591
3955
  for (let i = 2; i < argv.length; i++) {
3592
3956
  const arg = argv[i];
@@ -3596,18 +3960,26 @@ function parseArgs(argv) {
3596
3960
  out.root = arg.slice("--root=".length);
3597
3961
  }
3598
3962
  }
3599
- return out;
3963
+ return { root: out.root, versionOnly: false };
3600
3964
  }
3601
- async function main() {
3602
- const { root } = parseArgs(process.argv);
3603
- const { server, context } = createHaiveServer({ root });
3965
+ function printHaiveMcpVersion() {
3966
+ console.log(SERVER_VERSION);
3967
+ }
3968
+ async function runHaiveMcpStdio(options) {
3969
+ const { server, context } = createHaiveServer({ root: options.root });
3604
3970
  console.error(
3605
3971
  `[haive-mcp] starting server v${SERVER_VERSION} (project root: ${context.paths.root})`
3606
3972
  );
3607
- const transport = new StdioServerTransport();
3608
- await server.connect(transport);
3973
+ await server.connect(new StdioServerTransport());
3974
+ }
3975
+
3976
+ // src/index.ts
3977
+ var parsed = parseMcpCliArgs(process.argv);
3978
+ if (parsed.versionOnly) {
3979
+ printHaiveMcpVersion();
3980
+ process.exit(0);
3609
3981
  }
3610
- main().catch((err) => {
3982
+ runHaiveMcpStdio({ root: parsed.root }).catch((err) => {
3611
3983
  console.error("[haive-mcp] fatal:", err instanceof Error ? err.message : err);
3612
3984
  process.exit(1);
3613
3985
  });