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