@hiveai/mcp 0.8.0 → 0.9.0

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
@@ -1035,9 +1035,9 @@ async function memObserve(input, ctx) {
1035
1035
  }
1036
1036
 
1037
1037
  // 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";
1038
+ import { writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
1039
+ import { existsSync as existsSync17 } from "fs";
1040
+ import path8 from "path";
1041
1041
  import {
1042
1042
  buildFrontmatter as buildFrontmatter4,
1043
1043
  loadMemoriesFromDir as loadMemoriesFromDir12,
@@ -1045,6 +1045,134 @@ import {
1045
1045
  serializeMemory as serializeMemory8
1046
1046
  } from "@hiveai/core";
1047
1047
  import { z as z16 } from "zod";
1048
+
1049
+ // src/session-tracker.ts
1050
+ import { appendUsageEvent, loadConfig as loadConfig2 } from "@hiveai/core";
1051
+ import { mkdir as mkdir5, writeFile as writeFile9, rm } from "fs/promises";
1052
+ import { existsSync as existsSync16 } from "fs";
1053
+ import path7 from "path";
1054
+ import { execSync } from "child_process";
1055
+ function pendingDistillPath(ctx) {
1056
+ return path7.join(ctx.paths.haiveDir, ".cache", "pending-distill.json");
1057
+ }
1058
+ var SessionTracker = class {
1059
+ events = [];
1060
+ startedAt = (/* @__PURE__ */ new Date()).toISOString();
1061
+ config = null;
1062
+ ctx;
1063
+ shutdownRegistered = false;
1064
+ constructor(ctx) {
1065
+ this.ctx = ctx;
1066
+ }
1067
+ async init() {
1068
+ this.config = await loadConfig2(this.ctx.paths);
1069
+ if (this.config.autoSessionEnd) {
1070
+ this.registerShutdownHandler();
1071
+ }
1072
+ }
1073
+ record(tool, summary) {
1074
+ const event = { tool, at: (/* @__PURE__ */ new Date()).toISOString(), summary };
1075
+ this.events.push(event);
1076
+ void appendUsageEvent(this.ctx.paths, event);
1077
+ }
1078
+ registerShutdownHandler() {
1079
+ if (this.shutdownRegistered) return;
1080
+ this.shutdownRegistered = true;
1081
+ const save = async () => {
1082
+ const writingTools = this.events.filter(
1083
+ (e) => ["mem_save", "mem_tried", "mem_observe", "mem_update", "bootstrap_project_save"].includes(e.tool)
1084
+ );
1085
+ const totalCalls = this.events.length;
1086
+ if (totalCalls === 0) return;
1087
+ const toolSummary = summarizeTools(this.events);
1088
+ const filesSet = /* @__PURE__ */ new Set();
1089
+ for (const e of this.events) {
1090
+ if (e.summary) {
1091
+ const matches = e.summary.match(/[^\s"',]+\.[a-zA-Z]{1,6}/g) ?? [];
1092
+ for (const m of matches) filesSet.add(m);
1093
+ }
1094
+ }
1095
+ let gitDiff;
1096
+ try {
1097
+ const raw = execSync("git diff HEAD", {
1098
+ cwd: this.ctx.paths.root,
1099
+ timeout: 5e3,
1100
+ encoding: "utf8",
1101
+ stdio: ["ignore", "pipe", "ignore"]
1102
+ });
1103
+ gitDiff = raw.slice(0, 8192) || void 0;
1104
+ } catch {
1105
+ }
1106
+ let recapId;
1107
+ try {
1108
+ const result = await memSessionEnd(
1109
+ {
1110
+ goal: `Auto-captured session (${totalCalls} tool call${totalCalls === 1 ? "" : "s"})`,
1111
+ accomplished: toolSummary,
1112
+ discoveries: writingTools.length > 0 ? `${writingTools.length} memor${writingTools.length === 1 ? "y" : "ies"} saved during this session.` : "No new memories saved this session.",
1113
+ files_touched: [...filesSet].slice(0, 10),
1114
+ next_steps: "",
1115
+ scope: this.config?.defaultScope ?? "personal",
1116
+ module: void 0
1117
+ },
1118
+ this.ctx
1119
+ );
1120
+ recapId = result.id;
1121
+ } catch {
1122
+ }
1123
+ const ranPostTask = this.events.some(
1124
+ (e) => e.tool === "mem_session_end" && !e.summary?.startsWith("Auto-captured")
1125
+ );
1126
+ if (!ranPostTask && existsSync16(this.ctx.paths.haiveDir)) {
1127
+ try {
1128
+ const memoriesSaved = writingTools.map((e) => e.summary ?? "").filter(Boolean).slice(0, 20);
1129
+ const payload = {
1130
+ session_start: this.startedAt,
1131
+ session_end: (/* @__PURE__ */ new Date()).toISOString(),
1132
+ total_tool_calls: totalCalls,
1133
+ tool_summary: toolSummary,
1134
+ memories_saved: memoriesSaved,
1135
+ git_diff_available: !!gitDiff,
1136
+ ...gitDiff ? { git_diff: gitDiff } : {},
1137
+ ...recapId ? { recap_id: recapId } : {}
1138
+ };
1139
+ const cacheDir = path7.join(this.ctx.paths.haiveDir, ".cache");
1140
+ await mkdir5(cacheDir, { recursive: true });
1141
+ await writeFile9(
1142
+ pendingDistillPath(this.ctx),
1143
+ JSON.stringify(payload, null, 2) + "\n",
1144
+ "utf8"
1145
+ );
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ };
1150
+ process.once("SIGTERM", () => {
1151
+ void save().finally(() => process.exit(0));
1152
+ });
1153
+ process.once("SIGINT", () => {
1154
+ void save().finally(() => process.exit(0));
1155
+ });
1156
+ }
1157
+ };
1158
+ async function clearPendingDistill(ctx) {
1159
+ const p = pendingDistillPath(ctx);
1160
+ if (existsSync16(p)) {
1161
+ try {
1162
+ await rm(p);
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ }
1167
+ function summarizeTools(events) {
1168
+ const counts = /* @__PURE__ */ new Map();
1169
+ for (const e of events) {
1170
+ counts.set(e.tool, (counts.get(e.tool) ?? 0) + 1);
1171
+ }
1172
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} \xD7${n}`).join(", ");
1173
+ }
1174
+
1175
+ // src/tools/mem-session-end.ts
1048
1176
  var MemSessionEndInputSchema = {
1049
1177
  goal: z16.string().min(1).describe("What you were trying to accomplish this session (1\u20132 sentences)"),
1050
1178
  accomplished: z16.string().describe("What was actually done \u2014 bullet list recommended"),
@@ -1084,18 +1212,18 @@ ${input.next_steps}`);
1084
1212
  return lines.join("\n");
1085
1213
  }
1086
1214
  async function memSessionEnd(input, ctx) {
1087
- if (!existsSync16(ctx.paths.haiveDir)) {
1215
+ if (!existsSync17(ctx.paths.haiveDir)) {
1088
1216
  throw new Error(`No .ai/ directory at ${ctx.paths.root}. Run 'haive init' first.`);
1089
1217
  }
1090
1218
  const body = buildBody(input);
1091
1219
  const topic = recapTopic(input.scope, input.module);
1092
1220
  const invalidPaths = input.files_touched.filter(
1093
- (p) => !existsSync16(path7.resolve(ctx.paths.root, p))
1221
+ (p) => !existsSync17(path8.resolve(ctx.paths.root, p))
1094
1222
  );
1095
1223
  if (invalidPaths.length > 0) {
1096
1224
  console.warn(`[haive] session end: anchor path(s) not found: ${invalidPaths.join(", ")}`);
1097
1225
  }
1098
- const existing = existsSync16(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1226
+ const existing = existsSync17(ctx.paths.memoriesDir) ? await loadMemoriesFromDir12(ctx.paths.memoriesDir) : [];
1099
1227
  const topicMatch = existing.find(
1100
1228
  ({ memory }) => memory.frontmatter.topic === topic && memory.frontmatter.scope === input.scope && (!input.module || memory.frontmatter.module === input.module)
1101
1229
  );
@@ -1110,11 +1238,12 @@ async function memSessionEnd(input, ctx) {
1110
1238
  paths: input.files_touched.length ? input.files_touched : fm.anchor.paths
1111
1239
  }
1112
1240
  };
1113
- await writeFile9(
1241
+ await writeFile10(
1114
1242
  topicMatch.filePath,
1115
1243
  serializeMemory8({ frontmatter: newFrontmatter, body }),
1116
1244
  "utf8"
1117
1245
  );
1246
+ await clearPendingDistill(ctx);
1118
1247
  return {
1119
1248
  id: fm.id,
1120
1249
  scope: fm.scope,
@@ -1139,8 +1268,9 @@ async function memSessionEnd(input, ctx) {
1139
1268
  frontmatter.id,
1140
1269
  frontmatter.module
1141
1270
  );
1142
- await mkdir5(path7.dirname(file), { recursive: true });
1143
- await writeFile9(file, serializeMemory8({ frontmatter, body }), "utf8");
1271
+ await mkdir6(path8.dirname(file), { recursive: true });
1272
+ await writeFile10(file, serializeMemory8({ frontmatter, body }), "utf8");
1273
+ await clearPendingDistill(ctx);
1144
1274
  return {
1145
1275
  id: frontmatter.id,
1146
1276
  scope: frontmatter.scope,
@@ -1151,24 +1281,27 @@ async function memSessionEnd(input, ctx) {
1151
1281
  }
1152
1282
 
1153
1283
  // 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";
1284
+ import { readFile as readFile3, readdir as readdir3, writeFile as writeFile11 } from "fs/promises";
1285
+ import { existsSync as existsSync18 } from "fs";
1286
+ import path9 from "path";
1157
1287
  import {
1158
1288
  allocateBudget,
1289
+ DEFAULT_AUTO_PROMOTE_RULE,
1159
1290
  deriveConfidence as deriveConfidence4,
1160
1291
  estimateTokens,
1161
1292
  getUsage as getUsage5,
1162
1293
  inferModulesFromPaths as inferModulesFromPaths2,
1294
+ isAutoPromoteEligible,
1163
1295
  isDecaying,
1164
1296
  literalMatchesAllTokens as literalMatchesAllTokens2,
1165
1297
  literalMatchesAnyToken as literalMatchesAnyToken2,
1166
1298
  loadCodeMap,
1167
- loadConfig as loadConfig2,
1299
+ loadConfig as loadConfig3,
1168
1300
  loadMemoriesFromDir as loadMemoriesFromDir13,
1169
1301
  loadUsageIndex as loadUsageIndex7,
1170
1302
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
1171
1303
  queryCodeMap,
1304
+ serializeMemory as serializeMemory9,
1172
1305
  tokenizeQuery as tokenizeQuery2,
1173
1306
  trackReads as trackReads3,
1174
1307
  truncateToTokens
@@ -1207,7 +1340,7 @@ async function getBriefing(input, ctx) {
1207
1340
  let usage = { version: 1, updated_at: "", by_id: {} };
1208
1341
  let byId = /* @__PURE__ */ new Map();
1209
1342
  let lastSession;
1210
- if (existsSync17(ctx.paths.memoriesDir)) {
1343
+ if (existsSync18(ctx.paths.memoriesDir)) {
1211
1344
  const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1212
1345
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1213
1346
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -1325,15 +1458,37 @@ async function getBriefing(input, ctx) {
1325
1458
  memories.push(...ranked.slice(0, input.max_memories));
1326
1459
  if (input.track && memories.length > 0) {
1327
1460
  await trackReads3(ctx.paths, memories.map((m) => m.id));
1461
+ const freshUsage = await loadUsageIndex7(ctx.paths);
1462
+ const rule = {
1463
+ minReads: DEFAULT_AUTO_PROMOTE_RULE.minReads,
1464
+ maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
1465
+ };
1466
+ for (const m of memories) {
1467
+ const loaded = byId.get(m.id);
1468
+ if (!loaded) continue;
1469
+ const u = getUsage5(freshUsage, m.id);
1470
+ if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
1471
+ const newFm = { ...loaded.memory.frontmatter, status: "validated" };
1472
+ try {
1473
+ await writeFile11(
1474
+ loaded.filePath,
1475
+ serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
1476
+ "utf8"
1477
+ );
1478
+ m.status = "validated";
1479
+ m.confidence = "trusted";
1480
+ } catch {
1481
+ }
1482
+ }
1328
1483
  }
1329
1484
  }
1330
- const projectContextRaw = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1485
+ const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1331
1486
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1332
1487
  const setupWarnings = [];
1333
1488
  let autoContextGenerated = false;
1334
1489
  let projectContext = isTemplateContext ? "" : projectContextRaw;
1335
- if ((isTemplateContext || !existsSync17(ctx.paths.projectContext)) && input.include_project_context) {
1336
- const haiveConfig = await loadConfig2(ctx.paths);
1490
+ if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
1491
+ const haiveConfig = await loadConfig3(ctx.paths);
1337
1492
  if (haiveConfig.autoContext) {
1338
1493
  const codeMap = await loadCodeMap(ctx.paths);
1339
1494
  if (codeMap) {
@@ -1485,7 +1640,7 @@ ${m.content}`).join("\n\n---\n\n"),
1485
1640
  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
1641
  });
1487
1642
  }
1488
- if (existsSync17(ctx.paths.memoriesDir)) {
1643
+ if (existsSync18(ctx.paths.memoriesDir)) {
1489
1644
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1490
1645
  for (const { memory } of allMems) {
1491
1646
  const fm = memory.frontmatter;
@@ -1503,8 +1658,37 @@ ${m.content}`).join("\n\n---\n\n"),
1503
1658
  });
1504
1659
  }
1505
1660
  }
1661
+ const pendingDistillFile = pendingDistillPath(ctx);
1662
+ if (existsSync18(pendingDistillFile)) {
1663
+ try {
1664
+ const raw = await readFile3(pendingDistillFile, "utf8");
1665
+ const pd = JSON.parse(raw);
1666
+ const ageMs = Date.now() - new Date(pd.session_end).getTime();
1667
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
1668
+ if (ageMs < SEVEN_DAYS) {
1669
+ 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.";
1670
+ const diffNote = pd.git_diff_available ? " A git diff snapshot is available in the pending-distill file for context." : "";
1671
+ actionRequired.push({
1672
+ id: "__pending_distill__",
1673
+ summary: "Previous session has undistilled learnings \u2014 invoke post_task to capture them",
1674
+ 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}
1675
+
1676
+ **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.
1677
+
1678
+ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pending distill marker.`
1679
+ });
1680
+ } else {
1681
+ try {
1682
+ const { rm: rm2 } = await import("fs/promises");
1683
+ await rm2(pendingDistillFile);
1684
+ } catch {
1685
+ }
1686
+ }
1687
+ } catch {
1688
+ }
1689
+ }
1506
1690
  const memoriesEmpty = outputMemories.length === 0;
1507
- const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
1691
+ const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
1508
1692
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
1509
1693
  const hints = [];
1510
1694
  if (isColdStart) {
@@ -1583,15 +1767,15 @@ async function trySemanticHits(ctx, task, limit) {
1583
1767
  }
1584
1768
  async function loadModuleContexts2(ctx, modules) {
1585
1769
  if (modules.length === 0) return [];
1586
- if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1770
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
1587
1771
  const available = new Set(
1588
1772
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1589
1773
  );
1590
1774
  const out = [];
1591
1775
  for (const m of modules) {
1592
1776
  if (!available.has(m)) continue;
1593
- const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1594
- if (existsSync17(file)) {
1777
+ const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
1778
+ if (existsSync18(file)) {
1595
1779
  out.push({ name: m, content: await readFile3(file, "utf8") });
1596
1780
  }
1597
1781
  }
@@ -1680,7 +1864,7 @@ function estimateFileEntryTokens(f) {
1680
1864
  }
1681
1865
 
1682
1866
  // src/tools/mem-diff.ts
1683
- import { existsSync as existsSync18 } from "fs";
1867
+ import { existsSync as existsSync19 } from "fs";
1684
1868
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1685
1869
  import { z as z19 } from "zod";
1686
1870
  var MemDiffInputSchema = {
@@ -1688,7 +1872,7 @@ var MemDiffInputSchema = {
1688
1872
  id_b: z19.string().min(1).describe("Second memory id")
1689
1873
  };
1690
1874
  async function memDiff(input, ctx) {
1691
- if (!existsSync18(ctx.paths.memoriesDir)) {
1875
+ if (!existsSync19(ctx.paths.memoriesDir)) {
1692
1876
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1693
1877
  }
1694
1878
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -1725,7 +1909,7 @@ async function memDiff(input, ctx) {
1725
1909
  }
1726
1910
 
1727
1911
  // src/tools/get-recap.ts
1728
- import { existsSync as existsSync19 } from "fs";
1912
+ import { existsSync as existsSync20 } from "fs";
1729
1913
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
1730
1914
  import { z as z20 } from "zod";
1731
1915
  var GetRecapInputSchema = {
@@ -1734,7 +1918,7 @@ var GetRecapInputSchema = {
1734
1918
  )
1735
1919
  };
1736
1920
  async function getRecap(input, ctx) {
1737
- if (!existsSync19(ctx.paths.memoriesDir)) {
1921
+ if (!existsSync20(ctx.paths.memoriesDir)) {
1738
1922
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
1739
1923
  }
1740
1924
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -1832,9 +2016,9 @@ async function codeSearch(input, ctx) {
1832
2016
  }
1833
2017
 
1834
2018
  // src/tools/why-this-file.ts
1835
- import { existsSync as existsSync20 } from "fs";
2019
+ import { existsSync as existsSync21 } from "fs";
1836
2020
  import { spawn } from "child_process";
1837
- import path9 from "path";
2021
+ import path10 from "path";
1838
2022
  import {
1839
2023
  deriveConfidence as deriveConfidence5,
1840
2024
  getUsage as getUsage6,
@@ -1852,7 +2036,7 @@ var WhyThisFileInputSchema = {
1852
2036
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
1853
2037
  };
1854
2038
  async function whyThisFile(input, ctx) {
1855
- const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
2039
+ const fileExists = existsSync21(path10.join(ctx.paths.root, input.path));
1856
2040
  const [commits, memories, codeMap] = await Promise.all([
1857
2041
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
1858
2042
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -1893,7 +2077,7 @@ async function whyThisFile(input, ctx) {
1893
2077
  };
1894
2078
  }
1895
2079
  async function collectAnchoredMemories(ctx, filePath, limit) {
1896
- if (!existsSync20(ctx.paths.memoriesDir)) return [];
2080
+ if (!existsSync21(ctx.paths.memoriesDir)) return [];
1897
2081
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
1898
2082
  const usage = await loadUsageIndex8(ctx.paths);
1899
2083
  const out = [];
@@ -1948,7 +2132,7 @@ function runCommand(cmd, args, cwd) {
1948
2132
  }
1949
2133
 
1950
2134
  // src/tools/anti-patterns-check.ts
1951
- import { existsSync as existsSync21 } from "fs";
2135
+ import { existsSync as existsSync22 } from "fs";
1952
2136
  import {
1953
2137
  deriveConfidence as deriveConfidence6,
1954
2138
  getUsage as getUsage7,
@@ -1979,7 +2163,7 @@ async function antiPatternsCheck(input, ctx) {
1979
2163
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
1980
2164
  };
1981
2165
  }
1982
- if (!existsSync21(ctx.paths.memoriesDir)) {
2166
+ if (!existsSync22(ctx.paths.memoriesDir)) {
1983
2167
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
1984
2168
  }
1985
2169
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -2061,7 +2245,7 @@ async function antiPatternsCheck(input, ctx) {
2061
2245
  }
2062
2246
 
2063
2247
  // src/tools/mem-distill.ts
2064
- import { existsSync as existsSync22 } from "fs";
2248
+ import { existsSync as existsSync23 } from "fs";
2065
2249
  import {
2066
2250
  loadMemoriesFromDir as loadMemoriesFromDir18,
2067
2251
  tokenizeQuery as tokenizeQuery4
@@ -2113,7 +2297,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2113
2297
  "error"
2114
2298
  ]);
2115
2299
  async function memDistill(input, ctx) {
2116
- if (!existsSync22(ctx.paths.memoriesDir)) {
2300
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2117
2301
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2118
2302
  }
2119
2303
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -2221,7 +2405,7 @@ function firstHeading(body) {
2221
2405
  }
2222
2406
 
2223
2407
  // src/tools/why-this-decision.ts
2224
- import { existsSync as existsSync23 } from "fs";
2408
+ import { existsSync as existsSync24 } from "fs";
2225
2409
  import { spawn as spawn2 } from "child_process";
2226
2410
  import {
2227
2411
  deriveConfidence as deriveConfidence7,
@@ -2236,7 +2420,7 @@ var WhyThisDecisionInputSchema = {
2236
2420
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2237
2421
  };
2238
2422
  async function whyThisDecision(input, ctx) {
2239
- if (!existsSync23(ctx.paths.memoriesDir)) {
2423
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2240
2424
  return {
2241
2425
  found: false,
2242
2426
  related: [],
@@ -2368,7 +2552,7 @@ function runCommand2(cmd, args, cwd) {
2368
2552
  }
2369
2553
 
2370
2554
  // src/tools/mem-conflicts.ts
2371
- import { existsSync as existsSync24 } from "fs";
2555
+ import { existsSync as existsSync25 } from "fs";
2372
2556
  import {
2373
2557
  deriveConfidence as deriveConfidence8,
2374
2558
  getUsage as getUsage9,
@@ -2386,7 +2570,7 @@ var MemConflictsInputSchema = {
2386
2570
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2387
2571
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2388
2572
  async function memConflicts(input, ctx) {
2389
- if (!existsSync24(ctx.paths.memoriesDir)) {
2573
+ if (!existsSync25(ctx.paths.memoriesDir)) {
2390
2574
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2391
2575
  }
2392
2576
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -2567,13 +2751,224 @@ async function preCommitCheck(input, ctx) {
2567
2751
  };
2568
2752
  }
2569
2753
 
2570
- // src/prompts/bootstrap-project.ts
2754
+ // src/tools/pattern-detect.ts
2755
+ import { mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2756
+ import { existsSync as existsSync26 } from "fs";
2757
+ import path11 from "path";
2758
+ import { execSync as execSync2 } from "child_process";
2759
+ import {
2760
+ buildFrontmatter as buildFrontmatter5,
2761
+ memoryFilePath as memoryFilePath5,
2762
+ readUsageEvents,
2763
+ serializeMemory as serializeMemory10
2764
+ } from "@hiveai/core";
2571
2765
  import { z as z29 } from "zod";
2766
+ var CONFIG_PATTERNS = [
2767
+ ".eslintrc",
2768
+ "eslint.config",
2769
+ "prettier.config",
2770
+ ".prettierrc",
2771
+ "tsconfig",
2772
+ "jsconfig",
2773
+ "vitest.config",
2774
+ "jest.config",
2775
+ ".env.example",
2776
+ ".env.defaults",
2777
+ "tailwind.config",
2778
+ "vite.config",
2779
+ "next.config",
2780
+ "babel.config",
2781
+ "postcss.config",
2782
+ "renovate.json",
2783
+ "dependabot.yml"
2784
+ ];
2785
+ var MAX_DIFF_BYTES = 4096;
2786
+ var HOT_FILE_MIN = 3;
2787
+ var PatternDetectInputSchema = {
2788
+ since_days: z29.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
2789
+ dry_run: z29.boolean().default(false).describe("When true, report matches without writing any memory files."),
2790
+ scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
2791
+ };
2792
+ async function patternDetect(input, ctx) {
2793
+ if (!existsSync26(ctx.paths.haiveDir)) {
2794
+ return {
2795
+ scanned_events: 0,
2796
+ matches: [],
2797
+ saved: 0,
2798
+ saved_ids: [],
2799
+ notice: "No .ai/ directory found. Run 'haive init' first."
2800
+ };
2801
+ }
2802
+ const matches = [];
2803
+ try {
2804
+ const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
2805
+ const configFiles = changedFiles.filter(
2806
+ (f) => CONFIG_PATTERNS.some((p) => path11.basename(f.toLowerCase()).includes(p))
2807
+ );
2808
+ for (const file of configFiles.slice(0, 5)) {
2809
+ const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
2810
+ if (!diff) continue;
2811
+ const slug = path11.basename(file).replace(/\.[^.]+$/, "").replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
2812
+ matches.push({
2813
+ kind: "config_change",
2814
+ signal: `Config file modified: ${file}`,
2815
+ proposed_type: "convention",
2816
+ proposed_slug: `config-change-${slug}`,
2817
+ proposed_body: [
2818
+ `# Config change: \`${file}\``,
2819
+ "",
2820
+ "This configuration file was recently modified. The diff below captures the intent.",
2821
+ "Review and update this memory with the **reason** for the change if known.",
2822
+ "",
2823
+ "```diff",
2824
+ diff.slice(0, MAX_DIFF_BYTES),
2825
+ "```"
2826
+ ].join("\n"),
2827
+ anchor_paths: [file]
2828
+ });
2829
+ }
2830
+ } catch {
2831
+ }
2832
+ const events = await readUsageEvents(ctx.paths);
2833
+ const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
2834
+ const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
2835
+ const pathCounts = /* @__PURE__ */ new Map();
2836
+ for (const e of recent) {
2837
+ if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
2838
+ if (!e.summary) continue;
2839
+ const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
2840
+ for (const t of tokens) {
2841
+ const key = t.toLowerCase();
2842
+ const existing = pathCounts.get(key);
2843
+ if (existing) {
2844
+ existing.count++;
2845
+ existing.tools.add(e.tool);
2846
+ } else {
2847
+ pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
2848
+ }
2849
+ }
2850
+ }
2851
+ for (const [p, { count, tools }] of pathCounts) {
2852
+ if (count < HOT_FILE_MIN) continue;
2853
+ const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
2854
+ if (!isGotchaSignal) continue;
2855
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2856
+ matches.push({
2857
+ kind: "repeated_path",
2858
+ signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
2859
+ proposed_type: "gotcha",
2860
+ proposed_slug: `repeated-issue-${slug}`,
2861
+ proposed_body: [
2862
+ `# Recurring issue near \`${p}\``,
2863
+ "",
2864
+ `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.`,
2865
+ "",
2866
+ `**Source signals:** ${[...tools].join(", ")} (${count} events)`
2867
+ ].join("\n"),
2868
+ anchor_paths: [p]
2869
+ });
2870
+ }
2871
+ for (const [p, { count, tools }] of pathCounts) {
2872
+ if (count < HOT_FILE_MIN) continue;
2873
+ if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
2874
+ if (CONFIG_PATTERNS.some((cp) => path11.basename(p).includes(cp))) continue;
2875
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2876
+ matches.push({
2877
+ kind: "hot_file",
2878
+ signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
2879
+ proposed_type: "convention",
2880
+ proposed_slug: `hot-file-${slug}`,
2881
+ proposed_body: [
2882
+ `# Frequent edits to \`${p}\``,
2883
+ "",
2884
+ `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.`,
2885
+ "",
2886
+ "**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
2887
+ ].join("\n"),
2888
+ anchor_paths: [p]
2889
+ });
2890
+ }
2891
+ if (matches.length === 0) {
2892
+ return {
2893
+ scanned_events: recent.length,
2894
+ matches: [],
2895
+ saved: 0,
2896
+ saved_ids: [],
2897
+ notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
2898
+ };
2899
+ }
2900
+ if (input.dry_run) {
2901
+ return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
2902
+ }
2903
+ const savedIds = [];
2904
+ for (const match of matches) {
2905
+ try {
2906
+ const fm = buildFrontmatter5({
2907
+ type: match.proposed_type,
2908
+ slug: match.proposed_slug,
2909
+ scope: input.scope,
2910
+ tags: ["pattern-detect", match.kind],
2911
+ paths: match.anchor_paths,
2912
+ status: "proposed"
2913
+ });
2914
+ const file = memoryFilePath5(
2915
+ ctx.paths,
2916
+ fm.scope === "shared" ? "team" : fm.scope,
2917
+ fm.id,
2918
+ void 0
2919
+ );
2920
+ if (existsSync26(file)) continue;
2921
+ await mkdir7(path11.dirname(file), { recursive: true });
2922
+ await writeFile12(
2923
+ file,
2924
+ serializeMemory10({ frontmatter: fm, body: match.proposed_body }),
2925
+ "utf8"
2926
+ );
2927
+ savedIds.push(fm.id);
2928
+ } catch {
2929
+ }
2930
+ }
2931
+ return {
2932
+ scanned_events: recent.length,
2933
+ matches,
2934
+ saved: savedIds.length,
2935
+ saved_ids: savedIds
2936
+ };
2937
+ }
2938
+ function gitChangedFiles(root, sinceDays) {
2939
+ try {
2940
+ const out = execSync2(
2941
+ `git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
2942
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2943
+ );
2944
+ return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
2945
+ } catch {
2946
+ return [];
2947
+ }
2948
+ }
2949
+ function gitFileDiff(root, file, sinceDays) {
2950
+ try {
2951
+ const out = execSync2(
2952
+ `git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
2953
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2954
+ );
2955
+ if (!out.trim()) return null;
2956
+ const diffLines = out.split("\n").filter(
2957
+ (l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
2958
+ );
2959
+ return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
2960
+ } catch {
2961
+ return null;
2962
+ }
2963
+ }
2964
+
2965
+ // src/prompts/bootstrap-project.ts
2966
+ import { z as z30 } from "zod";
2572
2967
  var BootstrapProjectArgsSchema = {
2573
- module: z29.string().optional().describe(
2968
+ module: z30.string().optional().describe(
2574
2969
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
2575
2970
  ),
2576
- focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2971
+ focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2577
2972
  };
2578
2973
  var ROOT_TEMPLATE = `# Project context
2579
2974
 
@@ -2655,10 +3050,10 @@ ${template}\`\`\`
2655
3050
  }
2656
3051
 
2657
3052
  // src/prompts/post-task.ts
2658
- import { z as z30 } from "zod";
3053
+ import { z as z31 } from "zod";
2659
3054
  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")
3055
+ task_summary: z31.string().optional().describe("One sentence describing what you just did"),
3056
+ files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
2662
3057
  };
2663
3058
  function postTaskPrompt(args, ctx) {
2664
3059
  const taskLine = args.task_summary ? `
@@ -2726,6 +3121,8 @@ Call **\`mem_session_end\`** with:
2726
3121
 
2727
3122
  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
3123
 
3124
+ 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.
3125
+
2729
3126
  When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
2730
3127
  `;
2731
3128
  return {
@@ -2740,12 +3137,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
2740
3137
  }
2741
3138
 
2742
3139
  // src/prompts/import-docs.ts
2743
- import { z as z31 } from "zod";
3140
+ import { z as z32 } from "zod";
2744
3141
  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")
3142
+ content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3143
+ source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3144
+ scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3145
+ dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2749
3146
  };
2750
3147
  function importDocsPrompt(args, ctx) {
2751
3148
  const sourceLine = args.source ? `
@@ -2808,80 +3205,9 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
2808
3205
  };
2809
3206
  }
2810
3207
 
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
3208
  // src/server.ts
2883
3209
  var SERVER_NAME = "haive";
2884
- var SERVER_VERSION = "0.8.0";
3210
+ var SERVER_VERSION = "0.9.0";
2885
3211
  function jsonResult(data) {
2886
3212
  return {
2887
3213
  content: [
@@ -3526,6 +3852,37 @@ function createHaiveServer(options = {}) {
3526
3852
  return jsonResult(await preCommitCheck(input, context));
3527
3853
  }
3528
3854
  );
3855
+ server.tool(
3856
+ "pattern_detect",
3857
+ [
3858
+ "Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
3859
+ "",
3860
+ "Runs three signals over local git history and the tool-usage log:",
3861
+ " 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
3862
+ " \u2192 proposes a convention memory with the git diff as body.",
3863
+ " 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
3864
+ " \u2192 proposes a gotcha memory anchored to that path.",
3865
+ " 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
3866
+ " \u2192 proposes a convention memory (frequent edits = pattern emerging).",
3867
+ "",
3868
+ "Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
3869
+ "or are surfaced in the next post_task distillation for LLM review.",
3870
+ "",
3871
+ "USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
3872
+ "",
3873
+ "PARAMETERS:",
3874
+ " since_days \u2014 look-back window in days (default 7)",
3875
+ " dry_run \u2014 report matches without saving (default false)",
3876
+ " scope \u2014 'team' (default) | 'personal'",
3877
+ "",
3878
+ "RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
3879
+ ].join("\n"),
3880
+ PatternDetectInputSchema,
3881
+ async (input) => {
3882
+ tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
3883
+ return jsonResult(await patternDetect(input, context));
3884
+ }
3885
+ );
3529
3886
  server.tool(
3530
3887
  "mem_diff",
3531
3888
  [
@@ -3591,6 +3948,7 @@ export {
3591
3948
  memConflicts,
3592
3949
  memDistill,
3593
3950
  memRelevantTo,
3951
+ patternDetect,
3594
3952
  preCommitCheck,
3595
3953
  whyThisDecision,
3596
3954
  whyThisFile