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