@hiveai/mcp 0.7.2 → 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/index.js CHANGED
@@ -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,37 @@ 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 rule = {
1468
+ minReads: DEFAULT_AUTO_PROMOTE_RULE.minReads,
1469
+ maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
1470
+ };
1471
+ for (const m of memories) {
1472
+ const loaded = byId.get(m.id);
1473
+ if (!loaded) continue;
1474
+ const u = getUsage5(freshUsage, m.id);
1475
+ if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
1476
+ const newFm = { ...loaded.memory.frontmatter, status: "validated" };
1477
+ try {
1478
+ await writeFile11(
1479
+ loaded.filePath,
1480
+ serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
1481
+ "utf8"
1482
+ );
1483
+ m.status = "validated";
1484
+ m.confidence = "trusted";
1485
+ } catch {
1486
+ }
1487
+ }
1333
1488
  }
1334
1489
  }
1335
- const projectContextRaw = input.include_project_context && existsSync17(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1490
+ const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1336
1491
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1337
1492
  const setupWarnings = [];
1338
1493
  let autoContextGenerated = false;
1339
1494
  let projectContext = isTemplateContext ? "" : projectContextRaw;
1340
- if ((isTemplateContext || !existsSync17(ctx.paths.projectContext)) && input.include_project_context) {
1341
- const haiveConfig = await loadConfig2(ctx.paths);
1495
+ if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
1496
+ const haiveConfig = await loadConfig3(ctx.paths);
1342
1497
  if (haiveConfig.autoContext) {
1343
1498
  const codeMap = await loadCodeMap(ctx.paths);
1344
1499
  if (codeMap) {
@@ -1490,7 +1645,7 @@ ${m.content}`).join("\n\n---\n\n"),
1490
1645
  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
1646
  });
1492
1647
  }
1493
- if (existsSync17(ctx.paths.memoriesDir)) {
1648
+ if (existsSync18(ctx.paths.memoriesDir)) {
1494
1649
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1495
1650
  for (const { memory } of allMems) {
1496
1651
  const fm = memory.frontmatter;
@@ -1508,8 +1663,37 @@ ${m.content}`).join("\n\n---\n\n"),
1508
1663
  });
1509
1664
  }
1510
1665
  }
