@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/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/server.ts
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
 
4
5
  // src/context.ts
5
6
  import { findProjectRoot, resolveHaivePaths } from "@hiveai/core";
@@ -137,7 +138,9 @@ var MemSaveInputSchema = {
137
138
  ),
138
139
  slug: z4.string().min(1).describe("Short human-readable identifier \u2014 becomes part of the filename"),
139
140
  body: z4.string().describe("Markdown body of the memory"),
140
- scope: z4.enum(["personal", "team", "module"]).default("personal").describe("Visibility scope: personal | team | module"),
141
+ scope: z4.enum(["personal", "team", "module"]).optional().describe(
142
+ "Visibility scope: personal | team | module. When omitted, falls back to defaultScope in haive.config.json (default: personal)."
143
+ ),
141
144
  module: z4.string().optional().describe("Module name (required when scope=module)"),
142
145
  tags: z4.array(z4.string()).default([]).describe("Tags for filtering"),
143
146
  domain: z4.string().optional().describe("Domain (e.g. transactions, billing)"),
@@ -159,12 +162,14 @@ async function memSave(input, ctx) {
159
162
  );
160
163
  }
161
164
  const existing = existsSync4(ctx.paths.memoriesDir) ? await loadMemoriesFromDir2(ctx.paths.memoriesDir) : [];
165
+ const haiveConfig = await loadConfig(ctx.paths);
166
+ const resolvedScope = input.scope ?? haiveConfig.defaultScope ?? "personal";
162
167
  const invalidPaths = input.paths.filter(
163
168
  (p) => !existsSync4(path3.resolve(ctx.paths.root, p))
164
169
  );
165
170
  const incomingHash = bodyHash(input.body);
166
171
  const hashDuplicate = existing.find(
167
- ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === input.scope
172
+ ({ memory }) => bodyHash(memory.body) === incomingHash && memory.frontmatter.scope === resolvedScope
168
173
  );
