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