1666
+ const pendingDistillFile = pendingDistillPath(ctx);
1667
+ if (existsSync18(pendingDistillFile)) {
1668
+ try {
1669
+ const raw = await readFile3(pendingDistillFile, "utf8");
1670
+ const pd = JSON.parse(raw);
1671
+ const ageMs = Date.now() - new Date(pd.session_end).getTime();
1672
+ const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
1673
+ if (ageMs < SEVEN_DAYS) {
1674
+ 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.";
1675
+ const diffNote = pd.git_diff_available ? " A git diff snapshot is available in the pending-distill file for context." : "";
1676
+ actionRequired.push({
1677
+ id: "__pending_distill__",
1678
+ summary: "Previous session has undistilled learnings \u2014 invoke post_task to capture them",
1679
+ 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}
1680
+
1681
+ **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.
1682
+
1683
+ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pending distill marker.`
1684
+ });
1685
+ } else {
1686
+ try {
1687
+ const { rm: rm2 } = await import("fs/promises");
1688
+ await rm2(pendingDistillFile);
1689
+ } catch {
1690
+ }
1691
+ }
1692
+ } catch {
1693
+ }
1694
+ }
1511
1695
  const memoriesEmpty = outputMemories.length === 0;
1512
- const hasMemoriesDir = existsSync17(ctx.paths.memoriesDir);
1696
+ const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
1513
1697
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
1514
1698
  const hints = [];
1515
1699
  if (isColdStart) {
@@ -1588,15 +1772,15 @@ async function trySemanticHits(ctx, task, limit) {
1588
1772
  }
1589
1773
  async function loadModuleContexts2(ctx, modules) {
1590
1774
  if (modules.length === 0) return [];
1591
- if (!existsSync17(ctx.paths.modulesContextDir)) return [];
1775
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
1592
1776
  const available = new Set(
1593
1777
  (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1594
1778
  );
1595
1779
  const out = [];
1596
1780
  for (const m of modules) {
1597
1781
  if (!available.has(m)) continue;
1598
- const file = path8.join(ctx.paths.modulesContextDir, m, "context.md");
1599
- if (existsSync17(file)) {
1782
+ const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
1783
+ if (existsSync18(file)) {
1600
1784
  out.push({ name: m, content: await readFile3(file, "utf8") });
1601
1785
  }
1602
1786
  }
@@ -1685,7 +1869,7 @@ function estimateFileEntryTokens(f) {
1685
1869
  }
1686
1870
 
1687
1871
  // src/tools/mem-diff.ts
1688
- import { existsSync as existsSync18 } from "fs";
1872
+ import { existsSync as existsSync19 } from "fs";
1689
1873
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
1690
1874
  import { z as z19 } from "zod";
1691
1875
  var MemDiffInputSchema = {
@@ -1693,7 +1877,7 @@ var MemDiffInputSchema = {
1693
1877
  id_b: z19.string().min(1).describe("Second memory id")
1694
1878
  };
1695
1879
  async function memDiff(input, ctx) {
1696
- if (!existsSync18(ctx.paths.memoriesDir)) {
1880
+ if (!existsSync19(ctx.paths.memoriesDir)) {
1697
1881
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
1698
1882
  }
1699
1883
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -1730,7 +1914,7 @@ async function memDiff(input, ctx) {
1730
1914
  }
1731
1915
 
1732
1916
  // src/tools/get-recap.ts
1733
- import { existsSync as existsSync19 } from "fs";
1917
+ import { existsSync as existsSync20 } from "fs";
1734
1918
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
1735
1919
  import { z as z20 } from "zod";
1736
1920
  var GetRecapInputSchema = {
@@ -1739,7 +1923,7 @@ var GetRecapInputSchema = {
1739
1923
  )
1740
1924
  };
1741
1925
  async function getRecap(input, ctx) {
1742
- if (!existsSync19(ctx.paths.memoriesDir)) {
1926
+ if (!existsSync20(ctx.paths.memoriesDir)) {
1743
1927
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
1744
1928
  }
1745
1929
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -1837,9 +2021,9 @@ async function codeSearch(input, ctx) {
1837
2021
  }
1838
2022
 
1839
2023
  // src/tools/why-this-file.ts
1840
- import { existsSync as existsSync20 } from "fs";
2024
+ import { existsSync as existsSync21 } from "fs";
1841
2025
  import { spawn } from "child_process";
1842
- import path9 from "path";
2026
+ import path10 from "path";
1843
2027
  import {
1844
2028
  deriveConfidence as deriveConfidence5,
1845
2029
  getUsage as getUsage6,
@@ -1857,7 +2041,7 @@ var WhyThisFileInputSchema = {
1857
2041
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
1858
2042
  };
1859
2043
  async function whyThisFile(input, ctx) {
1860
- const fileExists = existsSync20(path9.join(ctx.paths.root, input.path));
2044
+ const fileExists = existsSync21(path10.join(ctx.paths.root, input.path));
1861
2045
  const [commits, memories, codeMap] = await Promise.all([
1862
2046
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
1863
2047
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -1898,7 +2082,7 @@ async function whyThisFile(input, ctx) {
1898
2082
  };
1899
2083
  }
1900
2084
  async function collectAnchoredMemories(ctx, filePath, limit) {
1901
- if (!existsSync20(ctx.paths.memoriesDir)) return [];
2085
+ if (!existsSync21(ctx.paths.memoriesDir)) return [];
1902
2086
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
1903
2087
  const usage = await loadUsageIndex8(ctx.paths);
1904
2088
  const out = [];
@@ -1953,7 +2137,7 @@ function runCommand(cmd, args, cwd) {
1953
2137
  }
1954
2138
 
1955
2139
  // src/tools/anti-patterns-check.ts
1956
- import { existsSync as existsSync21 } from "fs";
2140
+ import { existsSync as existsSync22 } from "fs";
1957
2141
  import {
1958
2142
  deriveConfidence as deriveConfidence6,
1959
2143
  getUsage as getUsage7,
@@ -1984,7 +2168,7 @@ async function antiPatternsCheck(input, ctx) {
1984
2168
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
1985
2169
  };
1986
2170
  }
1987
- if (!existsSync21(ctx.paths.memoriesDir)) {
2171
+ if (!existsSync22(ctx.paths.memoriesDir)) {
1988
2172
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
1989
2173
  }
1990
2174
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -2066,7 +2250,7 @@ async function antiPatternsCheck(input, ctx) {
2066
2250
  }
2067
2251
 
2068
2252
  // src/tools/mem-distill.ts
2069
- import { existsSync as existsSync22 } from "fs";
2253
+ import { existsSync as existsSync23 } from "fs";
2070
2254
  import {
2071
2255
  loadMemoriesFromDir as loadMemoriesFromDir18,
2072
2256
  tokenizeQuery as tokenizeQuery4
@@ -2118,7 +2302,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2118
2302
  "error"
2119
2303
  ]);
2120
2304
  async function memDistill(input, ctx) {
2121
- if (!existsSync22(ctx.paths.memoriesDir)) {
2305
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2122
2306
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2123
2307
  }
2124
2308
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -2226,7 +2410,7 @@ function firstHeading(body) {
2226
2410
  }
2227
2411
 
2228
2412
  // src/tools/why-this-decision.ts
2229
- import { existsSync as existsSync23 } from "fs";
2413
+ import { existsSync as existsSync24 } from "fs";
2230
2414
  import { spawn as spawn2 } from "child_process";
2231
2415
  import {
2232
2416
  deriveConfidence as deriveConfidence7,
@@ -2241,7 +2425,7 @@ var WhyThisDecisionInputSchema = {
2241
2425
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2242
2426
  };
2243
2427
  async function whyThisDecision(input, ctx) {
2244
- if (!existsSync23(ctx.paths.memoriesDir)) {
2428
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2245
2429
  return {
2246
2430
  found: false,
2247
2431
  related: [],
@@ -2373,7 +2557,7 @@ function runCommand2(cmd, args, cwd) {
2373
2557
  }
2374
2558
 
2375
2559
  // src/tools/mem-conflicts.ts
2376
- import { existsSync as existsSync24 } from "fs";
2560
+ import { existsSync as existsSync25 } from "fs";
2377
2561
  import {
2378
2562
  deriveConfidence as deriveConfidence8,
2379
2563
  getUsage as getUsage9,
@@ -2391,7 +2575,7 @@ var MemConflictsInputSchema = {
2391
2575
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2392
2576
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2393
2577
  async function memConflicts(input, ctx) {
2394
- if (!existsSync24(ctx.paths.memoriesDir)) {
2578
+ if (!existsSync25(ctx.paths.memoriesDir)) {
2395
2579
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2396
2580
  }
2397
2581
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -2572,13 +2756,224 @@ async function preCommitCheck(input, ctx) {
2572
2756
  };
2573
2757
  }
2574
2758
 
2575
- // src/prompts/bootstrap-project.ts
2759
+ // src/tools/pattern-detect.ts
2760
+ import { mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2761
+ import { existsSync as existsSync26 } from "fs";
2762
+ import path11 from "path";
2763
+ import { execSync as execSync2 } from "child_process";
2764
+ import {
2765
+ buildFrontmatter as buildFrontmatter5,
2766
+ memoryFilePath as memoryFilePath5,
2767
+ readUsageEvents,
2768
+ serializeMemory as serializeMemory10
2769
+ } from "@hiveai/core";
2576
2770
  import { z as z29 } from "zod";
2771
+ var CONFIG_PATTERNS = [
2772
+ ".eslintrc",
2773
+ "eslint.config",
2774
+ "prettier.config",
2775
+ ".prettierrc",
2776
+ "tsconfig",
2777
+ "jsconfig",
2778
+ "vitest.config",
2779
+ "jest.config",
2780
+ ".env.example",
2781
+ ".env.defaults",
2782
+ "tailwind.config",
2783
+ "vite.config",
2784
+ "next.config",
2785
+ "babel.config",
2786
+ "postcss.config",
2787
+ "renovate.json",
2788
+ "dependabot.yml"
2789
+ ];
2790
+ var MAX_DIFF_BYTES = 4096;
2791
+ var HOT_FILE_MIN = 3;
2792
+ var PatternDetectInputSchema = {
2793
+ since_days: z29.number().int().min(1).default(7).describe("Look-back window in days for both git history and usage log."),
2794
+ dry_run: z29.boolean().default(false).describe("When true, report matches without writing any memory files."),
2795
+ scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
2796
+ };
2797
+ async function patternDetect(input, ctx) {
2798
+ if (!existsSync26(ctx.paths.haiveDir)) {
2799
+ return {
2800
+ scanned_events: 0,
2801
+ matches: [],
2802
+ saved: 0,
2803
+ saved_ids: [],
2804
+ notice: "No .ai/ directory found. Run 'haive init' first."
2805
+ };
2806
+ }
2807
+ const matches = [];
2808
+ try {
2809
+ const changedFiles = gitChangedFiles(ctx.paths.root, input.since_days);
2810
+ const configFiles = changedFiles.filter(
2811
+ (f) => CONFIG_PATTERNS.some((p) => path11.basename(f.toLowerCase()).includes(p))
2812
+ );
2813
+ for (const file of configFiles.slice(0, 5)) {
2814
+ const diff = gitFileDiff(ctx.paths.root, file, input.since_days);
2815
+ if (!diff) continue;
2816
+ const slug = path11.basename(file).replace(/\.[^.]+$/, "").replace(/[^a-z0-9]/gi, "-").toLowerCase().slice(0, 40);
2817
+ matches.push({
2818
+ kind: "config_change",
2819
+ signal: `Config file modified: ${file}`,
2820
+ proposed_type: "convention",
2821
+ proposed_slug: `config-change-${slug}`,
2822
+ proposed_body: [
2823
+ `# Config change: \`${file}\``,
2824
+ "",
2825
+ "This configuration file was recently modified. The diff below captures the intent.",
2826
+ "Review and update this memory with the **reason** for the change if known.",
2827
+ "",
2828
+ "```diff",
2829
+ diff.slice(0, MAX_DIFF_BYTES),
2830
+ "```"
2831
+ ].join("\n"),
2832
+ anchor_paths: [file]
2833
+ });
2834
+ }
2835
+ } catch {
2836
+ }
2837
+ const events = await readUsageEvents(ctx.paths);
2838
+ const cutoff = Date.now() - input.since_days * 24 * 60 * 60 * 1e3;
2839
+ const recent = events.filter((e) => Date.parse(e.at) >= cutoff);
2840
+ const pathCounts = /* @__PURE__ */ new Map();
2841
+ for (const e of recent) {
2842
+ if (!["mem_tried", "mem_observe", "mem_save"].includes(e.tool)) continue;
2843
+ if (!e.summary) continue;
2844
+ const tokens = e.summary.match(/[^\s"'`,;()[\]{}]+\.[a-zA-Z]{1,6}/g) ?? [];
2845
+ for (const t of tokens) {
2846
+ const key = t.toLowerCase();
2847
+ const existing = pathCounts.get(key);
2848
+ if (existing) {
2849
+ existing.count++;
2850
+ existing.tools.add(e.tool);
2851
+ } else {
2852
+ pathCounts.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]) });
2853
+ }
2854
+ }
2855
+ }
2856
+ for (const [p, { count, tools }] of pathCounts) {
2857
+ if (count < HOT_FILE_MIN) continue;
2858
+ const isGotchaSignal = tools.has("mem_tried") || tools.has("mem_observe");
2859
+ if (!isGotchaSignal) continue;
2860
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2861
+ matches.push({
2862
+ kind: "repeated_path",
2863
+ signal: `Path '${p}' appears ${count}\xD7 in mem_tried/mem_observe events`,
2864
+ proposed_type: "gotcha",
2865
+ proposed_slug: `repeated-issue-${slug}`,
2866
+ proposed_body: [
2867
+ `# Recurring issue near \`${p}\``,
2868
+ "",
2869
+ `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.`,
2870
+ "",
2871
+ `**Source signals:** ${[...tools].join(", ")} (${count} events)`
2872
+ ].join("\n"),
2873
+ anchor_paths: [p]
2874
+ });
2875
+ }
2876
+ for (const [p, { count, tools }] of pathCounts) {
2877
+ if (count < HOT_FILE_MIN) continue;
2878
+ if (tools.has("mem_tried") || tools.has("mem_observe")) continue;
2879
+ if (CONFIG_PATTERNS.some((cp) => path11.basename(p).includes(cp))) continue;
2880
+ const slug = p.replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").slice(0, 40);
2881
+ matches.push({
2882
+ kind: "hot_file",
2883
+ signal: `Path '${p}' referenced ${count}\xD7 across mem_save events`,
2884
+ proposed_type: "convention",
2885
+ proposed_slug: `hot-file-${slug}`,
2886
+ proposed_body: [
2887
+ `# Frequent edits to \`${p}\``,
2888
+ "",
2889
+ `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.`,
2890
+ "",
2891
+ "**Suggested action:** review recent memories anchored to this path and extract the common pattern as a named convention."
2892
+ ].join("\n"),
2893
+ anchor_paths: [p]
2894
+ });
2895
+ }
2896
+ if (matches.length === 0) {
2897
+ return {
2898
+ scanned_events: recent.length,
2899
+ matches: [],
2900
+ saved: 0,
2901
+ saved_ids: [],
2902
+ notice: `No patterns detected in the last ${input.since_days} days (${recent.length} events scanned).`
2903
+ };
2904
+ }
2905
+ if (input.dry_run) {
2906
+ return { scanned_events: recent.length, matches, saved: 0, saved_ids: [] };
2907
+ }
2908
+ const savedIds = [];
2909
+ for (const match of matches) {
2910
+ try {
2911
+ const fm = buildFrontmatter5({
2912
+ type: match.proposed_type,
2913
+ slug: match.proposed_slug,
2914
+ scope: input.scope,
2915
+ tags: ["pattern-detect", match.kind],
2916
+ paths: match.anchor_paths,
2917
+ status: "proposed"
2918
+ });
2919
+ const file = memoryFilePath5(
2920
+ ctx.paths,
2921
+ fm.scope === "shared" ? "team" : fm.scope,
2922
+ fm.id,
2923
+ void 0
2924
+ );
2925
+ if (existsSync26(file)) continue;
2926
+ await mkdir7(path11.dirname(file), { recursive: true });
2927
+ await writeFile12(
2928
+ file,
2929
+ serializeMemory10({ frontmatter: fm, body: match.proposed_body }),
2930
+ "utf8"
2931
+ );
2932
+ savedIds.push(fm.id);
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ return {
2937
+ scanned_events: recent.length,
2938
+ matches,
2939
+ saved: savedIds.length,
2940
+ saved_ids: savedIds
2941
+ };
2942
+ }
2943
+ function gitChangedFiles(root, sinceDays) {
2944
+ try {
2945
+ const out = execSync2(
2946
+ `git log --name-only --pretty="" --diff-filter=AM --since="${sinceDays} days ago"`,
2947
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2948
+ );
2949
+ return [...new Set(out.split("\n").map((l) => l.trim()).filter(Boolean))];
2950
+ } catch {
2951
+ return [];
2952
+ }
2953
+ }
2954
+ function gitFileDiff(root, file, sinceDays) {
2955
+ try {
2956
+ const out = execSync2(
2957
+ `git log -p --follow --since="${sinceDays} days ago" -- "${file}"`,
2958
+ { cwd: root, encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
2959
+ );
2960
+ if (!out.trim()) return null;
2961
+ const diffLines = out.split("\n").filter(
2962
+ (l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("@@") || l.startsWith("diff")
2963
+ );
2964
+ return diffLines.join("\n").slice(0, MAX_DIFF_BYTES) || null;
2965
+ } catch {
2966
+ return null;
2967
+ }
2968
+ }
2969
+
2970
+ // src/prompts/bootstrap-project.ts
2971
+ import { z as z30 } from "zod";
2577
2972
  var BootstrapProjectArgsSchema = {
2578
- module: z29.string().optional().describe(
2973
+ module: z30.string().optional().describe(
2579
2974
  "Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
2580
2975
  ),
2581
- focus: z29.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2976
+ focus: z30.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
2582
2977
  };
2583
2978
  var ROOT_TEMPLATE = `# Project context
2584
2979
 
@@ -2660,10 +3055,10 @@ ${template}\`\`\`
2660
3055
  }
2661
3056
 
2662
3057
  // src/prompts/post-task.ts
2663
- import { z as z30 } from "zod";
3058
+ import { z as z31 } from "zod";
2664
3059
  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")
3060
+ task_summary: z31.string().optional().describe("One sentence describing what you just did"),
3061
+ files_touched: z31.array(z31.string()).optional().describe("Files you created or modified during the task")
2667
3062
  };
2668
3063
  function postTaskPrompt(args, ctx) {
2669
3064
  const taskLine = args.task_summary ? `
@@ -2731,6 +3126,8 @@ Call **\`mem_session_end\`** with:
2731
3126
 
2732
3127
  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
3128
 
3129
+ 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.
3130
+
2734
3131
  When done, respond with a brief summary: "Saved N memories: [list of IDs]. Session recap saved."
2735
3132
  `;
2736
3133
  return {
@@ -2745,12 +3142,12 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
2745
3142
  }
2746
3143
 
2747
3144
  // src/prompts/import-docs.ts
2748
- import { z as z31 } from "zod";
3145
+ import { z as z32 } from "zod";
2749
3146
  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")
3147
+ content: z32.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
3148
+ source: z32.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
3149
+ scope: z32.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
3150
+ dry_run: z32.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
2754
3151
  };
2755
3152
  function importDocsPrompt(args, ctx) {
2756
3153
  const sourceLine = args.source ? `
@@ -2813,80 +3210,9 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
2813
3210
  };
2814
3211
  }
2815
3212
 
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
3213
  // src/server.ts
2888
3214
  var SERVER_NAME = "haive";
2889
- var SERVER_VERSION = "0.7.2";
3215
+ var SERVER_VERSION = "0.9.0";
2890
3216
  function jsonResult(data) {
2891
3217
  return {
2892
3218
  content: [
@@ -3531,6 +3857,37 @@ function createHaiveServer(options = {}) {
3531
3857
  return jsonResult(await preCommitCheck(input, context));
3532
3858
  }
3533
3859
  );
3860
+ server.tool(
3861
+ "pattern_detect",
3862
+ [
3863
+ "Heuristic memory detector \u2014 finds knowledge worth saving WITHOUT calling an LLM.",
3864
+ "",
3865
+ "Runs three signals over local git history and the tool-usage log:",
3866
+ " 1. CONFIG_CHANGE \u2014 config files modified recently (tsconfig, eslint, prettier, \u2026)",
3867
+ " \u2192 proposes a convention memory with the git diff as body.",
3868
+ " 2. REPEATED_PATH \u2014 same file appears \u22653\xD7 in mem_tried/mem_observe events",
3869
+ " \u2192 proposes a gotcha memory anchored to that path.",
3870
+ " 3. HOT_FILE \u2014 source file referenced \u22653\xD7 in writing-tool events",
3871
+ " \u2192 proposes a convention memory (frequent edits = pattern emerging).",
3872
+ "",
3873
+ "Saves memories with status='proposed'. They feed into auto-promote (Phase 4)",
3874
+ "or are surfaced in the next post_task distillation for LLM review.",
3875
+ "",
3876
+ "USE periodically (e.g. end of sprint) or trigger from post-commit hook.",
3877
+ "",
3878
+ "PARAMETERS:",
3879
+ " since_days \u2014 look-back window in days (default 7)",
3880
+ " dry_run \u2014 report matches without saving (default false)",
3881
+ " scope \u2014 'team' (default) | 'personal'",
3882
+ "",
3883
+ "RETURNS: { scanned_events, matches: [{kind, signal, proposed_type, \u2026}], saved, saved_ids }"
3884
+ ].join("\n"),
3885
+ PatternDetectInputSchema,
3886
+ async (input) => {
3887
+ tracker.record("pattern_detect", `since=${input.since_days}d/dry_run=${input.dry_run}`);
3888
+ return jsonResult(await patternDetect(input, context));
3889
+ }
3890
+ );
3534
3891
  server.tool(
3535
3892
  "mem_diff",
3536
3893
  [