@hiveai/cli 0.5.0 → 0.6.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 +818 -127
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command39 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
8
8
|
import { readFile } from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
9
10
|
import "commander";
|
|
10
11
|
import {
|
|
11
12
|
findProjectRoot,
|
|
@@ -58,7 +59,12 @@ function registerBriefing(program2) {
|
|
|
58
59
|
"--scope <scope>",
|
|
59
60
|
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
60
61
|
"all"
|
|
61
|
-
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
62
|
+
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
63
|
+
"--include <path>",
|
|
64
|
+
"merge memories from another haive-initialized project (repeatable). Useful for teams with multiple coordinated repos (e.g. backend + frontend).",
|
|
65
|
+
collectInclude,
|
|
66
|
+
[]
|
|
67
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
62
68
|
const root = findProjectRoot(opts.dir);
|
|
63
69
|
const paths = resolveHaivePaths(root);
|
|
64
70
|
if (!existsSync(paths.memoriesDir)) {
|
|
@@ -71,7 +77,34 @@ function registerBriefing(program2) {
|
|
|
71
77
|
}
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
|
-
const
|
|
80
|
+
const ownMemories = await loadMemoriesFromDir(paths.memoriesDir);
|
|
81
|
+
const externalRoots = [];
|
|
82
|
+
if (opts.include && opts.include.length > 0) {
|
|
83
|
+
for (const includePath of opts.include) {
|
|
84
|
+
try {
|
|
85
|
+
const otherRoot = findProjectRoot(includePath);
|
|
86
|
+
if (otherRoot === root) continue;
|
|
87
|
+
const otherPaths = resolveHaivePaths(otherRoot);
|
|
88
|
+
if (!existsSync(otherPaths.memoriesDir)) {
|
|
89
|
+
ui.warn(`--include ${includePath}: no .ai/memories at ${otherRoot} \u2014 skipping`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const otherMemories = await loadMemoriesFromDir(otherPaths.memoriesDir);
|
|
93
|
+
const tag = path.basename(otherRoot);
|
|
94
|
+
for (const m of otherMemories) {
|
|
95
|
+
ownMemories.push({ ...m, origin: tag });
|
|
96
|
+
}
|
|
97
|
+
externalRoots.push(`${tag} (${otherMemories.length})`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
ui.warn(`--include ${includePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (externalRoots.length > 0) {
|
|
103
|
+
ui.info(`merged from: ${externalRoots.join(", ")}`);
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const all = ownMemories;
|
|
75
108
|
const filePaths = parseCsv(opts.files);
|
|
76
109
|
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
77
110
|
const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
|
|
@@ -148,15 +181,16 @@ function registerBriefing(program2) {
|
|
|
148
181
|
}
|
|
149
182
|
console.log(`${ui.bold("=== Relevant Memories ===")}
|
|
150
183
|
`);
|
|
151
|
-
for (const
|
|
152
|
-
const fm =
|
|
184
|
+
for (const item of top) {
|
|
185
|
+
const fm = item.memory.frontmatter;
|
|
153
186
|
const badge = ui.statusBadge(fm.status);
|
|
154
187
|
const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
|
|
155
188
|
const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
|
|
189
|
+
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
156
190
|
console.log(
|
|
157
|
-
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}`
|
|
191
|
+
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}`
|
|
158
192
|
);
|
|
159
|
-
console.log(
|
|
193
|
+
console.log(item.memory.body.trim());
|
|
160
194
|
console.log();
|
|
161
195
|
}
|
|
162
196
|
console.log(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
@@ -199,6 +233,9 @@ function parseCsv(value) {
|
|
|
199
233
|
if (!value) return [];
|
|
200
234
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
201
235
|
}
|
|
236
|
+
function collectInclude(value, previous) {
|
|
237
|
+
return [...previous, value];
|
|
238
|
+
}
|
|
202
239
|
|
|
203
240
|
// src/commands/tui.ts
|
|
204
241
|
import "commander";
|
|
@@ -223,7 +260,7 @@ function registerTui(program2) {
|
|
|
223
260
|
|
|
224
261
|
// src/commands/embeddings.ts
|
|
225
262
|
import { existsSync as existsSync2 } from "fs";
|
|
226
|
-
import
|
|
263
|
+
import path2 from "path";
|
|
227
264
|
import "commander";
|
|
228
265
|
import { findProjectRoot as findProjectRoot3, resolveHaivePaths as resolveHaivePaths2 } from "@hiveai/core";
|
|
229
266
|
function registerEmbeddings(program2) {
|
|
@@ -265,22 +302,22 @@ function registerEmbeddings(program2) {
|
|
|
265
302
|
for (const hit of result.hits) {
|
|
266
303
|
const score = hit.score.toFixed(3);
|
|
267
304
|
console.log(`${ui.bold(score)} ${hit.id}`);
|
|
268
|
-
console.log(` ${ui.dim(
|
|
305
|
+
console.log(` ${ui.dim(path2.relative(root, hit.file_path))}`);
|
|
269
306
|
}
|
|
270
307
|
});
|
|
271
308
|
embeddings.command("status").description("Show the embeddings index status").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
272
309
|
const root = findProjectRoot3(opts.dir);
|
|
273
310
|
const paths = resolveHaivePaths2(root);
|
|
274
311
|
const { indexStat } = await loadEmbeddings();
|
|
275
|
-
const
|
|
276
|
-
if (!
|
|
312
|
+
const stat2 = await indexStat(paths);
|
|
313
|
+
if (!stat2.exists) {
|
|
277
314
|
ui.warn("No embeddings index. Run `haive embeddings index` to create one.");
|
|
278
315
|
return;
|
|
279
316
|
}
|
|
280
|
-
console.log(`${ui.bold("entries:")} ${
|
|
281
|
-
console.log(`${ui.bold("model:")} ${
|
|
282
|
-
console.log(`${ui.bold("updated_at:")} ${
|
|
283
|
-
console.log(`${ui.bold("size:")} ${(
|
|
317
|
+
console.log(`${ui.bold("entries:")} ${stat2.count}`);
|
|
318
|
+
console.log(`${ui.bold("model:")} ${stat2.model}`);
|
|
319
|
+
console.log(`${ui.bold("updated_at:")} ${stat2.updatedAt}`);
|
|
320
|
+
console.log(`${ui.bold("size:")} ${(stat2.sizeBytes / 1024).toFixed(1)} KB`);
|
|
284
321
|
});
|
|
285
322
|
}
|
|
286
323
|
async function loadEmbeddings() {
|
|
@@ -295,7 +332,7 @@ async function loadEmbeddings() {
|
|
|
295
332
|
}
|
|
296
333
|
|
|
297
334
|
// src/commands/index-code.ts
|
|
298
|
-
import
|
|
335
|
+
import path3 from "path";
|
|
299
336
|
import "commander";
|
|
300
337
|
import {
|
|
301
338
|
buildCodeMap,
|
|
@@ -338,7 +375,7 @@ function registerIndexCode(program2) {
|
|
|
338
375
|
const fileCount = Object.keys(map.files).length;
|
|
339
376
|
const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
|
|
340
377
|
ui.success(
|
|
341
|
-
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${
|
|
378
|
+
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path3.relative(root, codeMapPath(paths))}`
|
|
342
379
|
);
|
|
343
380
|
});
|
|
344
381
|
idx.command("code-search").description(
|
|
@@ -373,7 +410,7 @@ function registerIndexCode(program2) {
|
|
|
373
410
|
// src/commands/init.ts
|
|
374
411
|
import { mkdir, writeFile } from "fs/promises";
|
|
375
412
|
import { existsSync as existsSync3 } from "fs";
|
|
376
|
-
import
|
|
413
|
+
import path4 from "path";
|
|
377
414
|
import { spawnSync } from "child_process";
|
|
378
415
|
import "commander";
|
|
379
416
|
import {
|
|
@@ -544,7 +581,7 @@ function registerInit(program2) {
|
|
|
544
581
|
"--manual",
|
|
545
582
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
546
583
|
).action(async (opts) => {
|
|
547
|
-
const root =
|
|
584
|
+
const root = path4.resolve(opts.dir);
|
|
548
585
|
const paths = resolveHaivePaths4(root);
|
|
549
586
|
const autopilot = opts.manual !== true;
|
|
550
587
|
if (existsSync3(paths.haiveDir)) {
|
|
@@ -556,10 +593,10 @@ function registerInit(program2) {
|
|
|
556
593
|
await mkdir(paths.modulesContextDir, { recursive: true });
|
|
557
594
|
if (!existsSync3(paths.projectContext)) {
|
|
558
595
|
await writeFile(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
559
|
-
ui.success(`Created ${
|
|
596
|
+
ui.success(`Created ${path4.relative(root, paths.projectContext)}`);
|
|
560
597
|
}
|
|
561
598
|
const configExists = existsSync3(
|
|
562
|
-
|
|
599
|
+
path4.join(paths.haiveDir, "haive.config.json")
|
|
563
600
|
);
|
|
564
601
|
if (!configExists) {
|
|
565
602
|
await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
|
|
@@ -570,17 +607,17 @@ function registerInit(program2) {
|
|
|
570
607
|
if (opts.bridges) {
|
|
571
608
|
await writeBridge(root, "CLAUDE.md");
|
|
572
609
|
await writeBridge(root, ".cursorrules");
|
|
573
|
-
await writeBridge(root,
|
|
610
|
+
await writeBridge(root, path4.join(".github", "copilot-instructions.md"));
|
|
574
611
|
}
|
|
575
612
|
const wantCi = opts.withCi || autopilot;
|
|
576
613
|
if (wantCi) {
|
|
577
|
-
const ciPath =
|
|
614
|
+
const ciPath = path4.join(root, ".github", "workflows", "haive-sync.yml");
|
|
578
615
|
if (existsSync3(ciPath)) {
|
|
579
616
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
580
617
|
} else {
|
|
581
|
-
await mkdir(
|
|
618
|
+
await mkdir(path4.dirname(ciPath), { recursive: true });
|
|
582
619
|
await writeFile(ciPath, CI_WORKFLOW, "utf8");
|
|
583
|
-
ui.success(`Created ${
|
|
620
|
+
ui.success(`Created ${path4.relative(root, ciPath)}`);
|
|
584
621
|
}
|
|
585
622
|
}
|
|
586
623
|
if (autopilot) {
|
|
@@ -635,12 +672,12 @@ function registerInit(program2) {
|
|
|
635
672
|
});
|
|
636
673
|
}
|
|
637
674
|
async function writeBridge(root, relPath) {
|
|
638
|
-
const target =
|
|
675
|
+
const target = path4.join(root, relPath);
|
|
639
676
|
if (existsSync3(target)) {
|
|
640
677
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
641
678
|
return;
|
|
642
679
|
}
|
|
643
|
-
await mkdir(
|
|
680
|
+
await mkdir(path4.dirname(target), { recursive: true });
|
|
644
681
|
await writeFile(target, BRIDGE_BODY, "utf8");
|
|
645
682
|
ui.success(`Created bridge ${relPath}`);
|
|
646
683
|
}
|
|
@@ -648,7 +685,7 @@ async function writeBridge(root, relPath) {
|
|
|
648
685
|
// src/commands/install-hooks.ts
|
|
649
686
|
import { mkdir as mkdir2, writeFile as writeFile2, chmod, readFile as readFile2 } from "fs/promises";
|
|
650
687
|
import { existsSync as existsSync4 } from "fs";
|
|
651
|
-
import
|
|
688
|
+
import path5 from "path";
|
|
652
689
|
import "commander";
|
|
653
690
|
import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
654
691
|
var HOOK_MARKER = "# hAIve auto-generated";
|
|
@@ -669,18 +706,18 @@ function registerInstallHooks(program2) {
|
|
|
669
706
|
"Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs a post-merge hook at .git/hooks/post-merge that runs:\n haive sync --quiet --since ORIG_HEAD --embed\n\n This ensures memory anchors are always verified and the embeddings index\n is kept fresh without requiring any manual steps.\n\n Installed automatically by haive init (autopilot mode).\n Use --force to overwrite an existing post-merge hook.\n"
|
|
670
707
|
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
671
708
|
const root = findProjectRoot6(opts.dir);
|
|
672
|
-
const gitDir =
|
|
709
|
+
const gitDir = path5.join(root, ".git");
|
|
673
710
|
if (!existsSync4(gitDir)) {
|
|
674
711
|
ui.error(`No .git directory at ${root}.`);
|
|
675
712
|
process.exitCode = 1;
|
|
676
713
|
return;
|
|
677
714
|
}
|
|
678
|
-
const hooksDir =
|
|
715
|
+
const hooksDir = path5.join(gitDir, "hooks");
|
|
679
716
|
await mkdir2(hooksDir, { recursive: true });
|
|
680
717
|
let installed = 0;
|
|
681
718
|
let skipped = 0;
|
|
682
719
|
for (const name of HOOKS) {
|
|
683
|
-
const file =
|
|
720
|
+
const file = path5.join(hooksDir, name);
|
|
684
721
|
if (existsSync4(file) && !opts.force) {
|
|
685
722
|
const existing = await readFile2(file, "utf8");
|
|
686
723
|
if (!existing.includes(HOOK_MARKER)) {
|
|
@@ -702,7 +739,7 @@ function registerInstallHooks(program2) {
|
|
|
702
739
|
import { spawn } from "child_process";
|
|
703
740
|
import { existsSync as existsSync5 } from "fs";
|
|
704
741
|
import { createRequire } from "module";
|
|
705
|
-
import
|
|
742
|
+
import path6 from "path";
|
|
706
743
|
import { fileURLToPath } from "url";
|
|
707
744
|
import "commander";
|
|
708
745
|
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
@@ -740,13 +777,13 @@ function registerMcp(program2) {
|
|
|
740
777
|
function locateMcpBin() {
|
|
741
778
|
try {
|
|
742
779
|
const pkgPath = require2.resolve("@hiveai/mcp/package.json");
|
|
743
|
-
const pkgDir =
|
|
744
|
-
const candidate =
|
|
780
|
+
const pkgDir = path6.dirname(pkgPath);
|
|
781
|
+
const candidate = path6.join(pkgDir, "dist", "index.js");
|
|
745
782
|
if (existsSync5(candidate)) return candidate;
|
|
746
783
|
} catch {
|
|
747
784
|
}
|
|
748
|
-
const here =
|
|
749
|
-
const sibling =
|
|
785
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
786
|
+
const sibling = path6.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
|
|
750
787
|
if (existsSync5(sibling)) return sibling;
|
|
751
788
|
return null;
|
|
752
789
|
}
|
|
@@ -755,7 +792,7 @@ function locateMcpBin() {
|
|
|
755
792
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
756
793
|
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
757
794
|
import { existsSync as existsSync6 } from "fs";
|
|
758
|
-
import
|
|
795
|
+
import path7 from "path";
|
|
759
796
|
import "commander";
|
|
760
797
|
import {
|
|
761
798
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
@@ -922,7 +959,7 @@ function registerSync(program2) {
|
|
|
922
959
|
);
|
|
923
960
|
}
|
|
924
961
|
if (opts.injectBridge) {
|
|
925
|
-
const bridgeFile = opts.bridgeFile ?
|
|
962
|
+
const bridgeFile = opts.bridgeFile ? path7.resolve(opts.bridgeFile) : path7.join(root, "CLAUDE.md");
|
|
926
963
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
927
964
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
928
965
|
}
|
|
@@ -1028,10 +1065,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1028
1065
|
paths: [result.file],
|
|
1029
1066
|
topic: `dep-bump-${slugParts}`
|
|
1030
1067
|
});
|
|
1031
|
-
const teamDir =
|
|
1068
|
+
const teamDir = path7.join(paths.memoriesDir, "team");
|
|
1032
1069
|
await mkdir3(teamDir, { recursive: true });
|
|
1033
1070
|
await writeFile3(
|
|
1034
|
-
|
|
1071
|
+
path7.join(teamDir, `${fm.id}.md`),
|
|
1035
1072
|
serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1036
1073
|
"utf8"
|
|
1037
1074
|
);
|
|
@@ -1095,10 +1132,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1095
1132
|
paths: [diff.file],
|
|
1096
1133
|
topic: `contract-breaking-${diff.contract}`
|
|
1097
1134
|
});
|
|
1098
|
-
const teamDir =
|
|
1135
|
+
const teamDir = path7.join(paths.memoriesDir, "team");
|
|
1099
1136
|
await mkdir3(teamDir, { recursive: true });
|
|
1100
1137
|
await writeFile3(
|
|
1101
|
-
|
|
1138
|
+
path7.join(teamDir, `${fm.id}.md`),
|
|
1102
1139
|
serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1103
1140
|
"utf8"
|
|
1104
1141
|
);
|
|
@@ -1190,11 +1227,11 @@ ${BRIDGE_END}`;
|
|
|
1190
1227
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
1191
1228
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
1192
1229
|
if (startIdx !== -1 && endIdx === -1) {
|
|
1193
|
-
ui.warn(`${
|
|
1230
|
+
ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
1194
1231
|
return;
|
|
1195
1232
|
}
|
|
1196
1233
|
if (startIdx === -1 && endIdx !== -1) {
|
|
1197
|
-
ui.warn(`${
|
|
1234
|
+
ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
1198
1235
|
return;
|
|
1199
1236
|
}
|
|
1200
1237
|
let updated;
|
|
@@ -1202,14 +1239,14 @@ ${BRIDGE_END}`;
|
|
|
1202
1239
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
1203
1240
|
} else {
|
|
1204
1241
|
if (!fileExists && !quiet) {
|
|
1205
|
-
ui.info(`Creating ${
|
|
1242
|
+
ui.info(`Creating ${path7.relative(root, bridgeFile)} with haive memory block.`);
|
|
1206
1243
|
}
|
|
1207
1244
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
1208
1245
|
}
|
|
1209
1246
|
await writeFile3(bridgeFile, updated, "utf8");
|
|
1210
1247
|
if (!quiet) {
|
|
1211
1248
|
console.log(
|
|
1212
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
1249
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path7.relative(root, bridgeFile)}`)
|
|
1213
1250
|
);
|
|
1214
1251
|
}
|
|
1215
1252
|
}
|
|
@@ -1236,7 +1273,7 @@ function collectSinceChanges(root, ref) {
|
|
|
1236
1273
|
import { createHash } from "crypto";
|
|
1237
1274
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1238
1275
|
import { existsSync as existsSync7 } from "fs";
|
|
1239
|
-
import
|
|
1276
|
+
import path8 from "path";
|
|
1240
1277
|
import "commander";
|
|
1241
1278
|
import {
|
|
1242
1279
|
buildFrontmatter as buildFrontmatter2,
|
|
@@ -1285,7 +1322,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1285
1322
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
|
|
1286
1323
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
1287
1324
|
if (anchorPaths.length > 0) {
|
|
1288
|
-
const missing = anchorPaths.filter((p) => !existsSync7(
|
|
1325
|
+
const missing = anchorPaths.filter((p) => !existsSync7(path8.resolve(root, p)));
|
|
1289
1326
|
if (missing.length > 0) {
|
|
1290
1327
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
1291
1328
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -1350,7 +1387,7 @@ TODO \u2014 write the memory body.
|
|
|
1350
1387
|
}
|
|
1351
1388
|
};
|
|
1352
1389
|
await writeFile4(topicMatch.filePath, serializeMemory2({ frontmatter: newFrontmatter, body }), "utf8");
|
|
1353
|
-
ui.success(`Updated (topic upsert) ${
|
|
1390
|
+
ui.success(`Updated (topic upsert) ${path8.relative(root, topicMatch.filePath)}`);
|
|
1354
1391
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
1355
1392
|
return;
|
|
1356
1393
|
}
|
|
@@ -1369,7 +1406,7 @@ TODO \u2014 write the memory body.
|
|
|
1369
1406
|
topic: opts.topic
|
|
1370
1407
|
});
|
|
1371
1408
|
const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1372
|
-
await mkdir4(
|
|
1409
|
+
await mkdir4(path8.dirname(file), { recursive: true });
|
|
1373
1410
|
if (existsSync7(file)) {
|
|
1374
1411
|
ui.error(`Memory already exists at ${file}`);
|
|
1375
1412
|
process.exitCode = 1;
|
|
@@ -1388,7 +1425,7 @@ TODO \u2014 write the memory body.
|
|
|
1388
1425
|
}
|
|
1389
1426
|
}
|
|
1390
1427
|
await writeFile4(file, serializeMemory2({ frontmatter, body }), "utf8");
|
|
1391
|
-
ui.success(`Created ${
|
|
1428
|
+
ui.success(`Created ${path8.relative(root, file)}`);
|
|
1392
1429
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
1393
1430
|
if (inferredTags.length > 0) {
|
|
1394
1431
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -1419,7 +1456,7 @@ function parseCsv2(value) {
|
|
|
1419
1456
|
|
|
1420
1457
|
// src/commands/memory-list.ts
|
|
1421
1458
|
import { existsSync as existsSync8 } from "fs";
|
|
1422
|
-
import
|
|
1459
|
+
import path9 from "path";
|
|
1423
1460
|
import "commander";
|
|
1424
1461
|
import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
|
|
1425
1462
|
|
|
@@ -1467,7 +1504,7 @@ function registerMemoryList(memory2) {
|
|
|
1467
1504
|
console.log(
|
|
1468
1505
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
1469
1506
|
);
|
|
1470
|
-
console.log(` ${ui.dim(
|
|
1507
|
+
console.log(` ${ui.dim(path9.relative(root, filePath))}`);
|
|
1471
1508
|
}
|
|
1472
1509
|
console.log(ui.dim(`
|
|
1473
1510
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -1504,7 +1541,7 @@ function matchesFilters(loaded, opts) {
|
|
|
1504
1541
|
// src/commands/memory-promote.ts
|
|
1505
1542
|
import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
|
|
1506
1543
|
import { existsSync as existsSync9 } from "fs";
|
|
1507
|
-
import
|
|
1544
|
+
import path10 from "path";
|
|
1508
1545
|
import "commander";
|
|
1509
1546
|
import {
|
|
1510
1547
|
findProjectRoot as findProjectRoot11,
|
|
@@ -1551,11 +1588,11 @@ function registerMemoryPromote(memory2) {
|
|
|
1551
1588
|
body: found.memory.body
|
|
1552
1589
|
};
|
|
1553
1590
|
const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
|
|
1554
|
-
await mkdir5(
|
|
1591
|
+
await mkdir5(path10.dirname(newPath), { recursive: true });
|
|
1555
1592
|
await writeFile5(newPath, serializeMemory3(updated), "utf8");
|
|
1556
1593
|
await unlink(found.filePath);
|
|
1557
1594
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
1558
|
-
ui.info(`Now at ${
|
|
1595
|
+
ui.info(`Now at ${path10.relative(root, newPath)}`);
|
|
1559
1596
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
1560
1597
|
});
|
|
1561
1598
|
}
|
|
@@ -1563,7 +1600,7 @@ function registerMemoryPromote(memory2) {
|
|
|
1563
1600
|
// src/commands/memory-approve.ts
|
|
1564
1601
|
import { existsSync as existsSync10 } from "fs";
|
|
1565
1602
|
import { writeFile as writeFile6 } from "fs/promises";
|
|
1566
|
-
import
|
|
1603
|
+
import path11 from "path";
|
|
1567
1604
|
import "commander";
|
|
1568
1605
|
import {
|
|
1569
1606
|
findProjectRoot as findProjectRoot12,
|
|
@@ -1627,14 +1664,14 @@ function registerMemoryApprove(memory2) {
|
|
|
1627
1664
|
};
|
|
1628
1665
|
await writeFile6(found.filePath, serializeMemory4(next), "utf8");
|
|
1629
1666
|
ui.success(`Approved ${id} (status=validated)`);
|
|
1630
|
-
ui.info(
|
|
1667
|
+
ui.info(path11.relative(root, found.filePath));
|
|
1631
1668
|
});
|
|
1632
1669
|
}
|
|
1633
1670
|
|
|
1634
1671
|
// src/commands/memory-update.ts
|
|
1635
1672
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
1636
1673
|
import { existsSync as existsSync11 } from "fs";
|
|
1637
|
-
import
|
|
1674
|
+
import path12 from "path";
|
|
1638
1675
|
import "commander";
|
|
1639
1676
|
import {
|
|
1640
1677
|
findProjectRoot as findProjectRoot13,
|
|
@@ -1697,7 +1734,7 @@ function registerMemoryUpdate(memory2) {
|
|
|
1697
1734
|
serializeMemory5({ frontmatter: newFrontmatter, body: newBody }),
|
|
1698
1735
|
"utf8"
|
|
1699
1736
|
);
|
|
1700
|
-
ui.success(`Updated ${
|
|
1737
|
+
ui.success(`Updated ${path12.relative(root, loaded.filePath)}`);
|
|
1701
1738
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
1702
1739
|
});
|
|
1703
1740
|
}
|
|
@@ -1718,7 +1755,7 @@ function parseCsv3(value) {
|
|
|
1718
1755
|
// src/commands/memory-auto-promote.ts
|
|
1719
1756
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
1720
1757
|
import { existsSync as existsSync12 } from "fs";
|
|
1721
|
-
import
|
|
1758
|
+
import path13 from "path";
|
|
1722
1759
|
import "commander";
|
|
1723
1760
|
import {
|
|
1724
1761
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
@@ -1763,7 +1800,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1763
1800
|
console.log(
|
|
1764
1801
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
1765
1802
|
);
|
|
1766
|
-
console.log(` ${ui.dim(
|
|
1803
|
+
console.log(` ${ui.dim(path13.relative(root, filePath))}`);
|
|
1767
1804
|
if (opts.apply) {
|
|
1768
1805
|
const next = {
|
|
1769
1806
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
@@ -1782,7 +1819,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1782
1819
|
import { spawn as spawn2 } from "child_process";
|
|
1783
1820
|
import { existsSync as existsSync13 } from "fs";
|
|
1784
1821
|
import { readFile as readFile5 } from "fs/promises";
|
|
1785
|
-
import
|
|
1822
|
+
import path14 from "path";
|
|
1786
1823
|
import "commander";
|
|
1787
1824
|
import {
|
|
1788
1825
|
findProjectRoot as findProjectRoot15,
|
|
@@ -1806,7 +1843,7 @@ function registerMemoryEdit(memory2) {
|
|
|
1806
1843
|
return;
|
|
1807
1844
|
}
|
|
1808
1845
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
1809
|
-
ui.info(`Opening ${
|
|
1846
|
+
ui.info(`Opening ${path14.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
1810
1847
|
const code = await runEditor(editor, found.filePath);
|
|
1811
1848
|
if (code !== 0) {
|
|
1812
1849
|
ui.warn(`Editor exited with status ${code}.`);
|
|
@@ -1834,7 +1871,7 @@ function runEditor(editor, file) {
|
|
|
1834
1871
|
|
|
1835
1872
|
// src/commands/memory-for-files.ts
|
|
1836
1873
|
import { existsSync as existsSync14 } from "fs";
|
|
1837
|
-
import
|
|
1874
|
+
import path15 from "path";
|
|
1838
1875
|
import "commander";
|
|
1839
1876
|
import {
|
|
1840
1877
|
deriveConfidence,
|
|
@@ -1956,13 +1993,13 @@ function printGroup(root, label, loaded, usage) {
|
|
|
1956
1993
|
const u = getUsage3(usage, fm.id);
|
|
1957
1994
|
const conf = deriveConfidence(fm, u);
|
|
1958
1995
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
1959
|
-
console.log(` ${ui.dim(
|
|
1996
|
+
console.log(` ${ui.dim(path15.relative(root, filePath))}`);
|
|
1960
1997
|
}
|
|
1961
1998
|
}
|
|
1962
1999
|
|
|
1963
2000
|
// src/commands/memory-hot.ts
|
|
1964
2001
|
import { existsSync as existsSync15 } from "fs";
|
|
1965
|
-
import
|
|
2002
|
+
import path16 from "path";
|
|
1966
2003
|
import "commander";
|
|
1967
2004
|
import {
|
|
1968
2005
|
findProjectRoot as findProjectRoot17,
|
|
@@ -2002,7 +2039,7 @@ function registerMemoryHot(memory2) {
|
|
|
2002
2039
|
console.log(
|
|
2003
2040
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2004
2041
|
);
|
|
2005
|
-
console.log(` ${ui.dim(
|
|
2042
|
+
console.log(` ${ui.dim(path16.relative(root, filePath))}`);
|
|
2006
2043
|
}
|
|
2007
2044
|
ui.info(
|
|
2008
2045
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -2013,7 +2050,7 @@ function registerMemoryHot(memory2) {
|
|
|
2013
2050
|
// src/commands/memory-tried.ts
|
|
2014
2051
|
import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
|
|
2015
2052
|
import { existsSync as existsSync16 } from "fs";
|
|
2016
|
-
import
|
|
2053
|
+
import path17 from "path";
|
|
2017
2054
|
import "commander";
|
|
2018
2055
|
import {
|
|
2019
2056
|
buildFrontmatter as buildFrontmatter3,
|
|
@@ -2064,14 +2101,14 @@ function registerMemoryTried(memory2) {
|
|
|
2064
2101
|
}
|
|
2065
2102
|
const body = lines.join("\n") + "\n";
|
|
2066
2103
|
const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2067
|
-
await mkdir6(
|
|
2104
|
+
await mkdir6(path17.dirname(file), { recursive: true });
|
|
2068
2105
|
if (existsSync16(file)) {
|
|
2069
2106
|
ui.error(`Memory already exists at ${file}`);
|
|
2070
2107
|
process.exitCode = 1;
|
|
2071
2108
|
return;
|
|
2072
2109
|
}
|
|
2073
2110
|
await writeFile9(file, serializeMemory7({ frontmatter, body }), "utf8");
|
|
2074
|
-
ui.success(`Recorded: ${
|
|
2111
|
+
ui.success(`Recorded: ${path17.relative(root, file)}`);
|
|
2075
2112
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
2076
2113
|
});
|
|
2077
2114
|
}
|
|
@@ -2082,7 +2119,7 @@ function parseCsv4(value) {
|
|
|
2082
2119
|
|
|
2083
2120
|
// src/commands/memory-pending.ts
|
|
2084
2121
|
import { existsSync as existsSync17 } from "fs";
|
|
2085
|
-
import
|
|
2122
|
+
import path18 from "path";
|
|
2086
2123
|
import "commander";
|
|
2087
2124
|
import {
|
|
2088
2125
|
findProjectRoot as findProjectRoot19,
|
|
@@ -2122,7 +2159,7 @@ function registerMemoryPending(memory2) {
|
|
|
2122
2159
|
console.log(
|
|
2123
2160
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2124
2161
|
);
|
|
2125
|
-
console.log(` ${ui.dim(
|
|
2162
|
+
console.log(` ${ui.dim(path18.relative(root, filePath))}`);
|
|
2126
2163
|
}
|
|
2127
2164
|
ui.info(`${proposed.length} pending`);
|
|
2128
2165
|
});
|
|
@@ -2130,7 +2167,7 @@ function registerMemoryPending(memory2) {
|
|
|
2130
2167
|
|
|
2131
2168
|
// src/commands/memory-query.ts
|
|
2132
2169
|
import { existsSync as existsSync18 } from "fs";
|
|
2133
|
-
import
|
|
2170
|
+
import path19 from "path";
|
|
2134
2171
|
import "commander";
|
|
2135
2172
|
import {
|
|
2136
2173
|
extractSnippet,
|
|
@@ -2187,7 +2224,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2187
2224
|
const fm = mem.frontmatter;
|
|
2188
2225
|
const statusBadge = ui.statusBadge(fm.status);
|
|
2189
2226
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
2190
|
-
console.log(` ${ui.dim(
|
|
2227
|
+
console.log(` ${ui.dim(path19.relative(root, filePath))}`);
|
|
2191
2228
|
const snippet = extractSnippet(mem.body, snippetNeedle);
|
|
2192
2229
|
if (snippet) console.log(` ${snippet}`);
|
|
2193
2230
|
}
|
|
@@ -2257,7 +2294,7 @@ function registerMemoryReject(memory2) {
|
|
|
2257
2294
|
// src/commands/memory-rm.ts
|
|
2258
2295
|
import { existsSync as existsSync20 } from "fs";
|
|
2259
2296
|
import { unlink as unlink2 } from "fs/promises";
|
|
2260
|
-
import
|
|
2297
|
+
import path20 from "path";
|
|
2261
2298
|
import { createInterface } from "readline/promises";
|
|
2262
2299
|
import "commander";
|
|
2263
2300
|
import {
|
|
@@ -2282,7 +2319,7 @@ function registerMemoryRm(memory2) {
|
|
|
2282
2319
|
process.exitCode = 1;
|
|
2283
2320
|
return;
|
|
2284
2321
|
}
|
|
2285
|
-
const rel =
|
|
2322
|
+
const rel = path20.relative(root, found.filePath);
|
|
2286
2323
|
if (!opts.yes) {
|
|
2287
2324
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2288
2325
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -2308,7 +2345,7 @@ function registerMemoryRm(memory2) {
|
|
|
2308
2345
|
// src/commands/memory-show.ts
|
|
2309
2346
|
import { existsSync as existsSync21 } from "fs";
|
|
2310
2347
|
import { readFile as readFile6 } from "fs/promises";
|
|
2311
|
-
import
|
|
2348
|
+
import path21 from "path";
|
|
2312
2349
|
import "commander";
|
|
2313
2350
|
import {
|
|
2314
2351
|
deriveConfidence as deriveConfidence2,
|
|
@@ -2350,7 +2387,7 @@ function registerMemoryShow(memory2) {
|
|
|
2350
2387
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
2351
2388
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
2352
2389
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
2353
|
-
console.log(`${ui.dim("file:")} ${
|
|
2390
|
+
console.log(`${ui.dim("file:")} ${path21.relative(root, found.filePath)}`);
|
|
2354
2391
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
2355
2392
|
console.log(ui.dim("anchor:"));
|
|
2356
2393
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -2366,7 +2403,7 @@ function registerMemoryShow(memory2) {
|
|
|
2366
2403
|
|
|
2367
2404
|
// src/commands/memory-stats.ts
|
|
2368
2405
|
import { existsSync as existsSync22 } from "fs";
|
|
2369
|
-
import
|
|
2406
|
+
import path22 from "path";
|
|
2370
2407
|
import "commander";
|
|
2371
2408
|
import {
|
|
2372
2409
|
deriveConfidence as deriveConfidence3,
|
|
@@ -2404,7 +2441,7 @@ function registerMemoryStats(memory2) {
|
|
|
2404
2441
|
console.log(
|
|
2405
2442
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
2406
2443
|
);
|
|
2407
|
-
console.log(` ${ui.dim(
|
|
2444
|
+
console.log(` ${ui.dim(path22.relative(root, filePath))}`);
|
|
2408
2445
|
}
|
|
2409
2446
|
});
|
|
2410
2447
|
}
|
|
@@ -2412,7 +2449,7 @@ function registerMemoryStats(memory2) {
|
|
|
2412
2449
|
// src/commands/memory-verify.ts
|
|
2413
2450
|
import { writeFile as writeFile11 } from "fs/promises";
|
|
2414
2451
|
import { existsSync as existsSync23 } from "fs";
|
|
2415
|
-
import
|
|
2452
|
+
import path23 from "path";
|
|
2416
2453
|
import "commander";
|
|
2417
2454
|
import {
|
|
2418
2455
|
findProjectRoot as findProjectRoot25,
|
|
@@ -2449,7 +2486,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2449
2486
|
anchorlessIds.push(mem.frontmatter.id);
|
|
2450
2487
|
continue;
|
|
2451
2488
|
}
|
|
2452
|
-
const rel =
|
|
2489
|
+
const rel = path23.relative(root, filePath);
|
|
2453
2490
|
if (result.stale) {
|
|
2454
2491
|
staleCount++;
|
|
2455
2492
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -2565,7 +2602,7 @@ function registerMemoryImport(memory2) {
|
|
|
2565
2602
|
// src/commands/memory-import-changelog.ts
|
|
2566
2603
|
import { existsSync as existsSync25 } from "fs";
|
|
2567
2604
|
import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
|
|
2568
|
-
import
|
|
2605
|
+
import path24 from "path";
|
|
2569
2606
|
import "commander";
|
|
2570
2607
|
import {
|
|
2571
2608
|
buildFrontmatter as buildFrontmatter4,
|
|
@@ -2636,7 +2673,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2636
2673
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2637
2674
|
const root = findProjectRoot27(opts.dir);
|
|
2638
2675
|
const paths = resolveHaivePaths24(root);
|
|
2639
|
-
const changelogPath =
|
|
2676
|
+
const changelogPath = path24.resolve(root, opts.fromChangelog);
|
|
2640
2677
|
if (!existsSync25(changelogPath)) {
|
|
2641
2678
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
2642
2679
|
process.exitCode = 1;
|
|
@@ -2656,9 +2693,9 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2656
2693
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
2657
2694
|
}
|
|
2658
2695
|
}
|
|
2659
|
-
const pkgName = opts.package ??
|
|
2696
|
+
const pkgName = opts.package ?? path24.basename(path24.dirname(changelogPath));
|
|
2660
2697
|
const scope = opts.scope ?? "team";
|
|
2661
|
-
const teamDir =
|
|
2698
|
+
const teamDir = path24.join(paths.memoriesDir, scope);
|
|
2662
2699
|
await mkdir7(teamDir, { recursive: true });
|
|
2663
2700
|
let saved = 0;
|
|
2664
2701
|
for (const entry of entries) {
|
|
@@ -2681,7 +2718,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2681
2718
|
lines.push("");
|
|
2682
2719
|
}
|
|
2683
2720
|
lines.push(
|
|
2684
|
-
`**Source:** \`${
|
|
2721
|
+
`**Source:** \`${path24.relative(root, changelogPath)}\`
|
|
2685
2722
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
2686
2723
|
);
|
|
2687
2724
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -2696,11 +2733,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2696
2733
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
2697
2734
|
`v${entry.version}`
|
|
2698
2735
|
],
|
|
2699
|
-
paths: [
|
|
2736
|
+
paths: [path24.relative(root, changelogPath)],
|
|
2700
2737
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
2701
2738
|
});
|
|
2702
2739
|
await writeFile12(
|
|
2703
|
-
|
|
2740
|
+
path24.join(teamDir, `${fm.id}.md`),
|
|
2704
2741
|
serializeMemory10({ frontmatter: fm, body: lines.join("\n") }),
|
|
2705
2742
|
"utf8"
|
|
2706
2743
|
);
|
|
@@ -2725,7 +2762,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
2725
2762
|
// src/commands/memory-digest.ts
|
|
2726
2763
|
import { existsSync as existsSync26 } from "fs";
|
|
2727
2764
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
2728
|
-
import
|
|
2765
|
+
import path25 from "path";
|
|
2729
2766
|
import "commander";
|
|
2730
2767
|
import {
|
|
2731
2768
|
deriveConfidence as deriveConfidence4,
|
|
@@ -2820,7 +2857,7 @@ function registerMemoryDigest(program2) {
|
|
|
2820
2857
|
);
|
|
2821
2858
|
const digest = lines.join("\n");
|
|
2822
2859
|
if (opts.out) {
|
|
2823
|
-
const outPath =
|
|
2860
|
+
const outPath = path25.resolve(process.cwd(), opts.out);
|
|
2824
2861
|
await writeFile13(outPath, digest, "utf8");
|
|
2825
2862
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
2826
2863
|
} else {
|
|
@@ -2832,7 +2869,7 @@ function registerMemoryDigest(program2) {
|
|
|
2832
2869
|
// src/commands/session-end.ts
|
|
2833
2870
|
import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
|
|
2834
2871
|
import { existsSync as existsSync27 } from "fs";
|
|
2835
|
-
import
|
|
2872
|
+
import path26 from "path";
|
|
2836
2873
|
import "commander";
|
|
2837
2874
|
import {
|
|
2838
2875
|
buildFrontmatter as buildFrontmatter5,
|
|
@@ -2900,7 +2937,7 @@ function registerSessionEnd(session2) {
|
|
|
2900
2937
|
const body = buildRecapBody(opts);
|
|
2901
2938
|
const topic = recapTopic(scope, opts.module);
|
|
2902
2939
|
const filesTouched = parseCsv5(opts.files);
|
|
2903
|
-
const missingPaths = filesTouched.filter((p) => !existsSync27(
|
|
2940
|
+
const missingPaths = filesTouched.filter((p) => !existsSync27(path26.resolve(root, p)));
|
|
2904
2941
|
if (missingPaths.length > 0) {
|
|
2905
2942
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
2906
2943
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
@@ -2923,7 +2960,7 @@ function registerSessionEnd(session2) {
|
|
|
2923
2960
|
};
|
|
2924
2961
|
await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2925
2962
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
2926
|
-
ui.info(`id=${fm.id} file=${
|
|
2963
|
+
ui.info(`id=${fm.id} file=${path26.relative(root, topicMatch.filePath)}`);
|
|
2927
2964
|
return;
|
|
2928
2965
|
}
|
|
2929
2966
|
}
|
|
@@ -2938,10 +2975,10 @@ function registerSessionEnd(session2) {
|
|
|
2938
2975
|
status: "validated"
|
|
2939
2976
|
});
|
|
2940
2977
|
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2941
|
-
await mkdir8(
|
|
2978
|
+
await mkdir8(path26.dirname(file), { recursive: true });
|
|
2942
2979
|
await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
|
|
2943
2980
|
ui.success(`Session recap created`);
|
|
2944
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
2981
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path26.relative(root, file)}`);
|
|
2945
2982
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
2946
2983
|
});
|
|
2947
2984
|
}
|
|
@@ -2953,7 +2990,7 @@ function parseCsv5(value) {
|
|
|
2953
2990
|
// src/commands/snapshot.ts
|
|
2954
2991
|
import { existsSync as existsSync28 } from "fs";
|
|
2955
2992
|
import { readdir } from "fs/promises";
|
|
2956
|
-
import
|
|
2993
|
+
import path27 from "path";
|
|
2957
2994
|
import "commander";
|
|
2958
2995
|
import {
|
|
2959
2996
|
diffContract,
|
|
@@ -2992,7 +3029,7 @@ function registerSnapshot(program2) {
|
|
|
2992
3029
|
return;
|
|
2993
3030
|
}
|
|
2994
3031
|
if (opts.list) {
|
|
2995
|
-
const contractsDir =
|
|
3032
|
+
const contractsDir = path27.join(paths.haiveDir, "contracts");
|
|
2996
3033
|
if (!existsSync28(contractsDir)) {
|
|
2997
3034
|
console.log(ui.dim("No contract snapshots found."));
|
|
2998
3035
|
return;
|
|
@@ -3048,7 +3085,7 @@ function registerSnapshot(program2) {
|
|
|
3048
3085
|
return;
|
|
3049
3086
|
}
|
|
3050
3087
|
const contractPath = opts.contract;
|
|
3051
|
-
const name = opts.name ??
|
|
3088
|
+
const name = opts.name ?? path27.basename(contractPath, path27.extname(contractPath));
|
|
3052
3089
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
3053
3090
|
const contract = { name, path: contractPath, format };
|
|
3054
3091
|
try {
|
|
@@ -3103,8 +3140,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
3103
3140
|
}
|
|
3104
3141
|
}
|
|
3105
3142
|
function detectFormat(filePath) {
|
|
3106
|
-
const ext =
|
|
3107
|
-
const base =
|
|
3143
|
+
const ext = path27.extname(filePath).toLowerCase();
|
|
3144
|
+
const base = path27.basename(filePath).toLowerCase();
|
|
3108
3145
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
3109
3146
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
3110
3147
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -3119,7 +3156,7 @@ function detectFormat(filePath) {
|
|
|
3119
3156
|
// src/commands/hub.ts
|
|
3120
3157
|
import { existsSync as existsSync29 } from "fs";
|
|
3121
3158
|
import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile15, copyFile } from "fs/promises";
|
|
3122
|
-
import
|
|
3159
|
+
import path28 from "path";
|
|
3123
3160
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3124
3161
|
import "commander";
|
|
3125
3162
|
import {
|
|
@@ -3138,7 +3175,7 @@ function registerHub(program2) {
|
|
|
3138
3175
|
hub.command("init <hubPath>").description(
|
|
3139
3176
|
"Initialize a new team-knowledge hub repo at <hubPath>.\n\n Creates a git repo with a .ai/ directory structure ready for shared memories.\n\n Example:\n haive hub init ../team-hub\n haive hub init /srv/git/team-knowledge\n"
|
|
3140
3177
|
).action(async (hubPath) => {
|
|
3141
|
-
const absPath =
|
|
3178
|
+
const absPath = path28.resolve(hubPath);
|
|
3142
3179
|
await mkdir9(absPath, { recursive: true });
|
|
3143
3180
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
3144
3181
|
if (gitCheck.status !== 0) {
|
|
@@ -3149,10 +3186,10 @@ function registerHub(program2) {
|
|
|
3149
3186
|
return;
|
|
3150
3187
|
}
|
|
3151
3188
|
}
|
|
3152
|
-
const sharedDir =
|
|
3189
|
+
const sharedDir = path28.join(absPath, ".ai", "memories", "shared");
|
|
3153
3190
|
await mkdir9(sharedDir, { recursive: true });
|
|
3154
3191
|
await writeFile15(
|
|
3155
|
-
|
|
3192
|
+
path28.join(absPath, ".ai", "README.md"),
|
|
3156
3193
|
`# hAIve Team Knowledge Hub
|
|
3157
3194
|
|
|
3158
3195
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -3174,7 +3211,7 @@ haive hub pull # import into a project
|
|
|
3174
3211
|
"utf8"
|
|
3175
3212
|
);
|
|
3176
3213
|
await writeFile15(
|
|
3177
|
-
|
|
3214
|
+
path28.join(absPath, ".gitignore"),
|
|
3178
3215
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
3179
3216
|
"utf8"
|
|
3180
3217
|
);
|
|
@@ -3189,7 +3226,7 @@ haive hub pull # import into a project
|
|
|
3189
3226
|
`
|
|
3190
3227
|
Next steps:
|
|
3191
3228
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
3192
|
-
{ "hubPath": "${
|
|
3229
|
+
{ "hubPath": "${path28.relative(process.cwd(), absPath)}" }
|
|
3193
3230
|
2. Run \`haive hub push\` to publish your shared memories
|
|
3194
3231
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
3195
3232
|
`
|
|
@@ -3218,14 +3255,14 @@ Next steps:
|
|
|
3218
3255
|
process.exitCode = 1;
|
|
3219
3256
|
return;
|
|
3220
3257
|
}
|
|
3221
|
-
const hubRoot =
|
|
3258
|
+
const hubRoot = path28.resolve(root, config.hubPath);
|
|
3222
3259
|
if (!existsSync29(hubRoot)) {
|
|
3223
3260
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
3224
3261
|
process.exitCode = 1;
|
|
3225
3262
|
return;
|
|
3226
3263
|
}
|
|
3227
|
-
const projectName =
|
|
3228
|
-
const destDir =
|
|
3264
|
+
const projectName = path28.basename(root);
|
|
3265
|
+
const destDir = path28.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
3229
3266
|
await mkdir9(destDir, { recursive: true });
|
|
3230
3267
|
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3231
3268
|
const shared = all.filter(
|
|
@@ -3244,7 +3281,7 @@ Next steps:
|
|
|
3244
3281
|
for (const { memory: memory2 } of shared) {
|
|
3245
3282
|
const fm = memory2.frontmatter;
|
|
3246
3283
|
const fileName = `${fm.id}.md`;
|
|
3247
|
-
const destPath =
|
|
3284
|
+
const destPath = path28.join(destDir, fileName);
|
|
3248
3285
|
await writeFile15(destPath, serializeMemory12(memory2), "utf8");
|
|
3249
3286
|
pushed++;
|
|
3250
3287
|
}
|
|
@@ -3252,7 +3289,7 @@ Next steps:
|
|
|
3252
3289
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
3253
3290
|
if (opts.commit) {
|
|
3254
3291
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
3255
|
-
spawnSync3("git", ["add",
|
|
3292
|
+
spawnSync3("git", ["add", path28.join(".ai", "memories", "shared", projectName)], {
|
|
3256
3293
|
cwd: hubRoot
|
|
3257
3294
|
});
|
|
3258
3295
|
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
@@ -3287,13 +3324,13 @@ Next steps:
|
|
|
3287
3324
|
process.exitCode = 1;
|
|
3288
3325
|
return;
|
|
3289
3326
|
}
|
|
3290
|
-
const hubRoot =
|
|
3291
|
-
const hubSharedDir =
|
|
3327
|
+
const hubRoot = path28.resolve(root, config.hubPath);
|
|
3328
|
+
const hubSharedDir = path28.join(hubRoot, ".ai", "memories", "shared");
|
|
3292
3329
|
if (!existsSync29(hubSharedDir)) {
|
|
3293
3330
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
3294
3331
|
return;
|
|
3295
3332
|
}
|
|
3296
|
-
const projectName =
|
|
3333
|
+
const projectName = path28.basename(root);
|
|
3297
3334
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3298
3335
|
const projectDirs = (await readdir2(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
3299
3336
|
if (projectDirs.length === 0) {
|
|
@@ -3303,16 +3340,16 @@ Next steps:
|
|
|
3303
3340
|
let totalImported = 0;
|
|
3304
3341
|
let totalUpdated = 0;
|
|
3305
3342
|
for (const sourceName of projectDirs) {
|
|
3306
|
-
const sourceDir =
|
|
3307
|
-
const destDir =
|
|
3343
|
+
const sourceDir = path28.join(hubSharedDir, sourceName);
|
|
3344
|
+
const destDir = path28.join(paths.memoriesDir, "shared", sourceName);
|
|
3308
3345
|
await mkdir9(destDir, { recursive: true });
|
|
3309
3346
|
const sourceFiles = (await readdir2(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
3310
3347
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
3311
3348
|
const existingInDest = await loadDir(destDir);
|
|
3312
3349
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
3313
3350
|
for (const file of sourceFiles) {
|
|
3314
|
-
const srcPath =
|
|
3315
|
-
const destPath =
|
|
3351
|
+
const srcPath = path28.join(sourceDir, file);
|
|
3352
|
+
const destPath = path28.join(destDir, file);
|
|
3316
3353
|
const fileContent = await readFile9(srcPath, "utf8");
|
|
3317
3354
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
3318
3355
|
if (!alreadyTagged) {
|
|
@@ -3343,14 +3380,14 @@ Next steps:
|
|
|
3343
3380
|
console.log(
|
|
3344
3381
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
3345
3382
|
);
|
|
3346
|
-
const sharedDir =
|
|
3383
|
+
const sharedDir = path28.join(paths.memoriesDir, "shared");
|
|
3347
3384
|
if (existsSync29(sharedDir)) {
|
|
3348
3385
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3349
3386
|
const sources = (await readdir2(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3350
3387
|
console.log(`
|
|
3351
3388
|
Imported from ${sources.length} source(s):`);
|
|
3352
3389
|
for (const src of sources) {
|
|
3353
|
-
const files = (await readdir2(
|
|
3390
|
+
const files = (await readdir2(path28.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
3354
3391
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
3355
3392
|
}
|
|
3356
3393
|
} else {
|
|
@@ -3568,13 +3605,20 @@ function summarize(name, t0, payload, notes) {
|
|
|
3568
3605
|
}
|
|
3569
3606
|
|
|
3570
3607
|
// src/commands/memory-suggest.ts
|
|
3608
|
+
import { mkdir as mkdir10, writeFile as writeFile16 } from "fs/promises";
|
|
3609
|
+
import { existsSync as existsSync30 } from "fs";
|
|
3610
|
+
import path29 from "path";
|
|
3571
3611
|
import "commander";
|
|
3572
3612
|
import {
|
|
3573
3613
|
aggregateUsage as aggregateUsage2,
|
|
3614
|
+
buildFrontmatter as buildFrontmatter6,
|
|
3574
3615
|
findProjectRoot as findProjectRoot34,
|
|
3616
|
+
loadMemoriesFromDir as loadMemoriesFromDir8,
|
|
3617
|
+
memoryFilePath as memoryFilePath5,
|
|
3575
3618
|
parseSince as parseSince2,
|
|
3576
3619
|
readUsageEvents as readUsageEvents2,
|
|
3577
|
-
resolveHaivePaths as resolveHaivePaths31
|
|
3620
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
3621
|
+
serializeMemory as serializeMemory13
|
|
3578
3622
|
} from "@hiveai/core";
|
|
3579
3623
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
3580
3624
|
"mem_search",
|
|
@@ -3583,7 +3627,9 @@ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
3583
3627
|
"get_briefing"
|
|
3584
3628
|
]);
|
|
3585
3629
|
function registerMemorySuggest(memory2) {
|
|
3586
|
-
memory2.command("suggest").description(
|
|
3630
|
+
memory2.command("suggest").description(
|
|
3631
|
+
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
|
|
3632
|
+
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3587
3633
|
const root = findProjectRoot34(opts.dir);
|
|
3588
3634
|
const paths = resolveHaivePaths31(root);
|
|
3589
3635
|
const events = await readUsageEvents2(paths);
|
|
@@ -3618,8 +3664,67 @@ function registerMemorySuggest(memory2) {
|
|
|
3618
3664
|
count: v.count,
|
|
3619
3665
|
tools: [...v.tools].sort(),
|
|
3620
3666
|
last_used: v.last,
|
|
3621
|
-
reason: chooseReason(v.tools, v.count)
|
|
3667
|
+
reason: chooseReason(v.tools, v.count),
|
|
3668
|
+
inferred_type: inferType(v.tools, query)
|
|
3622
3669
|
})).sort((a, b) => b.count - a.count);
|
|
3670
|
+
if (opts.autoSave) {
|
|
3671
|
+
const topN = Math.max(1, parseInt(opts.topN ?? "3", 10));
|
|
3672
|
+
const scope = opts.scope === "team" ? "team" : "personal";
|
|
3673
|
+
const top = suggestions.slice(0, topN);
|
|
3674
|
+
if (top.length === 0) {
|
|
3675
|
+
ui.warn(`No suggestions met --min=${minCount} \u2014 nothing to draft.`);
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
const created = [];
|
|
3679
|
+
const skipped = [];
|
|
3680
|
+
const existing = existsSync30(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
|
|
3681
|
+
for (const s of top) {
|
|
3682
|
+
const slug = slugify(s.query);
|
|
3683
|
+
if (!slug) {
|
|
3684
|
+
skipped.push({ query: s.query, reason: "could not derive a slug" });
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
const dup = existing.find(({ memory: memory3 }) => memory3.frontmatter.id.endsWith(`-${slug}`));
|
|
3688
|
+
if (dup) {
|
|
3689
|
+
skipped.push({ query: s.query, reason: `similar memory already exists (${dup.memory.frontmatter.id})` });
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
const fm = buildFrontmatter6({
|
|
3693
|
+
type: s.inferred_type,
|
|
3694
|
+
slug,
|
|
3695
|
+
scope,
|
|
3696
|
+
tags: ["auto-suggested", ...s.tools],
|
|
3697
|
+
paths: [],
|
|
3698
|
+
symbols: []
|
|
3699
|
+
});
|
|
3700
|
+
fm.status = "draft";
|
|
3701
|
+
const body = renderTemplate(s);
|
|
3702
|
+
const file = memoryFilePath5(paths, fm.scope, fm.id, fm.module);
|
|
3703
|
+
await mkdir10(path29.dirname(file), { recursive: true });
|
|
3704
|
+
if (existsSync30(file)) {
|
|
3705
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path29.relative(root, file)}` });
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
await writeFile16(file, serializeMemory13({ frontmatter: fm, body }), "utf8");
|
|
3709
|
+
created.push({ id: fm.id, file: path29.relative(root, file), query: s.query });
|
|
3710
|
+
}
|
|
3711
|
+
if (opts.json) {
|
|
3712
|
+
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
for (const c of created) {
|
|
3716
|
+
ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
|
|
3717
|
+
console.log(` ${ui.dim("from query:")} ${truncate(c.query, 60)}`);
|
|
3718
|
+
}
|
|
3719
|
+
for (const s of skipped) {
|
|
3720
|
+
ui.warn(`Skipped: ${truncate(s.query, 50)} \u2014 ${s.reason}`);
|
|
3721
|
+
}
|
|
3722
|
+
if (created.length > 0) {
|
|
3723
|
+
console.log();
|
|
3724
|
+
ui.info("Drafts are status=draft \u2014 edit them, then `haive memory promote` to validate.");
|
|
3725
|
+
}
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3623
3728
|
if (opts.json) {
|
|
3624
3729
|
console.log(JSON.stringify({ window: opts.since, suggestions }, null, 2));
|
|
3625
3730
|
return;
|
|
@@ -3640,6 +3745,8 @@ function registerMemorySuggest(memory2) {
|
|
|
3640
3745
|
);
|
|
3641
3746
|
console.log(` ${ui.dim("\u2192")} ${s.reason}`);
|
|
3642
3747
|
}
|
|
3748
|
+
console.log();
|
|
3749
|
+
ui.info("Run with --auto-save to draft the top-3 as draft memories.");
|
|
3643
3750
|
});
|
|
3644
3751
|
}
|
|
3645
3752
|
function chooseReason(tools, count) {
|
|
@@ -3651,14 +3758,594 @@ function chooseReason(tools, count) {
|
|
|
3651
3758
|
}
|
|
3652
3759
|
return `${count} agents asked the briefing for this \u2014 consider promoting the answer to a team memory.`;
|
|
3653
3760
|
}
|
|
3761
|
+
function inferType(tools, query) {
|
|
3762
|
+
const q = query.toLowerCase();
|
|
3763
|
+
if (q.includes("bug") || q.includes("error") || q.includes("crash") || q.includes("trap")) return "gotcha";
|
|
3764
|
+
if (q.includes("decid") || q.includes("why") || q.includes("choose") || q.includes("vs ")) return "decision";
|
|
3765
|
+
if (tools.has("code_search") && (q.includes("where") || q.includes("location") || q.includes("structure"))) {
|
|
3766
|
+
return "architecture";
|
|
3767
|
+
}
|
|
3768
|
+
return "convention";
|
|
3769
|
+
}
|
|
3770
|
+
function renderTemplate(s) {
|
|
3771
|
+
return [
|
|
3772
|
+
`# Auto-drafted from recurring searches`,
|
|
3773
|
+
``,
|
|
3774
|
+
`> This memory was drafted by \`haive memory suggest --auto-save\` because`,
|
|
3775
|
+
`> agents searched for this **${s.count} times** in the recent window`,
|
|
3776
|
+
`> via ${s.tools.join(", ")}.`,
|
|
3777
|
+
``,
|
|
3778
|
+
`## Query`,
|
|
3779
|
+
``,
|
|
3780
|
+
`> ${s.query}`,
|
|
3781
|
+
``,
|
|
3782
|
+
`## What to fill in`,
|
|
3783
|
+
``,
|
|
3784
|
+
`Replace this section with the actual answer the team keeps re-discovering:`,
|
|
3785
|
+
``,
|
|
3786
|
+
`- **What** \u2014 the convention / decision / gotcha (1-3 sentences)`,
|
|
3787
|
+
`- **Why** \u2014 the rationale or root cause`,
|
|
3788
|
+
`- **How to apply** \u2014 what an agent should do when this comes up again`,
|
|
3789
|
+
``,
|
|
3790
|
+
`Then run \`haive memory promote ${truncate(s.query, 30)}\` to mark it validated.`
|
|
3791
|
+
].join("\n");
|
|
3792
|
+
}
|
|
3793
|
+
function slugify(s) {
|
|
3794
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
3795
|
+
}
|
|
3654
3796
|
function truncate(text, max) {
|
|
3655
3797
|
if (text.length <= max) return text;
|
|
3656
3798
|
return text.slice(0, max - 1) + "\u2026";
|
|
3657
3799
|
}
|
|
3658
3800
|
|
|
3801
|
+
// src/commands/memory-archive.ts
|
|
3802
|
+
import { existsSync as existsSync31 } from "fs";
|
|
3803
|
+
import { writeFile as writeFile17 } from "fs/promises";
|
|
3804
|
+
import path30 from "path";
|
|
3805
|
+
import "commander";
|
|
3806
|
+
import {
|
|
3807
|
+
findProjectRoot as findProjectRoot35,
|
|
3808
|
+
getUsage as getUsage9,
|
|
3809
|
+
loadMemoriesFromDir as loadMemoriesFromDir9,
|
|
3810
|
+
loadUsageIndex as loadUsageIndex11,
|
|
3811
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
3812
|
+
serializeMemory as serializeMemory14
|
|
3813
|
+
} from "@hiveai/core";
|
|
3814
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3815
|
+
function registerMemoryArchive(memory2) {
|
|
3816
|
+
memory2.command("archive").description(
|
|
3817
|
+
"Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
|
|
3818
|
+
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3819
|
+
const root = findProjectRoot35(opts.dir);
|
|
3820
|
+
const paths = resolveHaivePaths32(root);
|
|
3821
|
+
if (!existsSync31(paths.memoriesDir)) {
|
|
3822
|
+
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
3823
|
+
process.exitCode = 1;
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
const minDays = parseDays(opts.since ?? "180d");
|
|
3827
|
+
if (minDays === null) {
|
|
3828
|
+
ui.error(`Invalid --since value: ${opts.since}. Use formats like '180d', '6m', '1y'.`);
|
|
3829
|
+
process.exitCode = 1;
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
const cutoff = Date.now() - minDays * MS_PER_DAY;
|
|
3833
|
+
const all = await loadMemoriesFromDir9(paths.memoriesDir);
|
|
3834
|
+
const usage = await loadUsageIndex11(paths);
|
|
3835
|
+
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
3836
|
+
const candidates = [];
|
|
3837
|
+
for (const { memory: mem, filePath } of all) {
|
|
3838
|
+
const fm = mem.frontmatter;
|
|
3839
|
+
if (typeFilter && fm.type !== typeFilter) continue;
|
|
3840
|
+
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
3841
|
+
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
3842
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync31(path30.join(paths.root, p)));
|
|
3843
|
+
const isAnchorless = !hasAnyAnchor;
|
|
3844
|
+
if (!isAnchorless && !allPathsGone) continue;
|
|
3845
|
+
const u = getUsage9(usage, fm.id);
|
|
3846
|
+
const lastSeen = u.last_read_at ?? fm.created_at;
|
|
3847
|
+
if (Date.parse(lastSeen) >= cutoff) continue;
|
|
3848
|
+
candidates.push({
|
|
3849
|
+
id: fm.id,
|
|
3850
|
+
type: fm.type,
|
|
3851
|
+
status: fm.status,
|
|
3852
|
+
last_seen: lastSeen,
|
|
3853
|
+
reason: isAnchorless ? `anchorless and not read since ${lastSeen.slice(0, 10)}` : `all ${fm.anchor.paths.length} anchored path(s) missing and not read since ${lastSeen.slice(0, 10)}`,
|
|
3854
|
+
filePath
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
if (opts.json) {
|
|
3858
|
+
console.log(JSON.stringify({
|
|
3859
|
+
dry_run: !opts.apply,
|
|
3860
|
+
window_days: minDays,
|
|
3861
|
+
candidates: candidates.length,
|
|
3862
|
+
archived: opts.apply ? candidates.length : 0,
|
|
3863
|
+
items: candidates
|
|
3864
|
+
}, null, 2));
|
|
3865
|
+
} else {
|
|
3866
|
+
const header = opts.apply ? "Archiving" : "Would archive";
|
|
3867
|
+
console.log(ui.bold(`${header} ${candidates.length} memor${candidates.length === 1 ? "y" : "ies"} (older than ${minDays}d, type=${typeFilter ?? "all"})`));
|
|
3868
|
+
if (candidates.length === 0) {
|
|
3869
|
+
ui.info("Nothing to archive \u2014 all memories are anchored or read recently.");
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
for (const c of candidates) {
|
|
3873
|
+
console.log(` ${ui.dim(c.last_seen.slice(0, 10))} ${c.id} ${ui.dim(`(${c.type})`)} \u2014 ${c.reason}`);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
if (!opts.apply) {
|
|
3877
|
+
if (!opts.json) {
|
|
3878
|
+
console.log();
|
|
3879
|
+
ui.info("Dry run \u2014 pass --apply to mark these as deprecated on disk.");
|
|
3880
|
+
}
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
let archived = 0;
|
|
3884
|
+
let failed = 0;
|
|
3885
|
+
for (const c of candidates) {
|
|
3886
|
+
const found = all.find(({ filePath }) => filePath === c.filePath);
|
|
3887
|
+
if (!found) continue;
|
|
3888
|
+
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
3889
|
+
try {
|
|
3890
|
+
await writeFile17(c.filePath, serializeMemory14({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
3891
|
+
archived++;
|
|
3892
|
+
} catch (err) {
|
|
3893
|
+
if (!opts.json) {
|
|
3894
|
+
ui.error(`Failed to archive ${c.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3895
|
+
}
|
|
3896
|
+
failed++;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
if (!opts.json) {
|
|
3900
|
+
ui.success(`Archived ${archived} memor${archived === 1 ? "y" : "ies"}${failed > 0 ? ` (${failed} failed)` : ""}`);
|
|
3901
|
+
}
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
function parseDays(input) {
|
|
3905
|
+
const m = input.match(/^(\d+)([dmy])$/);
|
|
3906
|
+
if (!m) return null;
|
|
3907
|
+
const n = parseInt(m[1] ?? "0", 10);
|
|
3908
|
+
const unit = m[2] ?? "d";
|
|
3909
|
+
if (unit === "d") return n;
|
|
3910
|
+
if (unit === "m") return n * 30;
|
|
3911
|
+
if (unit === "y") return n * 365;
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
// src/commands/doctor.ts
|
|
3916
|
+
import { existsSync as existsSync32 } from "fs";
|
|
3917
|
+
import { stat } from "fs/promises";
|
|
3918
|
+
import "path";
|
|
3919
|
+
import "commander";
|
|
3920
|
+
import {
|
|
3921
|
+
codeMapPath as codeMapPath2,
|
|
3922
|
+
findProjectRoot as findProjectRoot36,
|
|
3923
|
+
getUsage as getUsage10,
|
|
3924
|
+
loadCodeMap as loadCodeMap3,
|
|
3925
|
+
loadConfig as loadConfig4,
|
|
3926
|
+
loadMemoriesFromDir as loadMemoriesFromDir10,
|
|
3927
|
+
loadUsageIndex as loadUsageIndex12,
|
|
3928
|
+
readUsageEvents as readUsageEvents3,
|
|
3929
|
+
resolveHaivePaths as resolveHaivePaths33
|
|
3930
|
+
} from "@hiveai/core";
|
|
3931
|
+
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
3932
|
+
function registerDoctor(program2) {
|
|
3933
|
+
program2.command("doctor").description(
|
|
3934
|
+
"Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
|
|
3935
|
+
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3936
|
+
const root = findProjectRoot36(opts.dir);
|
|
3937
|
+
const paths = resolveHaivePaths33(root);
|
|
3938
|
+
const findings = [];
|
|
3939
|
+
if (!existsSync32(paths.haiveDir)) {
|
|
3940
|
+
findings.push({
|
|
3941
|
+
severity: "error",
|
|
3942
|
+
code: "not-initialized",
|
|
3943
|
+
message: ".ai/ directory missing \u2014 haive is not initialized in this project.",
|
|
3944
|
+
fix: "haive init"
|
|
3945
|
+
});
|
|
3946
|
+
return emit(findings, opts);
|
|
3947
|
+
}
|
|
3948
|
+
if (!existsSync32(paths.projectContext)) {
|
|
3949
|
+
findings.push({
|
|
3950
|
+
severity: "warn",
|
|
3951
|
+
code: "no-project-context",
|
|
3952
|
+
message: ".ai/project-context.md is missing.",
|
|
3953
|
+
fix: "haive init"
|
|
3954
|
+
});
|
|
3955
|
+
} else {
|
|
3956
|
+
const { readFile: readFile10 } = await import("fs/promises");
|
|
3957
|
+
const content = await readFile10(paths.projectContext, "utf8");
|
|
3958
|
+
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
3959
|
+
if (isTemplate) {
|
|
3960
|
+
findings.push({
|
|
3961
|
+
severity: "warn",
|
|
3962
|
+
code: "template-context",
|
|
3963
|
+
message: "project-context.md still contains the default template \u2014 get_briefing returns little value until filled.",
|
|
3964
|
+
fix: "Invoke the bootstrap_project MCP prompt from your AI client."
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
const memories = existsSync32(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
|
|
3969
|
+
const now = Date.now();
|
|
3970
|
+
if (memories.length === 0) {
|
|
3971
|
+
findings.push({
|
|
3972
|
+
severity: "info",
|
|
3973
|
+
code: "no-memories",
|
|
3974
|
+
message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
|
|
3975
|
+
});
|
|
3976
|
+
} else {
|
|
3977
|
+
const usage = await loadUsageIndex12(paths);
|
|
3978
|
+
const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
|
|
3979
|
+
if (stale.length > 0) {
|
|
3980
|
+
findings.push({
|
|
3981
|
+
severity: "warn",
|
|
3982
|
+
code: "stale-memories",
|
|
3983
|
+
message: `${stale.length} memor${stale.length === 1 ? "y" : "ies"} marked stale (anchored code drifted).`,
|
|
3984
|
+
fix: "haive memory verify --update # re-check anchors\nhaive memory edit <id> # manually refresh body"
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
const proposed = memories.filter((m) => m.memory.frontmatter.status === "proposed");
|
|
3988
|
+
if (proposed.length > 0) {
|
|
3989
|
+
findings.push({
|
|
3990
|
+
severity: "info",
|
|
3991
|
+
code: "pending-review",
|
|
3992
|
+
message: `${proposed.length} memor${proposed.length === 1 ? "y is" : "ies are"} proposed and awaiting validation.`,
|
|
3993
|
+
fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
const anchorless = memories.filter(
|
|
3997
|
+
(m) => m.memory.frontmatter.anchor.paths.length === 0 && m.memory.frontmatter.anchor.symbols.length === 0 && m.memory.frontmatter.type !== "session_recap" && m.memory.frontmatter.type !== "glossary"
|
|
3998
|
+
);
|
|
3999
|
+
if (anchorless.length / Math.max(memories.length, 1) > 0.3) {
|
|
4000
|
+
findings.push({
|
|
4001
|
+
severity: "warn",
|
|
4002
|
+
code: "anchorless-majority",
|
|
4003
|
+
message: `${anchorless.length}/${memories.length} memories have no anchor path/symbol \u2014 staleness undetectable.`,
|
|
4004
|
+
fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
const decayCandidates = memories.filter((m) => {
|
|
4008
|
+
if (m.memory.frontmatter.status !== "validated") return false;
|
|
4009
|
+
const u = getUsage10(usage, m.memory.frontmatter.id);
|
|
4010
|
+
const last = u.last_read_at ?? m.memory.frontmatter.created_at;
|
|
4011
|
+
return (now - Date.parse(last)) / MS_PER_DAY2 > 180;
|
|
4012
|
+
});
|
|
4013
|
+
if (decayCandidates.length > 0) {
|
|
4014
|
+
findings.push({
|
|
4015
|
+
severity: "info",
|
|
4016
|
+
code: "decay-candidates",
|
|
4017
|
+
message: `${decayCandidates.length} validated memor${decayCandidates.length === 1 ? "y has" : "ies have"} not been read in 180+ days \u2014 confidence is decaying.`,
|
|
4018
|
+
fix: "haive memory archive --type all --since 365d # dry run"
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
const codeMap = await loadCodeMap3(paths);
|
|
4023
|
+
if (!codeMap) {
|
|
4024
|
+
findings.push({
|
|
4025
|
+
severity: "warn",
|
|
4026
|
+
code: "no-code-map",
|
|
4027
|
+
message: "No code-map found \u2014 code_map MCP tool and symbol_locations are unavailable.",
|
|
4028
|
+
fix: "haive index code"
|
|
4029
|
+
});
|
|
4030
|
+
} else {
|
|
4031
|
+
const cmFile = codeMapPath2(paths);
|
|
4032
|
+
const cmStat = await stat(cmFile);
|
|
4033
|
+
const ageDays = (now - cmStat.mtimeMs) / MS_PER_DAY2;
|
|
4034
|
+
if (ageDays > 14) {
|
|
4035
|
+
findings.push({
|
|
4036
|
+
severity: "warn",
|
|
4037
|
+
code: "stale-code-map",
|
|
4038
|
+
message: `code-map is ${Math.round(ageDays)} days old (${Object.keys(codeMap.files).length} files indexed).`,
|
|
4039
|
+
fix: "haive index code # or rely on the post-merge git hook"
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
const events = await readUsageEvents3(paths);
|
|
4044
|
+
if (events.length === 0) {
|
|
4045
|
+
findings.push({
|
|
4046
|
+
severity: "info",
|
|
4047
|
+
code: "no-usage-log",
|
|
4048
|
+
message: "No usage log entries \u2014 MCP server hasn't recorded any calls yet, or this project hasn't been used by an agent."
|
|
4049
|
+
});
|
|
4050
|
+
} else {
|
|
4051
|
+
const queryRepeats = /* @__PURE__ */ new Map();
|
|
4052
|
+
for (const e of events) {
|
|
4053
|
+
if (!isSearchTool(e.tool)) continue;
|
|
4054
|
+
const key = (e.summary ?? "").toLowerCase().trim();
|
|
4055
|
+
if (!key) continue;
|
|
4056
|
+
queryRepeats.set(key, (queryRepeats.get(key) ?? 0) + 1);
|
|
4057
|
+
}
|
|
4058
|
+
const repeated = [...queryRepeats.entries()].filter(([, n]) => n >= 3);
|
|
4059
|
+
if (repeated.length > 0) {
|
|
4060
|
+
findings.push({
|
|
4061
|
+
severity: "info",
|
|
4062
|
+
code: "recurring-searches",
|
|
4063
|
+
message: `${repeated.length} query${repeated.length === 1 ? "" : "ies"} repeated 3+ times \u2014 agents keep asking the same things.`,
|
|
4064
|
+
fix: `haive memory suggest --auto-save --top-n ${Math.min(5, repeated.length)}`
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
const codeMapCalls = events.filter((e) => e.tool === "code_map").length;
|
|
4068
|
+
const briefingCalls = events.filter((e) => e.tool === "get_briefing").length;
|
|
4069
|
+
if (codeMapCalls > 0 && memories.length > 0) {
|
|
4070
|
+
findings.push({
|
|
4071
|
+
severity: "info",
|
|
4072
|
+
code: "tool-mix",
|
|
4073
|
+
message: `${briefingCalls} get_briefing call${briefingCalls === 1 ? "" : "s"}, ${codeMapCalls} code_map call${codeMapCalls === 1 ? "" : "s"} recorded.`
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
const config = await loadConfig4(paths);
|
|
4078
|
+
if (!config.autoSessionEnd) {
|
|
4079
|
+
findings.push({
|
|
4080
|
+
severity: "info",
|
|
4081
|
+
code: "no-autopilot",
|
|
4082
|
+
message: "Autopilot is OFF \u2014 session recaps are not auto-saved on shutdown.",
|
|
4083
|
+
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
emit(findings, opts);
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
function emit(findings, opts) {
|
|
4090
|
+
if (opts.json) {
|
|
4091
|
+
console.log(JSON.stringify({ findings }, null, 2));
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
if (findings.length === 0) {
|
|
4095
|
+
ui.success("hAIve doctor \u2014 no issues found.");
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
console.log(ui.bold(`hAIve doctor \u2014 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
|
|
4099
|
+
console.log();
|
|
4100
|
+
const order = ["error", "warn", "info"];
|
|
4101
|
+
for (const sev of order) {
|
|
4102
|
+
for (const f of findings.filter((x) => x.severity === sev)) {
|
|
4103
|
+
const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
|
|
4104
|
+
console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
|
|
4105
|
+
if (opts.fix && f.fix) {
|
|
4106
|
+
for (const line of f.fix.split("\n")) {
|
|
4107
|
+
console.log(` ${ui.dim("$")} ${line}`);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (!opts.fix && findings.some((f) => f.fix)) {
|
|
4113
|
+
console.log();
|
|
4114
|
+
ui.info("Re-run with --fix to see suggested commands.");
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
function isSearchTool(name) {
|
|
4118
|
+
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// src/commands/playback.ts
|
|
4122
|
+
import { existsSync as existsSync33 } from "fs";
|
|
4123
|
+
import "commander";
|
|
4124
|
+
import {
|
|
4125
|
+
findProjectRoot as findProjectRoot37,
|
|
4126
|
+
loadMemoriesFromDir as loadMemoriesFromDir11,
|
|
4127
|
+
parseSince as parseSince3,
|
|
4128
|
+
readUsageEvents as readUsageEvents4,
|
|
4129
|
+
resolveHaivePaths as resolveHaivePaths34
|
|
4130
|
+
} from "@hiveai/core";
|
|
4131
|
+
var MS_PER_MINUTE = 6e4;
|
|
4132
|
+
function registerPlayback(program2) {
|
|
4133
|
+
program2.command("playback").description(
|
|
4134
|
+
"Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
|
|
4135
|
+
).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4136
|
+
const root = findProjectRoot37(opts.dir);
|
|
4137
|
+
const paths = resolveHaivePaths34(root);
|
|
4138
|
+
const events = await readUsageEvents4(paths);
|
|
4139
|
+
if (events.length === 0) {
|
|
4140
|
+
if (opts.json) {
|
|
4141
|
+
console.log(JSON.stringify({ sessions: [] }));
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
ui.warn("No usage log entries yet.");
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
const since = parseSince3(opts.since);
|
|
4148
|
+
const cutoff = since ? since.getTime() : 0;
|
|
4149
|
+
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
4150
|
+
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
4151
|
+
const sessions = bucketSessions(filtered, gapMs);
|
|
4152
|
+
const all = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
|
|
4153
|
+
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
4154
|
+
const enriched = sessions.map((s, i) => {
|
|
4155
|
+
const startMs = Date.parse(s.start);
|
|
4156
|
+
const newer = memByCreatedAt.filter((m) => m.at > startMs);
|
|
4157
|
+
return {
|
|
4158
|
+
index: i,
|
|
4159
|
+
start: s.start,
|
|
4160
|
+
end: s.end,
|
|
4161
|
+
duration_minutes: (Date.parse(s.end) - startMs) / MS_PER_MINUTE,
|
|
4162
|
+
events: s.events.length,
|
|
4163
|
+
tools_count: countTools(s.events),
|
|
4164
|
+
briefing_tasks: s.events.filter((e) => e.tool === "get_briefing" && e.summary).map((e) => e.summary).slice(0, 5),
|
|
4165
|
+
memories_created_since: newer.length,
|
|
4166
|
+
new_memories: newer.slice(0, 5).map((m) => m.id)
|
|
4167
|
+
};
|
|
4168
|
+
});
|
|
4169
|
+
enriched.sort((a, b) => Date.parse(b.start) - Date.parse(a.start));
|
|
4170
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "10", 10));
|
|
4171
|
+
const shown = enriched.slice(0, limit);
|
|
4172
|
+
if (opts.json) {
|
|
4173
|
+
console.log(JSON.stringify({
|
|
4174
|
+
window: opts.since,
|
|
4175
|
+
session_gap_minutes: gapMs / MS_PER_MINUTE,
|
|
4176
|
+
total_sessions: enriched.length,
|
|
4177
|
+
sessions: shown
|
|
4178
|
+
}, null, 2));
|
|
4179
|
+
return;
|
|
4180
|
+
}
|
|
4181
|
+
console.log(ui.bold(`hAIve playback \u2014 ${enriched.length} session(s) over ${opts.since ?? "all time"}`));
|
|
4182
|
+
console.log();
|
|
4183
|
+
for (const s of shown) {
|
|
4184
|
+
console.log(
|
|
4185
|
+
`${ui.bold(`Session ${s.index + 1}`)} ${ui.dim(s.start.slice(0, 19) + " \u2192 " + s.end.slice(11, 19))} ${ui.dim(`(${Math.round(s.duration_minutes)}m, ${s.events} call${s.events === 1 ? "" : "s"})`)}`
|
|
4186
|
+
);
|
|
4187
|
+
const toolList = Object.entries(s.tools_count).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, n]) => `${t}\xD7${n}`).join(", ");
|
|
4188
|
+
if (toolList) console.log(` ${ui.dim("tools:")} ${toolList}`);
|
|
4189
|
+
if (s.briefing_tasks.length > 0) {
|
|
4190
|
+
console.log(` ${ui.dim("briefings asked:")}`);
|
|
4191
|
+
for (const t of s.briefing_tasks) {
|
|
4192
|
+
console.log(` \u2022 ${truncate2(t, 80)}`);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
if (s.memories_created_since > 0) {
|
|
4196
|
+
console.log(
|
|
4197
|
+
` ${ui.green("\u2934")} ${s.memories_created_since} memor${s.memories_created_since === 1 ? "y has" : "ies have"} been created since this session ` + ui.dim(`\u2014 newer haive could have answered better`)
|
|
4198
|
+
);
|
|
4199
|
+
for (const id of s.new_memories) {
|
|
4200
|
+
console.log(` + ${ui.dim(id)}`);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
console.log();
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
function bucketSessions(events, gapMs) {
|
|
4208
|
+
if (events.length === 0) return [];
|
|
4209
|
+
const sorted = [...events].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
4210
|
+
const buckets = [];
|
|
4211
|
+
let current = null;
|
|
4212
|
+
for (const e of sorted) {
|
|
4213
|
+
if (!current) {
|
|
4214
|
+
current = { start: e.at, end: e.at, events: [e] };
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
if (Date.parse(e.at) - Date.parse(current.end) > gapMs) {
|
|
4218
|
+
buckets.push(current);
|
|
4219
|
+
current = { start: e.at, end: e.at, events: [e] };
|
|
4220
|
+
} else {
|
|
4221
|
+
current.events.push(e);
|
|
4222
|
+
current.end = e.at;
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (current) buckets.push(current);
|
|
4226
|
+
return buckets;
|
|
4227
|
+
}
|
|
4228
|
+
function countTools(events) {
|
|
4229
|
+
const out = {};
|
|
4230
|
+
for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
|
|
4231
|
+
return out;
|
|
4232
|
+
}
|
|
4233
|
+
function truncate2(text, max) {
|
|
4234
|
+
if (text.length <= max) return text;
|
|
4235
|
+
return text.slice(0, max - 1) + "\u2026";
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
// src/commands/precommit.ts
|
|
4239
|
+
import { spawn as spawn3 } from "child_process";
|
|
4240
|
+
import "commander";
|
|
4241
|
+
import {
|
|
4242
|
+
findProjectRoot as findProjectRoot38,
|
|
4243
|
+
resolveHaivePaths as resolveHaivePaths35
|
|
4244
|
+
} from "@hiveai/core";
|
|
4245
|
+
import { preCommitCheck } from "@hiveai/mcp";
|
|
4246
|
+
function registerPrecommit(program2) {
|
|
4247
|
+
program2.command("precommit").description(
|
|
4248
|
+
"Run a pre-commit safety check: scans `git diff --cached` against known anti-patterns,\n surfaces conventions/decisions anchored to touched files, and warns about stale anchored memories.\n\n Wire it into git as: `.git/hooks/pre-commit` running `haive precommit` (exit 1 = block).\n\n Examples:\n haive precommit # auto-detects staged diff\n haive precommit --block-on any # block on any warning, not just high-confidence\n haive precommit --paths src/auth.ts src/db.ts # explicit paths instead of git diff"
|
|
4249
|
+
).option(
|
|
4250
|
+
"--block-on <mode>",
|
|
4251
|
+
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
4252
|
+
"high-confidence"
|
|
4253
|
+
).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4254
|
+
const root = findProjectRoot38(opts.dir);
|
|
4255
|
+
const paths = resolveHaivePaths35(root);
|
|
4256
|
+
const ctx = { paths };
|
|
4257
|
+
let diff = "";
|
|
4258
|
+
let touchedPaths = opts.paths ?? [];
|
|
4259
|
+
if (touchedPaths.length === 0) {
|
|
4260
|
+
try {
|
|
4261
|
+
diff = await runCommand("git", ["diff", "--cached"], root);
|
|
4262
|
+
if (!diff.trim()) {
|
|
4263
|
+
ui.warn("No staged changes \u2014 nothing to check. Stage with `git add` first.");
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
const nameOnly = await runCommand("git", ["diff", "--cached", "--name-only"], root);
|
|
4267
|
+
touchedPaths = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
4268
|
+
} catch (err) {
|
|
4269
|
+
ui.error(`git diff failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4270
|
+
process.exit(1);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
const result = await preCommitCheck({
|
|
4274
|
+
diff: diff || void 0,
|
|
4275
|
+
paths: touchedPaths,
|
|
4276
|
+
block_on: opts.blockOn ?? "high-confidence",
|
|
4277
|
+
semantic: opts.noSemantic ? false : true
|
|
4278
|
+
}, ctx);
|
|
4279
|
+
if (opts.json) {
|
|
4280
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4281
|
+
process.exit(result.should_block ? 1 : 0);
|
|
4282
|
+
}
|
|
4283
|
+
console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
|
|
4284
|
+
console.log(
|
|
4285
|
+
ui.dim(
|
|
4286
|
+
` anti-patterns: ${result.summary.anti_patterns} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
|
|
4287
|
+
)
|
|
4288
|
+
);
|
|
4289
|
+
console.log();
|
|
4290
|
+
if (result.warnings.length > 0) {
|
|
4291
|
+
console.log(ui.bold("\u26A0 Anti-patterns matched:"));
|
|
4292
|
+
for (const w of result.warnings.slice(0, 10)) {
|
|
4293
|
+
console.log(` ${ui.yellow("\u26A0")} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
4294
|
+
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
4295
|
+
console.log(` ${ui.dim(line)}`);
|
|
4296
|
+
}
|
|
4297
|
+
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
4298
|
+
}
|
|
4299
|
+
console.log();
|
|
4300
|
+
}
|
|
4301
|
+
if (result.relevant_memories.length > 0) {
|
|
4302
|
+
console.log(ui.bold("\u{1F4CC} Relevant conventions/decisions:"));
|
|
4303
|
+
for (const m of result.relevant_memories) {
|
|
4304
|
+
console.log(` \u2022 ${m.id} ${ui.dim(`(${m.type}, ${m.confidence})`)}`);
|
|
4305
|
+
}
|
|
4306
|
+
console.log();
|
|
4307
|
+
}
|
|
4308
|
+
if (result.stale_anchors.length > 0) {
|
|
4309
|
+
console.log(ui.bold("\u{1F552} Stale anchored memories:"));
|
|
4310
|
+
for (const s of result.stale_anchors) {
|
|
4311
|
+
console.log(` \u2022 ${s.id}`);
|
|
4312
|
+
if (s.body_preview) console.log(` ${ui.dim(s.body_preview)}`);
|
|
4313
|
+
}
|
|
4314
|
+
console.log();
|
|
4315
|
+
}
|
|
4316
|
+
if (result.should_block) {
|
|
4317
|
+
ui.error(`Blocking commit (block_on=${opts.blockOn ?? "high-confidence"}). Address the warnings above or pass --block-on never to bypass.`);
|
|
4318
|
+
process.exit(1);
|
|
4319
|
+
}
|
|
4320
|
+
if (result.warnings.length === 0 && result.stale_anchors.length === 0) {
|
|
4321
|
+
ui.success("No anti-patterns or stale anchors found.");
|
|
4322
|
+
} else {
|
|
4323
|
+
ui.success("Check passed (block_on threshold not met).");
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
function runCommand(cmd, args, cwd) {
|
|
4328
|
+
return new Promise((resolve, reject) => {
|
|
4329
|
+
const proc = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
4330
|
+
let stdout = "";
|
|
4331
|
+
let stderr = "";
|
|
4332
|
+
proc.stdout.on("data", (chunk) => {
|
|
4333
|
+
stdout += chunk.toString();
|
|
4334
|
+
});
|
|
4335
|
+
proc.stderr.on("data", (chunk) => {
|
|
4336
|
+
stderr += chunk.toString();
|
|
4337
|
+
});
|
|
4338
|
+
proc.on("error", reject);
|
|
4339
|
+
proc.on("close", (code) => {
|
|
4340
|
+
if (code === 0) resolve(stdout);
|
|
4341
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
4342
|
+
});
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
|
|
3659
4346
|
// src/index.ts
|
|
3660
|
-
var program = new
|
|
3661
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
4347
|
+
var program = new Command39();
|
|
4348
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.6.0");
|
|
3662
4349
|
registerInit(program);
|
|
3663
4350
|
registerMcp(program);
|
|
3664
4351
|
registerBriefing(program);
|
|
@@ -3689,12 +4376,16 @@ registerMemoryImport(memory);
|
|
|
3689
4376
|
registerMemoryImportChangelog(memory);
|
|
3690
4377
|
registerMemoryDigest(memory);
|
|
3691
4378
|
registerMemorySuggest(memory);
|
|
4379
|
+
registerMemoryArchive(memory);
|
|
3692
4380
|
var session = program.command("session").description("Manage session lifecycle");
|
|
3693
4381
|
registerSessionEnd(session);
|
|
3694
4382
|
registerSnapshot(program);
|
|
3695
4383
|
registerHub(program);
|
|
3696
4384
|
registerStats(program);
|
|
3697
4385
|
registerBench(program);
|
|
4386
|
+
registerDoctor(program);
|
|
4387
|
+
registerPlayback(program);
|
|
4388
|
+
registerPrecommit(program);
|
|
3698
4389
|
program.parseAsync(process.argv).catch((err) => {
|
|
3699
4390
|
if (isZodError(err)) {
|
|
3700
4391
|
for (const issue of err.issues) {
|