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