169
174
  if (hashDuplicate) {
170
175
  throw new Error(
@@ -173,7 +178,7 @@ async function memSave(input, ctx) {
173
178
  }
174
179
  if (input.topic) {
175
180
  const topicMatch = existing.find(
176
- ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
181
+ ({ memory }) => memory.frontmatter.topic === input.topic && memory.frontmatter.scope === resolvedScope && (!input.module || memory.frontmatter.module === input.module)
177
182
  );
178
183
  if (topicMatch) {
179
184
  const fm = topicMatch.memory.frontmatter;
@@ -203,8 +208,6 @@ async function memSave(input, ctx) {
203
208
  };
204
209
  }
205
210
  }
206
- const haiveConfig = await loadConfig(ctx.paths);
207
- const resolvedScope = input.scope !== "personal" ? input.scope : haiveConfig.defaultScope ?? "personal";
208
211
  const frontmatter = buildFrontmatter({
209
212
  type: input.type,
210
213
  slug: input.slug,
@@ -1035,9 +1038,9 @@ async function memObserve(input, ctx) {
1035
1038
  }
1036
1039
 
1037
1040
  // src/tools/mem-session-end.ts
1038
- import { writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
1039
- import { existsSync as existsSync16 } from "fs";
1040
- import path7 from "path";
1041
+ import { writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
1042
+ import { existsSync as existsSync17 } from "fs";
1043
+ import path8 from "path";
1041
1044
  import {
1042
1045
  buildFrontmatter as buildFrontmatter4,
1043
1046
  loadMemoriesFromDir as loadMemoriesFromDir12,
@@ -1045,6 +1048,134 @@ import {
1045
1048
  serializeMemory as serializeMemory8
1046
1049
  } from "@hiveai/core";
1047
1050
  import { z as z16 } from "zod";
1051
+
1052
+ // src/session-tracker.ts
1053
+ import { appendUsageEvent, loadConfig as loadConfig2 } from "@hiveai/core";
1054
+ import { mkdir as mkdir5, writeFile as writeFile9, rm } from "fs/promises";
1055
+ import { existsSync as existsSync16 } from "fs";
1056
+ import path7 from "path";
1057
+ import { execSync } from "child_process";
1058
+ function pendingDistillPath(ctx) {
1059
+ return path7.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1060
+ }
1061
+ var SessionTracker = class {
1062
+ events = [];
1063
+ startedAt = (/* @__PURE__ */ new Date()).toISOString();
1064
+ config = null;
1065
+ ctx;
1066
+ shutdownRegistered = false;
1067
+ constructor(ctx) {
1068
+ this.ctx = ctx;
1069
+ }
1070
+ async init() {
1071
+ this.config = await loadConfig2(this.ctx.paths);
1072
+ if (this.config.autoSessionEnd) {
1073
+ this.registerShutdownHandler();
1074
+ }
1075
+ }
1076
+ record(tool, summary) {
1077
+ const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
1078
+ this.events.push(event);
1079
+ void appendUsageEvent(this.ctx.paths, event);
1080
+ }
1081
+ registerShutdownHandler() {
1082
+ if (this.shutdownRegistered) return;
1083
+ this.shutdownRegistered = true;
1084
+ const save = async () => {
1085
+ const writingTools = this.events.filter(
1086
+ (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
1087
+ );
1088
+ const totalCalls = this.events.length;
1089
+ if (totalCalls === 0) return;
1090
+ const toolSummary = summarizeTools(this.events);
1091
+ const filesSet = /* @__PURE__ */ new Set();
1092
+ for (const e of this.events) {
1093
+ if (e.summary) {
1094
+ const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
1095
+ for (const m of matches) filesSet.add(m);
1096
+ }
1097
+ }
1098
+ let gitDiff;
1099
+ try {
1100
+ const raw = execSync("git diff HEAD", {
1101
+ cwd: this.ctx.paths.root,
1102
+ timeout: 5e3,
1103
+ encoding: "utf8",
1104
+ stdio: ["ignore", "pipe", "ignore"]
1105
+ });
1106
+ gitDiff = raw.slice(0, 8192) || void 0;
1107
+ } catch {
1108
+ }
1109
+ let recapId;
1110
+ try {
1111
+ const result = await memSessionEnd(
1112
+ {
1113
+ goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
1114
+ accomplished: toolSummary,
1115
+ discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
1116
+ files_touched: [...filesSet].slice(0, 10),
1117
+ next_steps: "",
1118
+ scope: this.config?.defaultScope ?? "personal",
1119
+ module: void 0
1120
+ },
1121
+ this.ctx
1122
+ );
1123
+ recapId = result.id;
1124
+ } catch {
1125
+ }
1126
+ const ranPostTask = this.events.some(
1127
+ (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1128
+ );
1129
+ if (!ranPostTask && existsSync16(this.ctx.paths.haiveDir)) {
1130
+ try {
1131
+ const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
1132
+ const payload = {
1133
+ session_start: this.startedAt,
1134
+ session_end: (/* @__PURE__ */ new Date()).toISOString(),
1135
+ total_tool_calls: totalCalls,
1136
+ tool_summary: toolSummary,
1137
+ memories_saved: memoriesSaved,
1138
+ git_diff_available: !!gitDiff,
1139
+ ...gitDiff ? { git_diff: gitDiff } : {},
1140
+ ...recapId ? { recap_id: recapId } : {}
1141
+ };
1142
+ const cacheDir = path7.join(this.ctx.paths.haiveDir, ".cache");
1143
+ await mkdir5(cacheDir, { recursive: true });
1144
+ await writeFile9(
1145
+ pendingDistillPath(this.ctx),
1146
+ JSON.stringify(payload, null, 2) + "\n",
1147
+ "utf8"
1148
+ );
1149
+ } catch {
1150
+ }
1151
+ }
1152
+ };
1153
+ process.once("SIGTERM", () => {
1154
+ void save().finally(() => process.exit(0));
1155
+ });
1156
+ process.once("SIGINT", () => {
1157
+ void save().finally(() => process.exit(0));
1158
+ });
1159
+ }
1160
+ };
1161
+ async function clearPendingDistill(ctx) {
1162
+ const p = pendingDistillPath(ctx);
1163
+ if (existsSync16(p)) {
1164
+ try {
1165
+ await rm(p);
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ }
1170
+ function summarizeTools(events) {
1171
+ const counts = /* @__PURE__ */ new Map();
1172
+ for (const e of events) {
1173
+ counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
1174
+ }
1175
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
1176
+ }
1177
+
1178
+ // src/tools/mem-session-end.ts
1048
1179
  var MemSessionEndInputSchema = {
1049
1180
  goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1050
1181
  accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
@@ -1084,18 +1215,18 @@ ${input.next_steps}`);
1084
1215
  return lines.join("\n");
1085
1216
  }
1086
1217
  async function memSessionEnd(input, ctx) {
1087
- if (!existsSync16(ctx.paths.haiveDir)) {
1218
+ if (!existsSync17(ctx.paths.haiveDir)) {
1088
1219
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1089
1220
  }
1090
1221
  const body = buildBody(input);
1091
1222
  const topic = recapTopic(input.scope, input.module);
1092
1223
  const invalidPaths = input.files_touched.filter(
1093
- (p) => !existsSync16(path7.resolve(ctx.paths.root, p))
1224
+ (p) => !existsSync17(path8.resolve(ctx.paths.root, p))
1094
1225
  );
1095
1226
  if (invalidPaths.length > 0) {
1096
1227
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
1097
1228
  }
1098
- const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1229
+ const existing = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1099
1230
  const topicMatch = existing.find(
1100
1231
  ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1101
1232
  );
@@ -1110,11 +1241,12 @@ async function memSessionEnd(input, ctx) {
1110
1241
  paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1111
1242
  }
1112
1243
  };
1113
- await writeFile9(
1244
+ await writeFile10(
1114
1245
  topicMatch.filePath,
1115
1246
  serializeMemory8({ frontmatter: newFrontmatter, body }),
1116
1247
  "utf8"
1117
1248
  );
1249
+ await clearPendingDistill(ctx);
1118
1250
  return {
1119
1251
  id: fm.id,
1120
1252
  scope: fm.scope,
@@ -1139,8 +1271,9 @@ async function memSessionEnd(input, ctx) {
1139
1271
  frontmatter.id,
1140
1272
  frontmatter.module
1141
1273
  );
1142
- await mkdir5(path7.dirname(file), { recursive: true });
1143
- await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1274
+ await mkdir6(path8.dirname(file), { recursive: true });
1275
+ await writeFile10(file, serializeMemory8({ frontmatter, body }), "utf8");
1276
+ await clearPendingDistill(ctx);
1144
1277
  return {
1145
1278
  id: frontmatter.id,
1146
1279
  scope: frontmatter.scope,
@@ -1151,24 +1284,27 @@ async function memSessionEnd(input, ctx) {
1151
1284
  }
1152
1285
 
1153
1286
  // src/tools/get-briefing.ts
1154
- import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
1155
- import { existsSync as existsSync17 } from "fs";
1156
- import path8 from "path";
1287
+ import { readFile as readFile3, readdir as readdir3, writeFile as writeFile11 } from "fs/promises";
1288
+ import { existsSync as existsSync18 } from "fs";
1289
+ import path9 from "path";
1157
1290
  import {
1158
1291
  allocateBudget,
1292
+ DEFAULT_AUTO_PROMOTE_RULE,
1159
1293
  deriveConfidence as deriveConfidence4,
1160
1294
  estimateTokens,
1161
1295
  getUsage as getUsage5,
1162
1296
  inferModulesFromPaths as inferModulesFromPaths2,
1297
+ isAutoPromoteEligible,
1163
1298
  isDecaying,
1164
1299
  literalMatchesAllTokens as literalMatchesAllTokens2,
1165
1300
  literalMatchesAnyToken as literalMatchesAnyToken2,
1166
1301
  loadCodeMap,
1167
- loadConfig as loadConfig2,
1302
+ loadConfig as loadConfig3,
1168
1303
  loadMemoriesFromDir as loadMemoriesFromDir13,
1169
1304
  loadUsageIndex as loadUsageIndex7,
1170
1305
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
1171
1306
  queryCodeMap,
1307
+ serializeMemory as serializeMemory9,
1172
1308
  tokenizeQuery as tokenizeQuery2,
1173
1309
  trackReads as trackReads3,
1174
1310
  truncateToTokens
@@ -1207,7 +1343,7 @@ async function getBriefing(input, ctx) {
1207
1343
  let usage = { version: 1, updated_at: "", by_id: {} };
1208
1344
  let byId = /* @__PURE__ */ new Map();
1209
1345
  let lastSession;
1210
- if (existsSync17(ctx.paths.memoriesDir)) {
1346
+ if (existsSync18(ctx.paths.memoriesDir)) {
1211
1347
  const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1212
1348
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1213
1349
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -1325,15 +1461,38 @@ async function getBriefing(input, ctx) {
1325
1461
  memories.push(...ranked.slice(0, input.max_memories));
1326
1462
  if (input.track && memories.length > 0) {
1327
1463
  await trackReads3(ctx.paths, memories.map((m) => m.id));
1464
+ const freshUsage = await loadUsageIndex7(ctx.paths);
1465
+ const cfg = await loadConfig3(ctx.paths);
1466
+ const rule = {
1467
+ minReads: cfg.autoPromoteMinReads ?? DEFAULT_AUTO_PROMOTE_RULE.minReads,
1468
+ maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
1469
+ };
1470
+ for (const m of memories) {
1471
+ const loaded = byId.get(m.id);
1472
+ if (!loaded) continue;
1473
+ const u = getUsage5(freshUsage, m.id);
1474
+ if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
1475
+ const newFm = { ...loaded.memory.frontmatter, status: "validated" };
1476
+ try {
1477
+ await writeFile11(
1478
+ loaded.filePath,
1479
+ serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
1480
+ "utf8"
1481
+ );
1482
+ m.status = "validated";
1483
+ m.confidence = "trusted";
1484
+ } catch {
1485
+ }
1486
+ }
1328
1487
  }
1329
1488
  }
1330
- const projectContextRaw = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1489
+ const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1331
1490
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1332
1491
  const setupWarnings = [];
1333
1492
  let autoContextGenerated = false;
1334
1493
  let projectContext = isTemplateContext ? "" : projectContextRaw;
1335
- if ((isTemplateContext || !existsSync17(ctx.paths.projectContext)) && input.include_project_context) {
1336
- const haiveConfig = await loadConfig2(ctx.paths);
1494
+ if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
1495
+ const haiveConfig = await loadConfig3(ctx.paths);
1337
1496
  if (haiveConfig.autoContext) {
1338
1497
  const codeMap = await loadCodeMap(ctx.paths);
1339
1498
  if (codeMap) {
@@ -1485,7 +1644,7 @@ ${m.content}`).join("\n\n---\n\n"),
1485
1644
  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 ?`
1486
1645
  });
1487
1646
  }
1488
- if (existsSync17(ctx.paths.memoriesDir)) {
1647
+ if (existsSync18(ctx.paths.memoriesDir)) {
1489
1648
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1490
1649
  for (const { memory } of allMems) {
1491
1650
  const fm = memory.frontmatter;
@@ -1503,8 +1662,37 @@ ${m.content}`).join("\n\n---\n\n"),
1503
1662
  });
1504
1663
  }
1505
1664
  }
1665
+ const pendingDistillFile = pendingDistillPath(ctx);
1666
+ if (existsSync18(pendingDistillFile)) {
1667
+ try {
1668
+ const raw = await readFile3(pendingDistillFile, "utf8");
1669
+ const pd = JSON.parse(raw);
1670
+ const ageMs = Date.now() - new Date(pd.session_end).getTime();
1671
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
1672
+ if (ageMs < SEVEN_DAYS) {
1673
+ 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.";
1674
+ const diffNote = pd.git_diff_available ? " A git diff snapshot is available in the pending-distill file for context." : "";
1675
+ actionRequired.push({
1676
+ id: "__pending_distill__",
1677
+ summary: "Previous session has undistilled learnings \u2014 invoke post_task to capture them",
1678
+ 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}
1679
+
1680
+ **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.
1681
+
1682
+ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pending distill marker.`
1683
+ });
1684
+ } else {
1685
+ try {
1686
+ const { rm: rm2 } = await import("fs/promises");
1687
+ await rm2(pendingDistillFile);
1688
+ } catch {
1689
+ }
1690
+ }
1691
+ } catch {
1692
+ }
1693
+ }
1506
1694
  const memoriesEmpty = outputMemories.length === 0;
1507
- const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
1695
+ const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
1508
1696
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
1509
1697
  const hints = [];
1510
1698
  if (isColdStart) {
@@ -1583,15 +1771,15 @@ async function trySemanticHits(ctx, task, limit) {
1583
1771
  }
1584
1772
  async function loadModuleContexts2(ctx, modules) {
1585
1773
  if (modules.length === 0) return [];
1586
- if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1774
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
1587
1775
  const available = new Set(
1588
1776
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1589
1777
  );
1590
1778
  const out = [];
1591
1779
  for (const m of modules) {
1592
1780
  if (!available.has(m)) continue;
1593
- const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1594
- if (existsSync17(file)) {
1781
+ const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
1782
+ if (existsSync18(file)) {
1595
1783
  out.push({ name: m, content: await readFile3(file, "utf8") });
1596
1784
  }
1597
1785
  }
@@ -1680,7 +1868,7 @@ function estimateFileEntryTokens(f) {
1680
1868
  }
1681
1869
 
1682
1870
  // src/tools/mem-diff.ts
1683
- import { existsSync as existsSync18 } from "fs";
1871
+ import { existsSync as existsSync19 } from "fs";
1684
1872
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1685
1873
  import { z as z19 } from "zod";
1686
1874
  var MemDiffInputSchema = {
@@ -1688,7 +1876,7 @@ var MemDiffInputSchema = {
1688
1876
  id_b: z19.string().min(1).describe("Second memory id")
1689
1877
  };
1690
1878
  async function memDiff(input, ctx) {
1691
- if (!existsSync18(ctx.paths.memoriesDir)) {
1879
+ if (!existsSync19(ctx.paths.memoriesDir)) {
1692
1880
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1693
1881
  }
1694
1882
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -1725,7 +1913,7 @@ async function memDiff(input, ctx) {
1725
1913
  }
1726
1914
 
1727
1915
  // src/tools/get-recap.ts
1728
- import { existsSync as existsSync19 } from "fs";
1916
+ import { existsSync as existsSync20 } from "fs";
1729
1917
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
1730
1918
  import { z as z20 } from "zod";
1731
1919
  var GetRecapInputSchema = {
@@ -1734,7 +1922,7 @@ var GetRecapInputSchema = {
1734
1922
  )
1735
1923
  };
1736
1924
  async function getRecap(input, ctx) {
1737
- if (!existsSync19(ctx.paths.memoriesDir)) {
1925
+ if (!existsSync20(ctx.paths.memoriesDir)) {
1738
1926
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
1739
1927
  }
1740
1928
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -1832,9 +2020,9 @@ async function codeSearch(input, ctx) {
1832
2020
  }
1833
2021
 
1834
2022
  // src/tools/why-this-file.ts
1835
- import { existsSync as existsSync20 } from "fs";
2023
+ import { existsSync as existsSync21 } from "fs";
1836
2024
  import { spawn } from "child_process";
1837
- import path9 from "path";
2025
+ import path10 from "path";
1838
2026
  import {
1839
2027
  deriveConfidence as deriveConfidence5,
1840
2028
  getUsage as getUsage6,
@@ -1852,7 +2040,7 @@ var WhyThisFileInputSchema = {
1852
2040
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
1853
2041
  };
1854
2042
  async function whyThisFile(input, ctx) {
1855
- const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
2043
+ const fileExists = existsSync21(path10.join(ctx.paths.root, input.path));
1856
2044
  const [commits, memories, codeMap] = await Promise.all([
1857
2045
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
1858
2046
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -1893,7 +2081,7 @@ async function whyThisFile(input, ctx) {
1893
2081
  };
1894
2082
  }
1895
2083
  async function collectAnchoredMemories(ctx, filePath, limit) {
1896
- if (!existsSync20(ctx.paths.memoriesDir)) return [];
2084
+ if (!existsSync21(ctx.paths.memoriesDir)) return [];
1897
2085
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
1898
2086
  const usage = await loadUsageIndex8(ctx.paths);
1899
2087
  const out = [];
@@ -1948,7 +2136,7 @@ function runCommand(cmd, args, cwd) {
1948
2136
  }
1949
2137
 
1950
2138
  // src/tools/anti-patterns-check.ts
1951
- import { existsSync as existsSync21 } from "fs";
2139
+ import { existsSync as existsSync22 } from "fs";
1952
2140
  import {
1953
2141
  deriveConfidence as deriveConfidence6,
1954
2142
  getUsage as getUsage7,
@@ -1979,7 +2167,7 @@ async function antiPatternsCheck(input, ctx) {
1979
2167
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
1980
2168
  };
1981
2169
  }
1982
- if (!existsSync21(ctx.paths.memoriesDir)) {
2170
+ if (!existsSync22(ctx.paths.memoriesDir)) {
1983
2171
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
1984
2172
  }
1985
2173
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -2061,7 +2249,7 @@ async function antiPatternsCheck(input, ctx) {
2061
2249
  }
2062
2250
 
2063
2251
  // src/tools/mem-distill.ts
2064
- import { existsSync as existsSync22 } from "fs";
2252
+ import { existsSync as existsSync23 } from "fs";
2065
2253
  import {
2066
2254
  loadMemoriesFromDir as loadMemoriesFromDir18,
2067
2255
  tokenizeQuery as tokenizeQuery4
@@ -2113,7 +2301,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2113
2301
  "error"
2114
2302
  ]);
2115
2303
  async function memDistill(input, ctx) {
2116
- if (!existsSync22(ctx.paths.memoriesDir)) {
2304
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2117
2305
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2118
2306
  }
2119
2307
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -2221,7 +2409,7 @@ function firstHeading(body) {
2221
2409
  }
2222
2410
 
2223
2411
  // src/tools/why-this-decision.ts
2224
- import { existsSync as existsSync23 } from "fs";
2412
+ import { existsSync as existsSync24 } from "fs";
2225
2413
  import { spawn as spawn2 } from "child_process";
2226
2414
  import {
2227
2415
  deriveConfidence as deriveConfidence7,
@@ -2236,7 +2424,7 @@ var WhyThisDecisionInputSchema = {
2236
2424
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2237
2425
  };
2238
2426
  async function whyThisDecision(input, ctx) {
2239
- if (!existsSync23(ctx.paths.memoriesDir)) {
2427
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2240
2428
  return {
2241
2429
  found: false,
2242
2430
  related: [],
@@ -2368,7 +2556,7 @@ function runCommand2(cmd, args, cwd) {
2368
2556
  }
2369
2557
 
2370
2558
  // src/tools/mem-conflicts.ts
2371
- import { existsSync as existsSync24 } from "fs";
2559
+ import { existsSync as existsSync25 } from "fs";
2372
2560
  import {
2373
2561
  deriveConfidence as deriveConfidence8,
2374
2562
  getUsage as getUsage9,
@@ -2386,7 +2574,7 @@ var MemConflictsInputSchema = {
2386
2574
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2387
2575
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2388
2576
  async function memConflicts(input, ctx) {
2389
- if (!existsSync24(ctx.paths.memoriesDir)) {
2577
+ if (!existsSync25(ctx.paths.memoriesDir)) {
2390
2578
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2391
2579
  }
2392
2580
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -2567,13 +2755,226 @@ async function preCommitCheck(input, ctx) {
2567
2755
  };
2568
2756
  }
2569
2757
 
2570
- // src/prompts/bootstrap-project.ts
2758
+ // src/tools/pattern-detect.ts
2759
+ import { mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2760
+ import { existsSync as existsSync26 } from "fs";
2761
+ import path11 from "path";
2762
+ import { execSync as execSync2 } from "child_process";
2763
+ import {
2764
+ buildFrontmatter as buildFrontmatter5,
2765
+ memoryFilePath as memoryFilePath5,
2766
+ readUsageEvents,
2767
+ serializeMemory as serializeMemory10
2768
+ } from "@hiveai/core";
2571
2769
  import { z as z29 } from "zod";
2770
+ var CONFIG_PATTERNS = [
2771
+ ".eslintrc",
2772
+ "eslint.config",
2773
+ "prettier.config",
2774
+ ".prettierrc",
2775
+ "tsconfig",
2776
+ "jsconfig",
2777
+ "vitest.config",
2778
+ "jest.config",
2779
+ ".env.example",
2780
+ ".env.defaults",
2781
+ "tailwind.config",
2782
+ "vite.config",
2783
+ "next.config",
2784
+ "babel.config",
2785
+ "postcss.config",
2786
+ "renovate.json",
2787
+ "dependabot.yml"
2788
+ ];
2789
+ var MAX_DIFF_BYTES = 4096;
2790
+ var HOT_FILE_MIN = 3;
2791
+ var PatternDetectInputSchema = {
2792
+ since_days: z29.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
2793
+ dry_run: z29.boolean().default(false).describe("When true, report matches without writing any memory files."),
2794
+ scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
2795
+ };
2796
+ async function patternDetect(input, ctx) {
2797
+ if (!existsSync26(ctx.paths.haiveDir)) {
2798
+ return {
2799
+ scanned_events: 0,
2800
+ matches: [],
2801
+ saved: 0,
2802
+ saved_ids: [],
2803
+ notice: "No .ai/ directory found. Run 'haive init' first."
2804
+ };
2805
+ }
2806
+ const matches = [];
2807
+ try {
2808
+ const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
2809
+ const configFiles = changedFiles.filter(
2810
+ (f) => CONFIG_PATTERNS.some((p) => path11.basename(f.toLowerCase()).includes(p))
2811
+ );
2812
+ for (const file of configFiles.slice(0, 5)) {
2813
+ const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
2814
+ if (!diff) continue;
2815
+ const parentDir = path11.basename(path11.dirname(file));
2816
+ const baseName = path11.basename(file).replace(/\.[^.]+$/, "");
2817
+ const slug = `${parentDir}-${baseName}`.replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
2818
+ matches.push({
2819
+ kind: "config_change",
2820
+ signal: `Config file modified: ${file}`,
2821
+ proposed_type: "convention",
2822
+ proposed_slug: `config-change-${slug}`,
2823
+ proposed_body: [
2824
+ `# Config change: \`${file}\``,
2825
+ "",
2826
+ "This configuration file was recently modified. The diff below captures the intent.",
2827
+ "Review and update this memory with the **reason** for the change if known.",
2828
+ "",
2829
+ "```diff",
2830
+ diff.slice(0, MAX_DIFF_BYTES),
2831
+ "```"
2832
+ ].join("\n"),
2833
+ anchor_paths: [file]
2834
+ });
2835
+ }
2836
+ } catch {
2837
+ }
2838
+ const events = await readUsageEvents(ctx.paths);
2839
+ const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
2840
+ const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
2841
+ const pathCounts = /* @__PURE__ */ new Map();
2842
+ for (const e of recent) {
2843
+ if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
2844
+ if (!e.summary) continue;
2845
+ const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
2846
+ for (const t of tokens) {
2847
+ const key = t.toLowerCase();
2848
+ const existing = pathCounts.get(key);
2849
+ if (existing) {
2850
+ existing.count++;
2851
+ existing.tools.add(e.tool);
2852
+ } else {
2853
+ pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
2854
+ }
2855
+ }
2856
+ }
2857
+ for (const [p, { count, tools }] of pathCounts) {
2858
+ if (count < HOT_FILE_MIN) continue;
2859
+ const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
2860
+ if (!isGotchaSignal) continue;
2861
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2862
+ matches.push({
2863
+ kind: "repeated_path",
2864
+ signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
2865
+ proposed_type: "gotcha",
2866
+ proposed_slug: `repeated-issue-${slug}`,
2867
+ proposed_body: [
2868
+ `# Recurring issue near \`${p}\``,
2869
+ "",
2870
+ `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.`,
2871
+ "",
2872
+ `**Source signals:** ${[...tools].join(", ")} (${count} events)`
2873
+ ].join("\n"),
2874
+ anchor_paths: [p]
2875
+ });
2876
+ }
2877
+ for (const [p, { count, tools }] of pathCounts) {
2878
+ if (count < HOT_FILE_MIN) continue;
2879
+ if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
2880
+ if (CONFIG_PATTERNS.some((cp) => path11.basename(p).includes(cp))) continue;
2881
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2882
+ matches.push({
2883
+ kind: "hot_file",
2884
+ signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
2885
+ proposed_type: "convention",
2886
+ proposed_slug: `hot-file-${slug}`,
2887
+ proposed_body: [
2888
+ `# Frequent edits to \`${p}\``,
2889
+ "",
2890
+ `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.`,
2891
+ "",
2892
+ "**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
2893
+ ].join("\n"),
2894
+ anchor_paths: [p]
2895
+ });
2896
+ }
2897
+ if (matches.length === 0) {
2898
+ return {
2899
+ scanned_events: recent.length,
2900
+ matches: [],
2901
+ saved: 0,
2902
+ saved_ids: [],
2903
+ notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
2904
+ };
2905
+ }
2906
+ if (input.dry_run) {
2907
+ return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
2908
+ }
2909
+ const savedIds = [];
2910
+ for (const match of matches) {
2911
+ try {
2912
+ const fm = buildFrontmatter5({
2913
+ type: match.proposed_type,
2914
+ slug: match.proposed_slug,
2915
+ scope: input.scope,
2916
+ tags: ["pattern-detect", match.kind],
2917
+ paths: match.anchor_paths,
2918
+ status: "proposed"
2919
+ });
2920
+ const file = memoryFilePath5(
2921
+ ctx.paths,
2922
+ fm.scope === "shared" ? "team" : fm.scope,
2923
+ fm.id,
2924
+ void 0
2925
+ );
2926
+ if (existsSync26(file)) continue;
2927
+ await mkdir7(path11.dirname(file), { recursive: true });
2928
+ await writeFile12(
2929
+ file,
2930
+ serializeMemory10({ frontmatter: fm, body: match.proposed_body }),
2931
+ "utf8"
2932
+ );
2933
+ savedIds.push(fm.id);
2934
+ } catch {
2935
+ }
2936
+ }
2937
+ return {
2938
+ scanned_events: recent.length,
2939
+ matches,
2940
+ saved: savedIds.length,
2941
+ saved_ids: savedIds
2942
+ };
2943
+ }
2944
+ function gitChangedFiles(root, sinceDays) {
2945
+ try {
2946
+ const out = execSync2(
2947
+ `git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
2948
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2949
+ );
2950
+ return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
2951
+ } catch {
2952
+ return [];
2953
+ }
2954
+ }
2955
+ function gitFileDiff(root, file, sinceDays) {
2956
+ try {
2957
+ const out = execSync2(
2958
+ `git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
2959
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2960
+ );
2961
+ if (!out.trim()) return null;
2962
+ const diffLines = out.split("\n").filter(
2963
+ (l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
2964
+ );
2965
+ return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
2966
+ } catch {
2967
+ return null;
2968
+ }
2969
+ }
2970
+
2971
+ // src/prompts/bootstrap-project.ts
2972
+ import { z as z30 } from "zod";
2572
2973
  var BootstrapProjectArgsSchema = {
2573
- module: z29.string().optional().describe(
2974
+ module: z30.string().optional().describe(
2574
2975
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
2575
2976
  ),
2576
- focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2977
+ focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2577
2978
  };
2578
2979
  var ROOT_TEMPLATE = `# Project context
2579
2980
 
@@ -2655,10 +3056,10 @@ ${template}\`\`\`
2655
3056
  }
2656
3057
 
2657
3058
  // src/prompts/post-task.ts
2658
- import { z as z30 } from "zod";
3059
+ import { z as z31 } from "zod";
2659
3060
  var PostTaskArgsSchema = {
2660
- task_summary: z30.string().optional().describe("One sentence describing what you just did"),
2661
- files_touched: z30.array(z30.string()).optional().describe("Files you created or modified during the task")
3061
+ task_summary: z31.string().optional().describe("One sentence describing what you just did"),
3062
+ files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
2662
3063
  };
2663
3064
  function postTaskPrompt(args, ctx) {
2664
3065
  const taskLine = args.task_summary ? `
@@ -2726,6 +3127,8 @@ Call **\`mem_session_end\`** with:
2726
3127
 
2727
3128
  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.
2728
3129
 
3130
+ 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.
3131
+
2729
3132
  When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
2730
3133
  `;
2731
3134
  return {
@@ -2740,12 +3143,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
2740
3143
  }
2741
3144
 
2742
3145
  // src/prompts/import-docs.ts
2743
- import { z as z31 } from "zod";
3146
+ import { z as z32 } from "zod";
2744
3147
  var ImportDocsArgsSchema = {
2745
- content: z31.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
2746
- source: z31.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
2747
- scope: z31.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
2748
- dry_run: z31.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
3148
+ content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3149
+ source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3150
+ scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3151
+ dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2749
3152
  };
2750
3153
  function importDocsPrompt(args, ctx) {
2751
3154
  const sourceLine = args.source ? `
@@ -2808,80 +3211,9 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
2808
3211
  };
2809
3212
  }
2810
3213
 
2811
- // src/session-tracker.ts
2812
- import { appendUsageEvent, loadConfig as loadConfig3 } from "@hiveai/core";
2813
- var SessionTracker = class {
2814
- events = [];
2815
- startedAt = (/* @__PURE__ */ new Date()).toISOString();
2816
- config = null;
2817
- ctx;
2818
- shutdownRegistered = false;
2819
- constructor(ctx) {
2820
- this.ctx = ctx;
2821
- }
2822
- async init() {
2823
- this.config = await loadConfig3(this.ctx.paths);
2824
- if (this.config.autoSessionEnd) {
2825
- this.registerShutdownHandler();
2826
- }
2827
- }
2828
- record(tool, summary) {
2829
- const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
2830
- this.events.push(event);
2831
- void appendUsageEvent(this.ctx.paths, event);
2832
- }
2833
- registerShutdownHandler() {
2834
- if (this.shutdownRegistered) return;
2835
- this.shutdownRegistered = true;
2836
- const save = async () => {
2837
- const writingTools = this.events.filter(
2838
- (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
2839
- );
2840
- const totalCalls = this.events.length;
2841
- if (totalCalls === 0) return;
2842
- const toolSummary = summarizeTools(this.events);
2843
- const filesSet = /* @__PURE__ */ new Set();
2844
- for (const e of this.events) {
2845
- if (e.summary) {
2846
- const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
2847
- for (const m of matches) filesSet.add(m);
2848
- }
2849
- }
2850
- try {
2851
- await memSessionEnd(
2852
- {
2853
- goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
2854
- accomplished: toolSummary,
2855
- discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
2856
- files_touched: [...filesSet].slice(0, 10),
2857
- next_steps: "",
2858
- scope: this.config?.defaultScope ?? "personal",
2859
- module: void 0
2860
- },
2861
- this.ctx
2862
- );
2863
- } catch {
2864
- }
2865
- };
2866
- process.once("SIGTERM", () => {
2867
- void save().finally(() => process.exit(0));
2868
- });
2869
- process.once("SIGINT", () => {
2870
- void save().finally(() => process.exit(0));
2871
- });
2872
- }
2873
- };
2874
- function summarizeTools(events) {
2875
- const counts = /* @__PURE__ */ new Map();
2876
- for (const e of events) {
2877
- counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
2878
- }
2879
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
2880
- }
2881
-
2882
3214
  // src/server.ts
2883
3215
  var SERVER_NAME = "haive";
2884
- var SERVER_VERSION = "0.8.0";
3216
+ var SERVER_VERSION = "0.9.2";
2885
3217
  function jsonResult(data) {
2886
3218
  return {
2887
3219
  content: [
@@ -3526,6 +3858,37 @@ function createHaiveServer(options = {}) {
3526
3858
  return jsonResult(await preCommitCheck(input, context));
3527
3859
  }
3528
3860
  );
3861
+ server.tool(
3862
+ "pattern_detect",
3863
+ [
3864
+ "Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
3865
+ "",
3866
+ "Runs three signals over local git history and the tool-usage log:",
3867
+ " 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
3868
+ " \u2192 proposes a convention memory with the git diff as body.",
3869
+ " 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
3870
+ " \u2192 proposes a gotcha memory anchored to that path.",
3871
+ " 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
3872
+ " \u2192 proposes a convention memory (frequent edits = pattern emerging).",
3873
+ "",
3874
+ "Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
3875
+ "or are surfaced in the next post_task distillation for LLM review.",
3876
+ "",
3877
+ "USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
3878
+ "",
3879
+ "PARAMETERS:",
3880
+ " since_days \u2014 look-back window in days (default 7)",
3881
+ " dry_run \u2014 report matches without saving (default false)",
3882
+ " scope \u2014 'team' (default) | 'personal'",
3883
+ "",
3884
+ "RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
3885
+ ].join("\n"),
3886
+ PatternDetectInputSchema,
3887
+ async (input) => {
3888
+ tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
3889
+ return jsonResult(await patternDetect(input, context));
3890
+ }
3891
+ );
3529
3892
  server.tool(
3530
3893
  "mem_diff",
3531
3894
  [
@@ -3579,6 +3942,34 @@ function createHaiveServer(options = {}) {
3579
3942
  );
3580
3943
  return { server, context, tracker };
3581
3944
  }
3945
+ function parseMcpCliArgs(argv) {
3946
+ for (let i = 2; i < argv.length; i++) {
3947
+ const arg = argv[i];
3948
+ if (arg === "--version" || arg === "-V") {
3949
+ return { versionOnly: true };
3950
+ }
3951
+ }
3952
+ const out = {};
3953
+ for (let i = 2; i < argv.length; i++) {
3954
+ const arg = argv[i];
3955
+ if (arg === "--root" || arg === "-r") {
3956
+ out.root = argv[++i];
3957
+ } else if (arg?.startsWith("--root=")) {
3958
+ out.root = arg.slice("--root=".length);
3959
+ }
3960
+ }
3961
+ return { root: out.root, versionOnly: false };
3962
+ }
3963
+ function printHaiveMcpVersion() {
3964
+ console.log(SERVER_VERSION);
3965
+ }
3966
+ async function runHaiveMcpStdio(options) {
3967
+ const { server, context } = createHaiveServer({ root: options.root });
3968
+ console.error(
3969
+ `[haive-mcp] starting server v${SERVER_VERSION} (project root: ${context.paths.root})`
3970
+ );
3971
+ await server.connect(new StdioServerTransport());
3972
+ }
3582
3973
  export {
3583
3974
  SERVER_NAME,
3584
3975
  SERVER_VERSION,
@@ -3591,7 +3982,11 @@ export {
3591
3982
  memConflicts,
3592
3983
  memDistill,
3593
3984
  memRelevantTo,
3985
+ parseMcpCliArgs,
3986
+ patternDetect,
3594
3987
  preCommitCheck,
3988
+ printHaiveMcpVersion,
3989
+ runHaiveMcpStdio,
3595
3990
  whyThisDecision,
3596
3991
  whyThisFile
3597
3992
  };