@hiveai/cli 0.4.5 → 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 +1130 -124
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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,15 +375,42 @@ 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
|
});
|
|
381
|
+
idx.command("code-search").description(
|
|
382
|
+
"Build the semantic-search embeddings index for code (powers the code_search MCP tool).\n\n Reads .ai/code-map.json (run `haive index code` first) and embeds each exported\n symbol's metadata (filename + name + kind + description).\n\n Re-runs are incremental: unchanged entries keep their cached vectors, only the\n diff is re-embedded. First run downloads the bge-small-en-v1.5 model (~110MB).\n"
|
|
383
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
384
|
+
const root = findProjectRoot4(opts.dir);
|
|
385
|
+
const paths = resolveHaivePaths3(root);
|
|
386
|
+
let mod;
|
|
387
|
+
try {
|
|
388
|
+
mod = await import("@hiveai/embeddings");
|
|
389
|
+
} catch {
|
|
390
|
+
ui.error(
|
|
391
|
+
"@hiveai/embeddings is not installed. Install it (`pnpm add @hiveai/embeddings`) or run `haive embeddings install`."
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
ui.info("Loading embedder (first run downloads ~110MB)\u2026");
|
|
396
|
+
const embedder = await mod.Embedder.create();
|
|
397
|
+
ui.info(`Embedding code-map symbols\u2026`);
|
|
398
|
+
try {
|
|
399
|
+
const { report } = await mod.rebuildCodeIndex(paths, embedder);
|
|
400
|
+
ui.success(
|
|
401
|
+
`Code-search index ready: ${report.total} symbols (+${report.added} new, ~${report.updated} updated, =${report.unchanged} cached, -${report.removed} removed)`
|
|
402
|
+
);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
344
408
|
}
|
|
345
409
|
|
|
346
410
|
// src/commands/init.ts
|
|
347
411
|
import { mkdir, writeFile } from "fs/promises";
|
|
348
412
|
import { existsSync as existsSync3 } from "fs";
|
|
349
|
-
import
|
|
413
|
+
import path4 from "path";
|
|
350
414
|
import { spawnSync } from "child_process";
|
|
351
415
|
import "commander";
|
|
352
416
|
import {
|
|
@@ -517,7 +581,7 @@ function registerInit(program2) {
|
|
|
517
581
|
"--manual",
|
|
518
582
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
519
583
|
).action(async (opts) => {
|
|
520
|
-
const root =
|
|
584
|
+
const root = path4.resolve(opts.dir);
|
|
521
585
|
const paths = resolveHaivePaths4(root);
|
|
522
586
|
const autopilot = opts.manual !== true;
|
|
523
587
|
if (existsSync3(paths.haiveDir)) {
|
|
@@ -529,10 +593,10 @@ function registerInit(program2) {
|
|
|
529
593
|
await mkdir(paths.modulesContextDir, { recursive: true });
|
|
530
594
|
if (!existsSync3(paths.projectContext)) {
|
|
531
595
|
await writeFile(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
532
|
-
ui.success(`Created ${
|
|
596
|
+
ui.success(`Created ${path4.relative(root, paths.projectContext)}`);
|
|
533
597
|
}
|
|
534
598
|
const configExists = existsSync3(
|
|
535
|
-
|
|
599
|
+
path4.join(paths.haiveDir, "haive.config.json")
|
|
536
600
|
);
|
|
537
601
|
if (!configExists) {
|
|
538
602
|
await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
|
|
@@ -543,17 +607,17 @@ function registerInit(program2) {
|
|
|
543
607
|
if (opts.bridges) {
|
|
544
608
|
await writeBridge(root, "CLAUDE.md");
|
|
545
609
|
await writeBridge(root, ".cursorrules");
|
|
546
|
-
await writeBridge(root,
|
|
610
|
+
await writeBridge(root, path4.join(".github", "copilot-instructions.md"));
|
|
547
611
|
}
|
|
548
612
|
const wantCi = opts.withCi || autopilot;
|
|
549
613
|
if (wantCi) {
|
|
550
|
-
const ciPath =
|
|
614
|
+
const ciPath = path4.join(root, ".github", "workflows", "haive-sync.yml");
|
|
551
615
|
if (existsSync3(ciPath)) {
|
|
552
616
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
553
617
|
} else {
|
|
554
|
-
await mkdir(
|
|
618
|
+
await mkdir(path4.dirname(ciPath), { recursive: true });
|
|
555
619
|
await writeFile(ciPath, CI_WORKFLOW, "utf8");
|
|
556
|
-
ui.success(`Created ${
|
|
620
|
+
ui.success(`Created ${path4.relative(root, ciPath)}`);
|
|
557
621
|
}
|
|
558
622
|
}
|
|
559
623
|
if (autopilot) {
|
|
@@ -608,12 +672,12 @@ function registerInit(program2) {
|
|
|
608
672
|
});
|
|
609
673
|
}
|
|
610
674
|
async function writeBridge(root, relPath) {
|
|
611
|
-
const target =
|
|
675
|
+
const target = path4.join(root, relPath);
|
|
612
676
|
if (existsSync3(target)) {
|
|
613
677
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
614
678
|
return;
|
|
615
679
|
}
|
|
616
|
-
await mkdir(
|
|
680
|
+
await mkdir(path4.dirname(target), { recursive: true });
|
|
617
681
|
await writeFile(target, BRIDGE_BODY, "utf8");
|
|
618
682
|
ui.success(`Created bridge ${relPath}`);
|
|
619
683
|
}
|
|
@@ -621,7 +685,7 @@ async function writeBridge(root, relPath) {
|
|
|
621
685
|
// src/commands/install-hooks.ts
|
|
622
686
|
import { mkdir as mkdir2, writeFile as writeFile2, chmod, readFile as readFile2 } from "fs/promises";
|
|
623
687
|
import { existsSync as existsSync4 } from "fs";
|
|
624
|
-
import
|
|
688
|
+
import path5 from "path";
|
|
625
689
|
import "commander";
|
|
626
690
|
import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
627
691
|
var HOOK_MARKER = "# hAIve auto-generated";
|
|
@@ -642,18 +706,18 @@ function registerInstallHooks(program2) {
|
|
|
642
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"
|
|
643
707
|
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
644
708
|
const root = findProjectRoot6(opts.dir);
|
|
645
|
-
const gitDir =
|
|
709
|
+
const gitDir = path5.join(root, ".git");
|
|
646
710
|
if (!existsSync4(gitDir)) {
|
|
647
711
|
ui.error(`No .git directory at ${root}.`);
|
|
648
712
|
process.exitCode = 1;
|
|
649
713
|
return;
|
|
650
714
|
}
|
|
651
|
-
const hooksDir =
|
|
715
|
+
const hooksDir = path5.join(gitDir, "hooks");
|
|
652
716
|
await mkdir2(hooksDir, { recursive: true });
|
|
653
717
|
let installed = 0;
|
|
654
718
|
let skipped = 0;
|
|
655
719
|
for (const name of HOOKS) {
|
|
656
|
-
const file =
|
|
720
|
+
const file = path5.join(hooksDir, name);
|
|
657
721
|
if (existsSync4(file) && !opts.force) {
|
|
658
722
|
const existing = await readFile2(file, "utf8");
|
|
659
723
|
if (!existing.includes(HOOK_MARKER)) {
|
|
@@ -675,7 +739,7 @@ function registerInstallHooks(program2) {
|
|
|
675
739
|
import { spawn } from "child_process";
|
|
676
740
|
import { existsSync as existsSync5 } from "fs";
|
|
677
741
|
import { createRequire } from "module";
|
|
678
|
-
import
|
|
742
|
+
import path6 from "path";
|
|
679
743
|
import { fileURLToPath } from "url";
|
|
680
744
|
import "commander";
|
|
681
745
|
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
@@ -713,13 +777,13 @@ function registerMcp(program2) {
|
|
|
713
777
|
function locateMcpBin() {
|
|
714
778
|
try {
|
|
715
779
|
const pkgPath = require2.resolve("@hiveai/mcp/package.json");
|
|
716
|
-
const pkgDir =
|
|
717
|
-
const candidate =
|
|
780
|
+
const pkgDir = path6.dirname(pkgPath);
|
|
781
|
+
const candidate = path6.join(pkgDir, "dist", "index.js");
|
|
718
782
|
if (existsSync5(candidate)) return candidate;
|
|
719
783
|
} catch {
|
|
720
784
|
}
|
|
721
|
-
const here =
|
|
722
|
-
const sibling =
|
|
785
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
786
|
+
const sibling = path6.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
|
|
723
787
|
if (existsSync5(sibling)) return sibling;
|
|
724
788
|
return null;
|
|
725
789
|
}
|
|
@@ -728,7 +792,7 @@ function locateMcpBin() {
|
|
|
728
792
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
729
793
|
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
730
794
|
import { existsSync as existsSync6 } from "fs";
|
|
731
|
-
import
|
|
795
|
+
import path7 from "path";
|
|
732
796
|
import "commander";
|
|
733
797
|
import {
|
|
734
798
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
@@ -895,7 +959,7 @@ function registerSync(program2) {
|
|
|
895
959
|
);
|
|
896
960
|
}
|
|
897
961
|
if (opts.injectBridge) {
|
|
898
|
-
const bridgeFile = opts.bridgeFile ?
|
|
962
|
+
const bridgeFile = opts.bridgeFile ? path7.resolve(opts.bridgeFile) : path7.join(root, "CLAUDE.md");
|
|
899
963
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
900
964
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
901
965
|
}
|
|
@@ -1001,10 +1065,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1001
1065
|
paths: [result.file],
|
|
1002
1066
|
topic: `dep-bump-${slugParts}`
|
|
1003
1067
|
});
|
|
1004
|
-
const teamDir =
|
|
1068
|
+
const teamDir = path7.join(paths.memoriesDir, "team");
|
|
1005
1069
|
await mkdir3(teamDir, { recursive: true });
|
|
1006
1070
|
await writeFile3(
|
|
1007
|
-
|
|
1071
|
+
path7.join(teamDir, `${fm.id}.md`),
|
|
1008
1072
|
serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1009
1073
|
"utf8"
|
|
1010
1074
|
);
|
|
@@ -1068,10 +1132,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1068
1132
|
paths: [diff.file],
|
|
1069
1133
|
topic: `contract-breaking-${diff.contract}`
|
|
1070
1134
|
});
|
|
1071
|
-
const teamDir =
|
|
1135
|
+
const teamDir = path7.join(paths.memoriesDir, "team");
|
|
1072
1136
|
await mkdir3(teamDir, { recursive: true });
|
|
1073
1137
|
await writeFile3(
|
|
1074
|
-
|
|
1138
|
+
path7.join(teamDir, `${fm.id}.md`),
|
|
1075
1139
|
serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1076
1140
|
"utf8"
|
|
1077
1141
|
);
|
|
@@ -1163,11 +1227,11 @@ ${BRIDGE_END}`;
|
|
|
1163
1227
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
1164
1228
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
1165
1229
|
if (startIdx !== -1 && endIdx === -1) {
|
|
1166
|
-
ui.warn(`${
|
|
1230
|
+
ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
1167
1231
|
return;
|
|
1168
1232
|
}
|
|
1169
1233
|
if (startIdx === -1 && endIdx !== -1) {
|
|
1170
|
-
ui.warn(`${
|
|
1234
|
+
ui.warn(`${path7.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
1171
1235
|
return;
|
|
1172
1236
|
}
|
|
1173
1237
|
let updated;
|
|
@@ -1175,14 +1239,14 @@ ${BRIDGE_END}`;
|
|
|
1175
1239
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
1176
1240
|
} else {
|
|
1177
1241
|
if (!fileExists && !quiet) {
|
|
1178
|
-
ui.info(`Creating ${
|
|
1242
|
+
ui.info(`Creating ${path7.relative(root, bridgeFile)} with haive memory block.`);
|
|
1179
1243
|
}
|
|
1180
1244
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
1181
1245
|
}
|
|
1182
1246
|
await writeFile3(bridgeFile, updated, "utf8");
|
|
1183
1247
|
if (!quiet) {
|
|
1184
1248
|
console.log(
|
|
1185
|
-
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)}`)
|
|
1186
1250
|
);
|
|
1187
1251
|
}
|
|
1188
1252
|
}
|
|
@@ -1209,7 +1273,7 @@ function collectSinceChanges(root, ref) {
|
|
|
1209
1273
|
import { createHash } from "crypto";
|
|
1210
1274
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1211
1275
|
import { existsSync as existsSync7 } from "fs";
|
|
1212
|
-
import
|
|
1276
|
+
import path8 from "path";
|
|
1213
1277
|
import "commander";
|
|
1214
1278
|
import {
|
|
1215
1279
|
buildFrontmatter as buildFrontmatter2,
|
|
@@ -1258,7 +1322,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1258
1322
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
|
|
1259
1323
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
1260
1324
|
if (anchorPaths.length > 0) {
|
|
1261
|
-
const missing = anchorPaths.filter((p) => !existsSync7(
|
|
1325
|
+
const missing = anchorPaths.filter((p) => !existsSync7(path8.resolve(root, p)));
|
|
1262
1326
|
if (missing.length > 0) {
|
|
1263
1327
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
1264
1328
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -1323,7 +1387,7 @@ TODO \u2014 write the memory body.
|
|
|
1323
1387
|
}
|
|
1324
1388
|
};
|
|
1325
1389
|
await writeFile4(topicMatch.filePath, serializeMemory2({ frontmatter: newFrontmatter, body }), "utf8");
|
|
1326
|
-
ui.success(`Updated (topic upsert) ${
|
|
1390
|
+
ui.success(`Updated (topic upsert) ${path8.relative(root, topicMatch.filePath)}`);
|
|
1327
1391
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
1328
1392
|
return;
|
|
1329
1393
|
}
|
|
@@ -1342,7 +1406,7 @@ TODO \u2014 write the memory body.
|
|
|
1342
1406
|
topic: opts.topic
|
|
1343
1407
|
});
|
|
1344
1408
|
const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1345
|
-
await mkdir4(
|
|
1409
|
+
await mkdir4(path8.dirname(file), { recursive: true });
|
|
1346
1410
|
if (existsSync7(file)) {
|
|
1347
1411
|
ui.error(`Memory already exists at ${file}`);
|
|
1348
1412
|
process.exitCode = 1;
|
|
@@ -1361,7 +1425,7 @@ TODO \u2014 write the memory body.
|
|
|
1361
1425
|
}
|
|
1362
1426
|
}
|
|
1363
1427
|
await writeFile4(file, serializeMemory2({ frontmatter, body }), "utf8");
|
|
1364
|
-
ui.success(`Created ${
|
|
1428
|
+
ui.success(`Created ${path8.relative(root, file)}`);
|
|
1365
1429
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
1366
1430
|
if (inferredTags.length > 0) {
|
|
1367
1431
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -1392,7 +1456,7 @@ function parseCsv2(value) {
|
|
|
1392
1456
|
|
|
1393
1457
|
// src/commands/memory-list.ts
|
|
1394
1458
|
import { existsSync as existsSync8 } from "fs";
|
|
1395
|
-
import
|
|
1459
|
+
import path9 from "path";
|
|
1396
1460
|
import "commander";
|
|
1397
1461
|
import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
|
|
1398
1462
|
|
|
@@ -1440,7 +1504,7 @@ function registerMemoryList(memory2) {
|
|
|
1440
1504
|
console.log(
|
|
1441
1505
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
1442
1506
|
);
|
|
1443
|
-
console.log(` ${ui.dim(
|
|
1507
|
+
console.log(` ${ui.dim(path9.relative(root, filePath))}`);
|
|
1444
1508
|
}
|
|
1445
1509
|
console.log(ui.dim(`
|
|
1446
1510
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -1477,7 +1541,7 @@ function matchesFilters(loaded, opts) {
|
|
|
1477
1541
|
// src/commands/memory-promote.ts
|
|
1478
1542
|
import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
|
|
1479
1543
|
import { existsSync as existsSync9 } from "fs";
|
|
1480
|
-
import
|
|
1544
|
+
import path10 from "path";
|
|
1481
1545
|
import "commander";
|
|
1482
1546
|
import {
|
|
1483
1547
|
findProjectRoot as findProjectRoot11,
|
|
@@ -1524,11 +1588,11 @@ function registerMemoryPromote(memory2) {
|
|
|
1524
1588
|
body: found.memory.body
|
|
1525
1589
|
};
|
|
1526
1590
|
const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
|
|
1527
|
-
await mkdir5(
|
|
1591
|
+
await mkdir5(path10.dirname(newPath), { recursive: true });
|
|
1528
1592
|
await writeFile5(newPath, serializeMemory3(updated), "utf8");
|
|
1529
1593
|
await unlink(found.filePath);
|
|
1530
1594
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
1531
|
-
ui.info(`Now at ${
|
|
1595
|
+
ui.info(`Now at ${path10.relative(root, newPath)}`);
|
|
1532
1596
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
1533
1597
|
});
|
|
1534
1598
|
}
|
|
@@ -1536,7 +1600,7 @@ function registerMemoryPromote(memory2) {
|
|
|
1536
1600
|
// src/commands/memory-approve.ts
|
|
1537
1601
|
import { existsSync as existsSync10 } from "fs";
|
|
1538
1602
|
import { writeFile as writeFile6 } from "fs/promises";
|
|
1539
|
-
import
|
|
1603
|
+
import path11 from "path";
|
|
1540
1604
|
import "commander";
|
|
1541
1605
|
import {
|
|
1542
1606
|
findProjectRoot as findProjectRoot12,
|
|
@@ -1600,14 +1664,14 @@ function registerMemoryApprove(memory2) {
|
|
|
1600
1664
|
};
|
|
1601
1665
|
await writeFile6(found.filePath, serializeMemory4(next), "utf8");
|
|
1602
1666
|
ui.success(`Approved ${id} (status=validated)`);
|
|
1603
|
-
ui.info(
|
|
1667
|
+
ui.info(path11.relative(root, found.filePath));
|
|
1604
1668
|
});
|
|
1605
1669
|
}
|
|
1606
1670
|
|
|
1607
1671
|
// src/commands/memory-update.ts
|
|
1608
1672
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
1609
1673
|
import { existsSync as existsSync11 } from "fs";
|
|
1610
|
-
import
|
|
1674
|
+
import path12 from "path";
|
|
1611
1675
|
import "commander";
|
|
1612
1676
|
import {
|
|
1613
1677
|
findProjectRoot as findProjectRoot13,
|
|
@@ -1670,7 +1734,7 @@ function registerMemoryUpdate(memory2) {
|
|
|
1670
1734
|
serializeMemory5({ frontmatter: newFrontmatter, body: newBody }),
|
|
1671
1735
|
"utf8"
|
|
1672
1736
|
);
|
|
1673
|
-
ui.success(`Updated ${
|
|
1737
|
+
ui.success(`Updated ${path12.relative(root, loaded.filePath)}`);
|
|
1674
1738
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
1675
1739
|
});
|
|
1676
1740
|
}
|
|
@@ -1691,7 +1755,7 @@ function parseCsv3(value) {
|
|
|
1691
1755
|
// src/commands/memory-auto-promote.ts
|
|
1692
1756
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
1693
1757
|
import { existsSync as existsSync12 } from "fs";
|
|
1694
|
-
import
|
|
1758
|
+
import path13 from "path";
|
|
1695
1759
|
import "commander";
|
|
1696
1760
|
import {
|
|
1697
1761
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
@@ -1736,7 +1800,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1736
1800
|
console.log(
|
|
1737
1801
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
1738
1802
|
);
|
|
1739
|
-
console.log(` ${ui.dim(
|
|
1803
|
+
console.log(` ${ui.dim(path13.relative(root, filePath))}`);
|
|
1740
1804
|
if (opts.apply) {
|
|
1741
1805
|
const next = {
|
|
1742
1806
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
@@ -1755,7 +1819,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1755
1819
|
import { spawn as spawn2 } from "child_process";
|
|
1756
1820
|
import { existsSync as existsSync13 } from "fs";
|
|
1757
1821
|
import { readFile as readFile5 } from "fs/promises";
|
|
1758
|
-
import
|
|
1822
|
+
import path14 from "path";
|
|
1759
1823
|
import "commander";
|
|
1760
1824
|
import {
|
|
1761
1825
|
findProjectRoot as findProjectRoot15,
|
|
@@ -1779,7 +1843,7 @@ function registerMemoryEdit(memory2) {
|
|
|
1779
1843
|
return;
|
|
1780
1844
|
}
|
|
1781
1845
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
1782
|
-
ui.info(`Opening ${
|
|
1846
|
+
ui.info(`Opening ${path14.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
1783
1847
|
const code = await runEditor(editor, found.filePath);
|
|
1784
1848
|
if (code !== 0) {
|
|
1785
1849
|
ui.warn(`Editor exited with status ${code}.`);
|
|
@@ -1807,7 +1871,7 @@ function runEditor(editor, file) {
|
|
|
1807
1871
|
|
|
1808
1872
|
// src/commands/memory-for-files.ts
|
|
1809
1873
|
import { existsSync as existsSync14 } from "fs";
|
|
1810
|
-
import
|
|
1874
|
+
import path15 from "path";
|
|
1811
1875
|
import "commander";
|
|
1812
1876
|
import {
|
|
1813
1877
|
deriveConfidence,
|
|
@@ -1929,13 +1993,13 @@ function printGroup(root, label, loaded, usage) {
|
|
|
1929
1993
|
const u = getUsage3(usage, fm.id);
|
|
1930
1994
|
const conf = deriveConfidence(fm, u);
|
|
1931
1995
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
1932
|
-
console.log(` ${ui.dim(
|
|
1996
|
+
console.log(` ${ui.dim(path15.relative(root, filePath))}`);
|
|
1933
1997
|
}
|
|
1934
1998
|
}
|
|
1935
1999
|
|
|
1936
2000
|
// src/commands/memory-hot.ts
|
|
1937
2001
|
import { existsSync as existsSync15 } from "fs";
|
|
1938
|
-
import
|
|
2002
|
+
import path16 from "path";
|
|
1939
2003
|
import "commander";
|
|
1940
2004
|
import {
|
|
1941
2005
|
findProjectRoot as findProjectRoot17,
|
|
@@ -1975,7 +2039,7 @@ function registerMemoryHot(memory2) {
|
|
|
1975
2039
|
console.log(
|
|
1976
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}`)}`
|
|
1977
2041
|
);
|
|
1978
|
-
console.log(` ${ui.dim(
|
|
2042
|
+
console.log(` ${ui.dim(path16.relative(root, filePath))}`);
|
|
1979
2043
|
}
|
|
1980
2044
|
ui.info(
|
|
1981
2045
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -1986,7 +2050,7 @@ function registerMemoryHot(memory2) {
|
|
|
1986
2050
|
// src/commands/memory-tried.ts
|
|
1987
2051
|
import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
|
|
1988
2052
|
import { existsSync as existsSync16 } from "fs";
|
|
1989
|
-
import
|
|
2053
|
+
import path17 from "path";
|
|
1990
2054
|
import "commander";
|
|
1991
2055
|
import {
|
|
1992
2056
|
buildFrontmatter as buildFrontmatter3,
|
|
@@ -2037,14 +2101,14 @@ function registerMemoryTried(memory2) {
|
|
|
2037
2101
|
}
|
|
2038
2102
|
const body = lines.join("\n") + "\n";
|
|
2039
2103
|
const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2040
|
-
await mkdir6(
|
|
2104
|
+
await mkdir6(path17.dirname(file), { recursive: true });
|
|
2041
2105
|
if (existsSync16(file)) {
|
|
2042
2106
|
ui.error(`Memory already exists at ${file}`);
|
|
2043
2107
|
process.exitCode = 1;
|
|
2044
2108
|
return;
|
|
2045
2109
|
}
|
|
2046
2110
|
await writeFile9(file, serializeMemory7({ frontmatter, body }), "utf8");
|
|
2047
|
-
ui.success(`Recorded: ${
|
|
2111
|
+
ui.success(`Recorded: ${path17.relative(root, file)}`);
|
|
2048
2112
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
2049
2113
|
});
|
|
2050
2114
|
}
|
|
@@ -2055,7 +2119,7 @@ function parseCsv4(value) {
|
|
|
2055
2119
|
|
|
2056
2120
|
// src/commands/memory-pending.ts
|
|
2057
2121
|
import { existsSync as existsSync17 } from "fs";
|
|
2058
|
-
import
|
|
2122
|
+
import path18 from "path";
|
|
2059
2123
|
import "commander";
|
|
2060
2124
|
import {
|
|
2061
2125
|
findProjectRoot as findProjectRoot19,
|
|
@@ -2095,7 +2159,7 @@ function registerMemoryPending(memory2) {
|
|
|
2095
2159
|
console.log(
|
|
2096
2160
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2097
2161
|
);
|
|
2098
|
-
console.log(` ${ui.dim(
|
|
2162
|
+
console.log(` ${ui.dim(path18.relative(root, filePath))}`);
|
|
2099
2163
|
}
|
|
2100
2164
|
ui.info(`${proposed.length} pending`);
|
|
2101
2165
|
});
|
|
@@ -2103,7 +2167,7 @@ function registerMemoryPending(memory2) {
|
|
|
2103
2167
|
|
|
2104
2168
|
// src/commands/memory-query.ts
|
|
2105
2169
|
import { existsSync as existsSync18 } from "fs";
|
|
2106
|
-
import
|
|
2170
|
+
import path19 from "path";
|
|
2107
2171
|
import "commander";
|
|
2108
2172
|
import {
|
|
2109
2173
|
extractSnippet,
|
|
@@ -2160,7 +2224,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2160
2224
|
const fm = mem.frontmatter;
|
|
2161
2225
|
const statusBadge = ui.statusBadge(fm.status);
|
|
2162
2226
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
2163
|
-
console.log(` ${ui.dim(
|
|
2227
|
+
console.log(` ${ui.dim(path19.relative(root, filePath))}`);
|
|
2164
2228
|
const snippet = extractSnippet(mem.body, snippetNeedle);
|
|
2165
2229
|
if (snippet) console.log(` ${snippet}`);
|
|
2166
2230
|
}
|
|
@@ -2230,7 +2294,7 @@ function registerMemoryReject(memory2) {
|
|
|
2230
2294
|
// src/commands/memory-rm.ts
|
|
2231
2295
|
import { existsSync as existsSync20 } from "fs";
|
|
2232
2296
|
import { unlink as unlink2 } from "fs/promises";
|
|
2233
|
-
import
|
|
2297
|
+
import path20 from "path";
|
|
2234
2298
|
import { createInterface } from "readline/promises";
|
|
2235
2299
|
import "commander";
|
|
2236
2300
|
import {
|
|
@@ -2255,7 +2319,7 @@ function registerMemoryRm(memory2) {
|
|
|
2255
2319
|
process.exitCode = 1;
|
|
2256
2320
|
return;
|
|
2257
2321
|
}
|
|
2258
|
-
const rel =
|
|
2322
|
+
const rel = path20.relative(root, found.filePath);
|
|
2259
2323
|
if (!opts.yes) {
|
|
2260
2324
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2261
2325
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -2281,7 +2345,7 @@ function registerMemoryRm(memory2) {
|
|
|
2281
2345
|
// src/commands/memory-show.ts
|
|
2282
2346
|
import { existsSync as existsSync21 } from "fs";
|
|
2283
2347
|
import { readFile as readFile6 } from "fs/promises";
|
|
2284
|
-
import
|
|
2348
|
+
import path21 from "path";
|
|
2285
2349
|
import "commander";
|
|
2286
2350
|
import {
|
|
2287
2351
|
deriveConfidence as deriveConfidence2,
|
|
@@ -2323,7 +2387,7 @@ function registerMemoryShow(memory2) {
|
|
|
2323
2387
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
2324
2388
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
2325
2389
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
2326
|
-
console.log(`${ui.dim("file:")} ${
|
|
2390
|
+
console.log(`${ui.dim("file:")} ${path21.relative(root, found.filePath)}`);
|
|
2327
2391
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
2328
2392
|
console.log(ui.dim("anchor:"));
|
|
2329
2393
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -2339,7 +2403,7 @@ function registerMemoryShow(memory2) {
|
|
|
2339
2403
|
|
|
2340
2404
|
// src/commands/memory-stats.ts
|
|
2341
2405
|
import { existsSync as existsSync22 } from "fs";
|
|
2342
|
-
import
|
|
2406
|
+
import path22 from "path";
|
|
2343
2407
|
import "commander";
|
|
2344
2408
|
import {
|
|
2345
2409
|
deriveConfidence as deriveConfidence3,
|
|
@@ -2377,7 +2441,7 @@ function registerMemoryStats(memory2) {
|
|
|
2377
2441
|
console.log(
|
|
2378
2442
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
2379
2443
|
);
|
|
2380
|
-
console.log(` ${ui.dim(
|
|
2444
|
+
console.log(` ${ui.dim(path22.relative(root, filePath))}`);
|
|
2381
2445
|
}
|
|
2382
2446
|
});
|
|
2383
2447
|
}
|
|
@@ -2385,7 +2449,7 @@ function registerMemoryStats(memory2) {
|
|
|
2385
2449
|
// src/commands/memory-verify.ts
|
|
2386
2450
|
import { writeFile as writeFile11 } from "fs/promises";
|
|
2387
2451
|
import { existsSync as existsSync23 } from "fs";
|
|
2388
|
-
import
|
|
2452
|
+
import path23 from "path";
|
|
2389
2453
|
import "commander";
|
|
2390
2454
|
import {
|
|
2391
2455
|
findProjectRoot as findProjectRoot25,
|
|
@@ -2422,7 +2486,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2422
2486
|
anchorlessIds.push(mem.frontmatter.id);
|
|
2423
2487
|
continue;
|
|
2424
2488
|
}
|
|
2425
|
-
const rel =
|
|
2489
|
+
const rel = path23.relative(root, filePath);
|
|
2426
2490
|
if (result.stale) {
|
|
2427
2491
|
staleCount++;
|
|
2428
2492
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -2538,7 +2602,7 @@ function registerMemoryImport(memory2) {
|
|
|
2538
2602
|
// src/commands/memory-import-changelog.ts
|
|
2539
2603
|
import { existsSync as existsSync25 } from "fs";
|
|
2540
2604
|
import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
|
|
2541
|
-
import
|
|
2605
|
+
import path24 from "path";
|
|
2542
2606
|
import "commander";
|
|
2543
2607
|
import {
|
|
2544
2608
|
buildFrontmatter as buildFrontmatter4,
|
|
@@ -2609,7 +2673,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2609
2673
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2610
2674
|
const root = findProjectRoot27(opts.dir);
|
|
2611
2675
|
const paths = resolveHaivePaths24(root);
|
|
2612
|
-
const changelogPath =
|
|
2676
|
+
const changelogPath = path24.resolve(root, opts.fromChangelog);
|
|
2613
2677
|
if (!existsSync25(changelogPath)) {
|
|
2614
2678
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
2615
2679
|
process.exitCode = 1;
|
|
@@ -2629,9 +2693,9 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2629
2693
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
2630
2694
|
}
|
|
2631
2695
|
}
|
|
2632
|
-
const pkgName = opts.package ??
|
|
2696
|
+
const pkgName = opts.package ?? path24.basename(path24.dirname(changelogPath));
|
|
2633
2697
|
const scope = opts.scope ?? "team";
|
|
2634
|
-
const teamDir =
|
|
2698
|
+
const teamDir = path24.join(paths.memoriesDir, scope);
|
|
2635
2699
|
await mkdir7(teamDir, { recursive: true });
|
|
2636
2700
|
let saved = 0;
|
|
2637
2701
|
for (const entry of entries) {
|
|
@@ -2654,7 +2718,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2654
2718
|
lines.push("");
|
|
2655
2719
|
}
|
|
2656
2720
|
lines.push(
|
|
2657
|
-
`**Source:** \`${
|
|
2721
|
+
`**Source:** \`${path24.relative(root, changelogPath)}\`
|
|
2658
2722
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
2659
2723
|
);
|
|
2660
2724
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -2669,11 +2733,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2669
2733
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
2670
2734
|
`v${entry.version}`
|
|
2671
2735
|
],
|
|
2672
|
-
paths: [
|
|
2736
|
+
paths: [path24.relative(root, changelogPath)],
|
|
2673
2737
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
2674
2738
|
});
|
|
2675
2739
|
await writeFile12(
|
|
2676
|
-
|
|
2740
|
+
path24.join(teamDir, `${fm.id}.md`),
|
|
2677
2741
|
serializeMemory10({ frontmatter: fm, body: lines.join("\n") }),
|
|
2678
2742
|
"utf8"
|
|
2679
2743
|
);
|
|
@@ -2698,7 +2762,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
2698
2762
|
// src/commands/memory-digest.ts
|
|
2699
2763
|
import { existsSync as existsSync26 } from "fs";
|
|
2700
2764
|
import { writeFile as writeFile13 } from "fs/promises";
|
|
2701
|
-
import
|
|
2765
|
+
import path25 from "path";
|
|
2702
2766
|
import "commander";
|
|
2703
2767
|
import {
|
|
2704
2768
|
deriveConfidence as deriveConfidence4,
|
|
@@ -2793,7 +2857,7 @@ function registerMemoryDigest(program2) {
|
|
|
2793
2857
|
);
|
|
2794
2858
|
const digest = lines.join("\n");
|
|
2795
2859
|
if (opts.out) {
|
|
2796
|
-
const outPath =
|
|
2860
|
+
const outPath = path25.resolve(process.cwd(), opts.out);
|
|
2797
2861
|
await writeFile13(outPath, digest, "utf8");
|
|
2798
2862
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
2799
2863
|
} else {
|
|
@@ -2805,7 +2869,7 @@ function registerMemoryDigest(program2) {
|
|
|
2805
2869
|
// src/commands/session-end.ts
|
|
2806
2870
|
import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
|
|
2807
2871
|
import { existsSync as existsSync27 } from "fs";
|
|
2808
|
-
import
|
|
2872
|
+
import path26 from "path";
|
|
2809
2873
|
import "commander";
|
|
2810
2874
|
import {
|
|
2811
2875
|
buildFrontmatter as buildFrontmatter5,
|
|
@@ -2873,7 +2937,7 @@ function registerSessionEnd(session2) {
|
|
|
2873
2937
|
const body = buildRecapBody(opts);
|
|
2874
2938
|
const topic = recapTopic(scope, opts.module);
|
|
2875
2939
|
const filesTouched = parseCsv5(opts.files);
|
|
2876
|
-
const missingPaths = filesTouched.filter((p) => !existsSync27(
|
|
2940
|
+
const missingPaths = filesTouched.filter((p) => !existsSync27(path26.resolve(root, p)));
|
|
2877
2941
|
if (missingPaths.length > 0) {
|
|
2878
2942
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
2879
2943
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
@@ -2896,7 +2960,7 @@ function registerSessionEnd(session2) {
|
|
|
2896
2960
|
};
|
|
2897
2961
|
await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2898
2962
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
2899
|
-
ui.info(`id=${fm.id} file=${
|
|
2963
|
+
ui.info(`id=${fm.id} file=${path26.relative(root, topicMatch.filePath)}`);
|
|
2900
2964
|
return;
|
|
2901
2965
|
}
|
|
2902
2966
|
}
|
|
@@ -2911,10 +2975,10 @@ function registerSessionEnd(session2) {
|
|
|
2911
2975
|
status: "validated"
|
|
2912
2976
|
});
|
|
2913
2977
|
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2914
|
-
await mkdir8(
|
|
2978
|
+
await mkdir8(path26.dirname(file), { recursive: true });
|
|
2915
2979
|
await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
|
|
2916
2980
|
ui.success(`Session recap created`);
|
|
2917
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
2981
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path26.relative(root, file)}`);
|
|
2918
2982
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
2919
2983
|
});
|
|
2920
2984
|
}
|
|
@@ -2926,7 +2990,7 @@ function parseCsv5(value) {
|
|
|
2926
2990
|
// src/commands/snapshot.ts
|
|
2927
2991
|
import { existsSync as existsSync28 } from "fs";
|
|
2928
2992
|
import { readdir } from "fs/promises";
|
|
2929
|
-
import
|
|
2993
|
+
import path27 from "path";
|
|
2930
2994
|
import "commander";
|
|
2931
2995
|
import {
|
|
2932
2996
|
diffContract,
|
|
@@ -2965,7 +3029,7 @@ function registerSnapshot(program2) {
|
|
|
2965
3029
|
return;
|
|
2966
3030
|
}
|
|
2967
3031
|
if (opts.list) {
|
|
2968
|
-
const contractsDir =
|
|
3032
|
+
const contractsDir = path27.join(paths.haiveDir, "contracts");
|
|
2969
3033
|
if (!existsSync28(contractsDir)) {
|
|
2970
3034
|
console.log(ui.dim("No contract snapshots found."));
|
|
2971
3035
|
return;
|
|
@@ -3021,7 +3085,7 @@ function registerSnapshot(program2) {
|
|
|
3021
3085
|
return;
|
|
3022
3086
|
}
|
|
3023
3087
|
const contractPath = opts.contract;
|
|
3024
|
-
const name = opts.name ??
|
|
3088
|
+
const name = opts.name ?? path27.basename(contractPath, path27.extname(contractPath));
|
|
3025
3089
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
3026
3090
|
const contract = { name, path: contractPath, format };
|
|
3027
3091
|
try {
|
|
@@ -3076,8 +3140,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
3076
3140
|
}
|
|
3077
3141
|
}
|
|
3078
3142
|
function detectFormat(filePath) {
|
|
3079
|
-
const ext =
|
|
3080
|
-
const base =
|
|
3143
|
+
const ext = path27.extname(filePath).toLowerCase();
|
|
3144
|
+
const base = path27.basename(filePath).toLowerCase();
|
|
3081
3145
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
3082
3146
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
3083
3147
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -3092,7 +3156,7 @@ function detectFormat(filePath) {
|
|
|
3092
3156
|
// src/commands/hub.ts
|
|
3093
3157
|
import { existsSync as existsSync29 } from "fs";
|
|
3094
3158
|
import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile15, copyFile } from "fs/promises";
|
|
3095
|
-
import
|
|
3159
|
+
import path28 from "path";
|
|
3096
3160
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3097
3161
|
import "commander";
|
|
3098
3162
|
import {
|
|
@@ -3111,7 +3175,7 @@ function registerHub(program2) {
|
|
|
3111
3175
|
hub.command("init <hubPath>").description(
|
|
3112
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"
|
|
3113
3177
|
).action(async (hubPath) => {
|
|
3114
|
-
const absPath =
|
|
3178
|
+
const absPath = path28.resolve(hubPath);
|
|
3115
3179
|
await mkdir9(absPath, { recursive: true });
|
|
3116
3180
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
3117
3181
|
if (gitCheck.status !== 0) {
|
|
@@ -3122,10 +3186,10 @@ function registerHub(program2) {
|
|
|
3122
3186
|
return;
|
|
3123
3187
|
}
|
|
3124
3188
|
}
|
|
3125
|
-
const sharedDir =
|
|
3189
|
+
const sharedDir = path28.join(absPath, ".ai", "memories", "shared");
|
|
3126
3190
|
await mkdir9(sharedDir, { recursive: true });
|
|
3127
3191
|
await writeFile15(
|
|
3128
|
-
|
|
3192
|
+
path28.join(absPath, ".ai", "README.md"),
|
|
3129
3193
|
`# hAIve Team Knowledge Hub
|
|
3130
3194
|
|
|
3131
3195
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -3147,7 +3211,7 @@ haive hub pull # import into a project
|
|
|
3147
3211
|
"utf8"
|
|
3148
3212
|
);
|
|
3149
3213
|
await writeFile15(
|
|
3150
|
-
|
|
3214
|
+
path28.join(absPath, ".gitignore"),
|
|
3151
3215
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
3152
3216
|
"utf8"
|
|
3153
3217
|
);
|
|
@@ -3162,7 +3226,7 @@ haive hub pull # import into a project
|
|
|
3162
3226
|
`
|
|
3163
3227
|
Next steps:
|
|
3164
3228
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
3165
|
-
{ "hubPath": "${
|
|
3229
|
+
{ "hubPath": "${path28.relative(process.cwd(), absPath)}" }
|
|
3166
3230
|
2. Run \`haive hub push\` to publish your shared memories
|
|
3167
3231
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
3168
3232
|
`
|
|
@@ -3191,14 +3255,14 @@ Next steps:
|
|
|
3191
3255
|
process.exitCode = 1;
|
|
3192
3256
|
return;
|
|
3193
3257
|
}
|
|
3194
|
-
const hubRoot =
|
|
3258
|
+
const hubRoot = path28.resolve(root, config.hubPath);
|
|
3195
3259
|
if (!existsSync29(hubRoot)) {
|
|
3196
3260
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
3197
3261
|
process.exitCode = 1;
|
|
3198
3262
|
return;
|
|
3199
3263
|
}
|
|
3200
|
-
const projectName =
|
|
3201
|
-
const destDir =
|
|
3264
|
+
const projectName = path28.basename(root);
|
|
3265
|
+
const destDir = path28.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
3202
3266
|
await mkdir9(destDir, { recursive: true });
|
|
3203
3267
|
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3204
3268
|
const shared = all.filter(
|
|
@@ -3217,7 +3281,7 @@ Next steps:
|
|
|
3217
3281
|
for (const { memory: memory2 } of shared) {
|
|
3218
3282
|
const fm = memory2.frontmatter;
|
|
3219
3283
|
const fileName = `${fm.id}.md`;
|
|
3220
|
-
const destPath =
|
|
3284
|
+
const destPath = path28.join(destDir, fileName);
|
|
3221
3285
|
await writeFile15(destPath, serializeMemory12(memory2), "utf8");
|
|
3222
3286
|
pushed++;
|
|
3223
3287
|
}
|
|
@@ -3225,7 +3289,7 @@ Next steps:
|
|
|
3225
3289
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
3226
3290
|
if (opts.commit) {
|
|
3227
3291
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
3228
|
-
spawnSync3("git", ["add",
|
|
3292
|
+
spawnSync3("git", ["add", path28.join(".ai", "memories", "shared", projectName)], {
|
|
3229
3293
|
cwd: hubRoot
|
|
3230
3294
|
});
|
|
3231
3295
|
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
@@ -3260,13 +3324,13 @@ Next steps:
|
|
|
3260
3324
|
process.exitCode = 1;
|
|
3261
3325
|
return;
|
|
3262
3326
|
}
|
|
3263
|
-
const hubRoot =
|
|
3264
|
-
const hubSharedDir =
|
|
3327
|
+
const hubRoot = path28.resolve(root, config.hubPath);
|
|
3328
|
+
const hubSharedDir = path28.join(hubRoot, ".ai", "memories", "shared");
|
|
3265
3329
|
if (!existsSync29(hubSharedDir)) {
|
|
3266
3330
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
3267
3331
|
return;
|
|
3268
3332
|
}
|
|
3269
|
-
const projectName =
|
|
3333
|
+
const projectName = path28.basename(root);
|
|
3270
3334
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3271
3335
|
const projectDirs = (await readdir2(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
3272
3336
|
if (projectDirs.length === 0) {
|
|
@@ -3276,16 +3340,16 @@ Next steps:
|
|
|
3276
3340
|
let totalImported = 0;
|
|
3277
3341
|
let totalUpdated = 0;
|
|
3278
3342
|
for (const sourceName of projectDirs) {
|
|
3279
|
-
const sourceDir =
|
|
3280
|
-
const destDir =
|
|
3343
|
+
const sourceDir = path28.join(hubSharedDir, sourceName);
|
|
3344
|
+
const destDir = path28.join(paths.memoriesDir, "shared", sourceName);
|
|
3281
3345
|
await mkdir9(destDir, { recursive: true });
|
|
3282
3346
|
const sourceFiles = (await readdir2(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
3283
3347
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
3284
3348
|
const existingInDest = await loadDir(destDir);
|
|
3285
3349
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
3286
3350
|
for (const file of sourceFiles) {
|
|
3287
|
-
const srcPath =
|
|
3288
|
-
const destPath =
|
|
3351
|
+
const srcPath = path28.join(sourceDir, file);
|
|
3352
|
+
const destPath = path28.join(destDir, file);
|
|
3289
3353
|
const fileContent = await readFile9(srcPath, "utf8");
|
|
3290
3354
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
3291
3355
|
if (!alreadyTagged) {
|
|
@@ -3316,14 +3380,14 @@ Next steps:
|
|
|
3316
3380
|
console.log(
|
|
3317
3381
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
3318
3382
|
);
|
|
3319
|
-
const sharedDir =
|
|
3383
|
+
const sharedDir = path28.join(paths.memoriesDir, "shared");
|
|
3320
3384
|
if (existsSync29(sharedDir)) {
|
|
3321
3385
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3322
3386
|
const sources = (await readdir2(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3323
3387
|
console.log(`
|
|
3324
3388
|
Imported from ${sources.length} source(s):`);
|
|
3325
3389
|
for (const src of sources) {
|
|
3326
|
-
const files = (await readdir2(
|
|
3390
|
+
const files = (await readdir2(path28.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
3327
3391
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
3328
3392
|
}
|
|
3329
3393
|
} else {
|
|
@@ -3344,9 +3408,944 @@ Next steps:
|
|
|
3344
3408
|
});
|
|
3345
3409
|
}
|
|
3346
3410
|
|
|
3411
|
+
// src/commands/stats.ts
|
|
3412
|
+
import "commander";
|
|
3413
|
+
import {
|
|
3414
|
+
aggregateUsage,
|
|
3415
|
+
findProjectRoot as findProjectRoot32,
|
|
3416
|
+
parseSince,
|
|
3417
|
+
readUsageEvents,
|
|
3418
|
+
resolveHaivePaths as resolveHaivePaths29,
|
|
3419
|
+
usageLogSize
|
|
3420
|
+
} from "@hiveai/core";
|
|
3421
|
+
function registerStats(program2) {
|
|
3422
|
+
program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3423
|
+
const root = findProjectRoot32(opts.dir);
|
|
3424
|
+
const paths = resolveHaivePaths29(root);
|
|
3425
|
+
const size = await usageLogSize(paths);
|
|
3426
|
+
if (!size.exists) {
|
|
3427
|
+
if (opts.json) {
|
|
3428
|
+
console.log(JSON.stringify({ error: "no_usage_log" }));
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
ui.warn(
|
|
3432
|
+
`No usage log found at ${root}. Stats are populated as the MCP server logs each tool call. Run a session first, then re-check.`
|
|
3433
|
+
);
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
const events = await readUsageEvents(paths);
|
|
3437
|
+
const since = parseSince(opts.since);
|
|
3438
|
+
const aggregate = aggregateUsage(events, since ?? void 0);
|
|
3439
|
+
if (opts.json) {
|
|
3440
|
+
console.log(JSON.stringify(aggregate, null, 2));
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
const window = opts.since ?? "all time";
|
|
3444
|
+
console.log(ui.bold(`hAIve usage stats (${window})`));
|
|
3445
|
+
console.log(
|
|
3446
|
+
` ${ui.dim("total calls:")} ${aggregate.total} ${ui.dim("unique tools:")} ${aggregate.by_tool.length} ${ui.dim("log lines:")} ${size.lines}`
|
|
3447
|
+
);
|
|
3448
|
+
if (aggregate.window_start) {
|
|
3449
|
+
console.log(
|
|
3450
|
+
` ${ui.dim("window:")} ${aggregate.window_start.slice(0, 19)} \u2192 ${aggregate.window_end?.slice(0, 19)}`
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
if (aggregate.by_tool.length === 0) {
|
|
3454
|
+
ui.info(`No events in window. Try a wider --since (current: ${window}).`);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
console.log();
|
|
3458
|
+
console.log(ui.bold("Top tools:"));
|
|
3459
|
+
const maxCount = aggregate.by_tool[0]?.count ?? 1;
|
|
3460
|
+
for (const t of aggregate.by_tool.slice(0, 20)) {
|
|
3461
|
+
const bar = "\u2588".repeat(Math.max(1, Math.round(t.count / maxCount * 30)));
|
|
3462
|
+
const pct = (t.count / aggregate.total * 100).toFixed(1);
|
|
3463
|
+
console.log(
|
|
3464
|
+
` ${t.tool.padEnd(28)} ${ui.green(bar)} ${ui.bold(String(t.count))} ${ui.dim(`(${pct}%, last ${t.last_used.slice(0, 19)})`)}`
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// src/commands/bench.ts
|
|
3471
|
+
import { performance } from "perf_hooks";
|
|
3472
|
+
import "commander";
|
|
3473
|
+
import {
|
|
3474
|
+
estimateTokens,
|
|
3475
|
+
findProjectRoot as findProjectRoot33,
|
|
3476
|
+
resolveHaivePaths as resolveHaivePaths30
|
|
3477
|
+
} from "@hiveai/core";
|
|
3478
|
+
import {
|
|
3479
|
+
antiPatternsCheck,
|
|
3480
|
+
codeMapTool,
|
|
3481
|
+
codeSearch,
|
|
3482
|
+
getBriefing,
|
|
3483
|
+
getRecap,
|
|
3484
|
+
memRelevantTo
|
|
3485
|
+
} from "@hiveai/mcp";
|
|
3486
|
+
function registerBench(program2) {
|
|
3487
|
+
program2.command("bench").description("Self-test the local hAIve setup: runs core MCP tools against this project and reports latency + payload size.").option("-t, --task <task>", "task description for ranking-aware tools", "audit dependencies for security risks").option("--json", "emit JSON instead of a table", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3488
|
+
const root = findProjectRoot33(opts.dir);
|
|
3489
|
+
const paths = resolveHaivePaths30(root);
|
|
3490
|
+
const ctx = { paths };
|
|
3491
|
+
const task = opts.task ?? "audit dependencies for security risks";
|
|
3492
|
+
const scenarios = [
|
|
3493
|
+
async () => {
|
|
3494
|
+
const t0 = performance.now();
|
|
3495
|
+
const out = await getBriefing(
|
|
3496
|
+
{
|
|
3497
|
+
task,
|
|
3498
|
+
files: [],
|
|
3499
|
+
max_tokens: 4e3,
|
|
3500
|
+
max_memories: 8,
|
|
3501
|
+
include_project_context: true,
|
|
3502
|
+
include_module_contexts: true,
|
|
3503
|
+
semantic: true,
|
|
3504
|
+
include_stale: false,
|
|
3505
|
+
track: false,
|
|
3506
|
+
format: "compact",
|
|
3507
|
+
symbols: [],
|
|
3508
|
+
min_semantic_score: 0
|
|
3509
|
+
},
|
|
3510
|
+
ctx
|
|
3511
|
+
);
|
|
3512
|
+
return summarize("get_briefing(compact)", t0, out, [
|
|
3513
|
+
out.low_value ? "low_value (cold-start)" : `${out.memories.length} memories`,
|
|
3514
|
+
`search=${out.search_mode}`
|
|
3515
|
+
]);
|
|
3516
|
+
},
|
|
3517
|
+
async () => {
|
|
3518
|
+
const t0 = performance.now();
|
|
3519
|
+
const out = await codeMapTool({ paths: [], max_files: 40, max_tokens: 2e3 }, ctx);
|
|
3520
|
+
return summarize("code_map(budget=2k)", t0, out, [
|
|
3521
|
+
out.available ? `${out.files.length}/${out.total_files} files` : "unavailable",
|
|
3522
|
+
out.budget_clipped ? "clipped" : "fits"
|
|
3523
|
+
]);
|
|
3524
|
+
},
|
|
3525
|
+
async () => {
|
|
3526
|
+
const t0 = performance.now();
|
|
3527
|
+
const out = await getRecap({ scope: "any" }, ctx);
|
|
3528
|
+
return summarize("get_recap", t0, out, [
|
|
3529
|
+
out.recap ? `${out.recap.id.slice(0, 30)}\u2026` : "no recap"
|
|
3530
|
+
]);
|
|
3531
|
+
},
|
|
3532
|
+
async () => {
|
|
3533
|
+
const t0 = performance.now();
|
|
3534
|
+
const out = await memRelevantTo(
|
|
3535
|
+
{ task, files: [], limit: 8, min_semantic_score: 0.25, format: "compact" },
|
|
3536
|
+
ctx
|
|
3537
|
+
);
|
|
3538
|
+
return summarize("mem_relevant_to", t0, out, [
|
|
3539
|
+
`${out.memories.length} memories`,
|
|
3540
|
+
`search=${out.search_mode}`
|
|
3541
|
+
]);
|
|
3542
|
+
},
|
|
3543
|
+
async () => {
|
|
3544
|
+
const t0 = performance.now();
|
|
3545
|
+
const out = await codeSearch({ query: task, k: 5, min_score: 0.2 }, ctx);
|
|
3546
|
+
return summarize("code_search", t0, out, [
|
|
3547
|
+
out.available ? `${out.hits.length} hits` : "needs index (haive index code-search)"
|
|
3548
|
+
]);
|
|
3549
|
+
},
|
|
3550
|
+
async () => {
|
|
3551
|
+
const t0 = performance.now();
|
|
3552
|
+
const out = await antiPatternsCheck({ diff: task, paths: [], limit: 5, semantic: true }, ctx);
|
|
3553
|
+
return summarize("anti_patterns_check", t0, out, [
|
|
3554
|
+
`${out.warnings.length}/${out.scanned} warn`
|
|
3555
|
+
]);
|
|
3556
|
+
}
|
|
3557
|
+
];
|
|
3558
|
+
const results = [];
|
|
3559
|
+
for (const run of scenarios) {
|
|
3560
|
+
try {
|
|
3561
|
+
results.push(await run());
|
|
3562
|
+
} catch (err) {
|
|
3563
|
+
results.push({
|
|
3564
|
+
name: "(error)",
|
|
3565
|
+
ok: false,
|
|
3566
|
+
latency_ms: 0,
|
|
3567
|
+
payload_tokens: 0,
|
|
3568
|
+
notes: [err instanceof Error ? err.message : String(err)]
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (opts.json) {
|
|
3573
|
+
console.log(JSON.stringify({ root, task, scenarios: results }, null, 2));
|
|
3574
|
+
return;
|
|
3575
|
+
}
|
|
3576
|
+
console.log(ui.bold(`hAIve bench \u2014 ${root}`));
|
|
3577
|
+
console.log(ui.dim(`task: ${task}`));
|
|
3578
|
+
console.log();
|
|
3579
|
+
console.log(
|
|
3580
|
+
`${"scenario".padEnd(28)} ${"latency".padStart(8)} ${"tokens".padStart(7)} notes`
|
|
3581
|
+
);
|
|
3582
|
+
console.log("\u2500".repeat(88));
|
|
3583
|
+
for (const r of results) {
|
|
3584
|
+
const status = r.ok ? ui.green("\u2713") : ui.red("\u2717");
|
|
3585
|
+
console.log(
|
|
3586
|
+
`${status} ${r.name.padEnd(26)} ${`${r.latency_ms.toFixed(0)} ms`.padStart(8)} ${String(r.payload_tokens).padStart(7)} ${r.notes.join("; ")}`
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
const totalTokens = results.reduce((s, r) => s + r.payload_tokens, 0);
|
|
3590
|
+
const totalMs = results.reduce((s, r) => s + r.latency_ms, 0);
|
|
3591
|
+
console.log("\u2500".repeat(88));
|
|
3592
|
+
console.log(
|
|
3593
|
+
`${ui.dim("totals:")} ${`${totalMs.toFixed(0)} ms`.padStart(8)} ${String(totalTokens).padStart(7)}`
|
|
3594
|
+
);
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
function summarize(name, t0, payload, notes) {
|
|
3598
|
+
return {
|
|
3599
|
+
name,
|
|
3600
|
+
ok: true,
|
|
3601
|
+
latency_ms: performance.now() - t0,
|
|
3602
|
+
payload_tokens: estimateTokens(JSON.stringify(payload)),
|
|
3603
|
+
notes
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
|
|
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";
|
|
3611
|
+
import "commander";
|
|
3612
|
+
import {
|
|
3613
|
+
aggregateUsage as aggregateUsage2,
|
|
3614
|
+
buildFrontmatter as buildFrontmatter6,
|
|
3615
|
+
findProjectRoot as findProjectRoot34,
|
|
3616
|
+
loadMemoriesFromDir as loadMemoriesFromDir8,
|
|
3617
|
+
memoryFilePath as memoryFilePath5,
|
|
3618
|
+
parseSince as parseSince2,
|
|
3619
|
+
readUsageEvents as readUsageEvents2,
|
|
3620
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
3621
|
+
serializeMemory as serializeMemory13
|
|
3622
|
+
} from "@hiveai/core";
|
|
3623
|
+
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
3624
|
+
"mem_search",
|
|
3625
|
+
"code_search",
|
|
3626
|
+
"mem_relevant_to",
|
|
3627
|
+
"get_briefing"
|
|
3628
|
+
]);
|
|
3629
|
+
function registerMemorySuggest(memory2) {
|
|
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) => {
|
|
3633
|
+
const root = findProjectRoot34(opts.dir);
|
|
3634
|
+
const paths = resolveHaivePaths31(root);
|
|
3635
|
+
const events = await readUsageEvents2(paths);
|
|
3636
|
+
if (events.length === 0) {
|
|
3637
|
+
if (opts.json) {
|
|
3638
|
+
console.log(JSON.stringify({ suggestions: [] }));
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
ui.warn("No usage log entries yet. Suggestions appear after the MCP server records some calls.");
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
const since = parseSince2(opts.since);
|
|
3645
|
+
const minCount = Math.max(1, parseInt(opts.min ?? "2", 10));
|
|
3646
|
+
const cutoff = since ? since.getTime() : 0;
|
|
3647
|
+
const queries = /* @__PURE__ */ new Map();
|
|
3648
|
+
for (const e of events) {
|
|
3649
|
+
if (cutoff && Date.parse(e.at) < cutoff) continue;
|
|
3650
|
+
if (!SEARCH_TOOLS.has(e.tool)) continue;
|
|
3651
|
+
const key = (e.summary ?? "").toLowerCase().trim();
|
|
3652
|
+
if (!key) continue;
|
|
3653
|
+
const prior = queries.get(key);
|
|
3654
|
+
if (prior) {
|
|
3655
|
+
prior.count++;
|
|
3656
|
+
prior.tools.add(e.tool);
|
|
3657
|
+
if (e.at > prior.last) prior.last = e.at;
|
|
3658
|
+
} else {
|
|
3659
|
+
queries.set(key, { count: 1, tools: /* @__PURE__ */ new Set([e.tool]), last: e.at });
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
const suggestions = [...queries.entries()].filter(([, v]) => v.count >= minCount).map(([query, v]) => ({
|
|
3663
|
+
query,
|
|
3664
|
+
count: v.count,
|
|
3665
|
+
tools: [...v.tools].sort(),
|
|
3666
|
+
last_used: v.last,
|
|
3667
|
+
reason: chooseReason(v.tools, v.count),
|
|
3668
|
+
inferred_type: inferType(v.tools, query)
|
|
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
|
+
}
|
|
3728
|
+
if (opts.json) {
|
|
3729
|
+
console.log(JSON.stringify({ window: opts.since, suggestions }, null, 2));
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
const totals = aggregateUsage2(events, since ?? void 0);
|
|
3733
|
+
console.log(ui.bold(`hAIve memory suggestions (${opts.since ?? "all time"})`));
|
|
3734
|
+
console.log(
|
|
3735
|
+
ui.dim(`scanned ${totals.total} events, ${suggestions.length} repeated queries (\u2265${minCount})`)
|
|
3736
|
+
);
|
|
3737
|
+
console.log();
|
|
3738
|
+
if (suggestions.length === 0) {
|
|
3739
|
+
ui.info("No recurring searches yet \u2014 nothing to suggest.");
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
for (const s of suggestions.slice(0, 30)) {
|
|
3743
|
+
console.log(
|
|
3744
|
+
` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate(s.query, 70)}`
|
|
3745
|
+
);
|
|
3746
|
+
console.log(` ${ui.dim("\u2192")} ${s.reason}`);
|
|
3747
|
+
}
|
|
3748
|
+
console.log();
|
|
3749
|
+
ui.info("Run with --auto-save to draft the top-3 as draft memories.");
|
|
3750
|
+
});
|
|
3751
|
+
}
|
|
3752
|
+
function chooseReason(tools, count) {
|
|
3753
|
+
if (tools.has("code_search")) {
|
|
3754
|
+
return `${count} agents searched the code for this \u2014 consider mem_save (architecture/decision) capturing where it lives.`;
|
|
3755
|
+
}
|
|
3756
|
+
if (tools.has("mem_search") || tools.has("mem_relevant_to")) {
|
|
3757
|
+
return `${count} agents asked but the memory layer had no clear answer \u2014 consider mem_save (convention/decision/gotcha).`;
|
|
3758
|
+
}
|
|
3759
|
+
return `${count} agents asked the briefing for this \u2014 consider promoting the answer to a team memory.`;
|
|
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
|
+
}
|
|
3796
|
+
function truncate(text, max) {
|
|
3797
|
+
if (text.length <= max) return text;
|
|
3798
|
+
return text.slice(0, max - 1) + "\u2026";
|
|
3799
|
+
}
|
|
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
|
+
|
|
3347
4346
|
// src/index.ts
|
|
3348
|
-
var program = new
|
|
3349
|
-
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");
|
|
3350
4349
|
registerInit(program);
|
|
3351
4350
|
registerMcp(program);
|
|
3352
4351
|
registerBriefing(program);
|
|
@@ -3376,10 +4375,17 @@ registerMemoryTried(memory);
|
|
|
3376
4375
|
registerMemoryImport(memory);
|
|
3377
4376
|
registerMemoryImportChangelog(memory);
|
|
3378
4377
|
registerMemoryDigest(memory);
|
|
4378
|
+
registerMemorySuggest(memory);
|
|
4379
|
+
registerMemoryArchive(memory);
|
|
3379
4380
|
var session = program.command("session").description("Manage session lifecycle");
|
|
3380
4381
|
registerSessionEnd(session);
|
|
3381
4382
|
registerSnapshot(program);
|
|
3382
4383
|
registerHub(program);
|
|
4384
|
+
registerStats(program);
|
|
4385
|
+
registerBench(program);
|
|
4386
|
+
registerDoctor(program);
|
|
4387
|
+
registerPlayback(program);
|
|
4388
|
+
registerPrecommit(program);
|
|
3383
4389
|
program.parseAsync(process.argv).catch((err) => {
|
|
3384
4390
|
if (isZodError(err)) {
|
|
3385
4391
|
for (const issue of err.issues) {
|