@hiveai/cli 0.5.0 → 0.7.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/Dashboard-Y2AIWFZK.js +0 -0
- package/dist/index.js +1862 -333
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command39 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
8
8
|
import { readFile } from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
9
10
|
import "commander";
|
|
10
11
|
import {
|
|
11
12
|
findProjectRoot,
|
|
@@ -58,7 +59,12 @@ function registerBriefing(program2) {
|
|
|
58
59
|
"--scope <scope>",
|
|
59
60
|
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
60
61
|
"all"
|
|
61
|
-
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
62
|
+
).option("--include-draft", "include draft memories (excluded by default)").option("--include-stale", "include stale memories (excluded by default \u2014 may be outdated)").option(
|
|
63
|
+
"--include <path>",
|
|
64
|
+
"merge memories from another haive-initialized project (repeatable). Useful for teams with multiple coordinated repos (e.g. backend + frontend).",
|
|
65
|
+
collectInclude,
|
|
66
|
+
[]
|
|
67
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
62
68
|
const root = findProjectRoot(opts.dir);
|
|
63
69
|
const paths = resolveHaivePaths(root);
|
|
64
70
|
if (!existsSync(paths.memoriesDir)) {
|
|
@@ -71,7 +77,34 @@ function registerBriefing(program2) {
|
|
|
71
77
|
}
|
|
72
78
|
return;
|
|
73
79
|
}
|
|
74
|
-
const
|
|
80
|
+
const ownMemories = await loadMemoriesFromDir(paths.memoriesDir);
|
|
81
|
+
const externalRoots = [];
|
|
82
|
+
if (opts.include && opts.include.length > 0) {
|
|
83
|
+
for (const includePath of opts.include) {
|
|
84
|
+
try {
|
|
85
|
+
const otherRoot = findProjectRoot(includePath);
|
|
86
|
+
if (otherRoot === root) continue;
|
|
87
|
+
const otherPaths = resolveHaivePaths(otherRoot);
|
|
88
|
+
if (!existsSync(otherPaths.memoriesDir)) {
|
|
89
|
+
ui.warn(`--include ${includePath}: no .ai/memories at ${otherRoot} \u2014 skipping`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const otherMemories = await loadMemoriesFromDir(otherPaths.memoriesDir);
|
|
93
|
+
const tag = path.basename(otherRoot);
|
|
94
|
+
for (const m of otherMemories) {
|
|
95
|
+
ownMemories.push({ ...m, origin: tag });
|
|
96
|
+
}
|
|
97
|
+
externalRoots.push(`${tag} (${otherMemories.length})`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
ui.warn(`--include ${includePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (externalRoots.length > 0) {
|
|
103
|
+
ui.info(`merged from: ${externalRoots.join(", ")}`);
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const all = ownMemories;
|
|
75
108
|
const filePaths = parseCsv(opts.files);
|
|
76
109
|
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
77
110
|
const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
|
|
@@ -148,15 +181,16 @@ function registerBriefing(program2) {
|
|
|
148
181
|
}
|
|
149
182
|
console.log(`${ui.bold("=== Relevant Memories ===")}
|
|
150
183
|
`);
|
|
151
|
-
for (const
|
|
152
|
-
const fm =
|
|
184
|
+
for (const item of top) {
|
|
185
|
+
const fm = item.memory.frontmatter;
|
|
153
186
|
const badge = ui.statusBadge(fm.status);
|
|
154
187
|
const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
|
|
155
188
|
const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
|
|
189
|
+
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
156
190
|
console.log(
|
|
157
|
-
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}`
|
|
191
|
+
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}`
|
|
158
192
|
);
|
|
159
|
-
console.log(
|
|
193
|
+
console.log(item.memory.body.trim());
|
|
160
194
|
console.log();
|
|
161
195
|
}
|
|
162
196
|
console.log(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
@@ -199,6 +233,9 @@ function parseCsv(value) {
|
|
|
199
233
|
if (!value) return [];
|
|
200
234
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
201
235
|
}
|
|
236
|
+
function collectInclude(value, previous) {
|
|
237
|
+
return [...previous, value];
|
|
238
|
+
}
|
|
202
239
|
|
|
203
240
|
// src/commands/tui.ts
|
|
204
241
|
import "commander";
|
|
@@ -223,7 +260,7 @@ function registerTui(program2) {
|
|
|
223
260
|
|
|
224
261
|
// src/commands/embeddings.ts
|
|
225
262
|
import { existsSync as existsSync2 } from "fs";
|
|
226
|
-
import
|
|
263
|
+
import path2 from "path";
|
|
227
264
|
import "commander";
|
|
228
265
|
import { findProjectRoot as findProjectRoot3, resolveHaivePaths as resolveHaivePaths2 } from "@hiveai/core";
|
|
229
266
|
function registerEmbeddings(program2) {
|
|
@@ -265,22 +302,22 @@ function registerEmbeddings(program2) {
|
|
|
265
302
|
for (const hit of result.hits) {
|
|
266
303
|
const score = hit.score.toFixed(3);
|
|
267
304
|
console.log(`${ui.bold(score)} ${hit.id}`);
|
|
268
|
-
console.log(` ${ui.dim(
|
|
305
|
+
console.log(` ${ui.dim(path2.relative(root, hit.file_path))}`);
|
|
269
306
|
}
|
|
270
307
|
});
|
|
271
308
|
embeddings.command("status").description("Show the embeddings index status").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
272
309
|
const root = findProjectRoot3(opts.dir);
|
|
273
310
|
const paths = resolveHaivePaths2(root);
|
|
274
311
|
const { indexStat } = await loadEmbeddings();
|
|
275
|
-
const
|
|
276
|
-
if (!
|
|
312
|
+
const stat2 = await indexStat(paths);
|
|
313
|
+
if (!stat2.exists) {
|
|
277
314
|
ui.warn("No embeddings index. Run `haive embeddings index` to create one.");
|
|
278
315
|
return;
|
|
279
316
|
}
|
|
280
|
-
console.log(`${ui.bold("entries:")} ${
|
|
281
|
-
console.log(`${ui.bold("model:")} ${
|
|
282
|
-
console.log(`${ui.bold("updated_at:")} ${
|
|
283
|
-
console.log(`${ui.bold("size:")} ${(
|
|
317
|
+
console.log(`${ui.bold("entries:")} ${stat2.count}`);
|
|
318
|
+
console.log(`${ui.bold("model:")} ${stat2.model}`);
|
|
319
|
+
console.log(`${ui.bold("updated_at:")} ${stat2.updatedAt}`);
|
|
320
|
+
console.log(`${ui.bold("size:")} ${(stat2.sizeBytes / 1024).toFixed(1)} KB`);
|
|
284
321
|
});
|
|
285
322
|
}
|
|
286
323
|
async function loadEmbeddings() {
|
|
@@ -295,7 +332,7 @@ async function loadEmbeddings() {
|
|
|
295
332
|
}
|
|
296
333
|
|
|
297
334
|
// src/commands/index-code.ts
|
|
298
|
-
import
|
|
335
|
+
import path3 from "path";
|
|
299
336
|
import "commander";
|
|
300
337
|
import {
|
|
301
338
|
buildCodeMap,
|
|
@@ -338,7 +375,7 @@ function registerIndexCode(program2) {
|
|
|
338
375
|
const fileCount = Object.keys(map.files).length;
|
|
339
376
|
const exportCount = Object.values(map.files).reduce((s, f) => s + f.exports.length, 0);
|
|
340
377
|
ui.success(
|
|
341
|
-
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${
|
|
378
|
+
`Indexed ${fileCount} file(s) with ${exportCount} export(s) \u2192 ${path3.relative(root, codeMapPath(paths))}`
|
|
342
379
|
);
|
|
343
380
|
});
|
|
344
381
|
idx.command("code-search").description(
|
|
@@ -371,9 +408,9 @@ function registerIndexCode(program2) {
|
|
|
371
408
|
}
|
|
372
409
|
|
|
373
410
|
// src/commands/init.ts
|
|
374
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
375
|
-
import { existsSync as
|
|
376
|
-
import
|
|
411
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
412
|
+
import { existsSync as existsSync6 } from "fs";
|
|
413
|
+
import path7 from "path";
|
|
377
414
|
import { spawnSync } from "child_process";
|
|
378
415
|
import "commander";
|
|
379
416
|
import {
|
|
@@ -383,9 +420,719 @@ import {
|
|
|
383
420
|
saveCodeMap as saveCodeMap2,
|
|
384
421
|
saveConfig
|
|
385
422
|
} from "@hiveai/core";
|
|
423
|
+
|
|
424
|
+
// src/commands/init-bootstrap.ts
|
|
425
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
426
|
+
import { existsSync as existsSync3 } from "fs";
|
|
427
|
+
import path4 from "path";
|
|
428
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
429
|
+
"node_modules",
|
|
430
|
+
"dist",
|
|
431
|
+
"build",
|
|
432
|
+
".next",
|
|
433
|
+
".nuxt",
|
|
434
|
+
".svelte-kit",
|
|
435
|
+
".git",
|
|
436
|
+
"coverage",
|
|
437
|
+
".turbo",
|
|
438
|
+
"out",
|
|
439
|
+
".cache",
|
|
440
|
+
"tmp",
|
|
441
|
+
"temp",
|
|
442
|
+
"__pycache__",
|
|
443
|
+
".venv",
|
|
444
|
+
"venv",
|
|
445
|
+
"target",
|
|
446
|
+
".gradle"
|
|
447
|
+
]);
|
|
448
|
+
var FRAMEWORK_SIGNALS = {
|
|
449
|
+
"NestJS": ["@nestjs/core", "@nestjs/common"],
|
|
450
|
+
"Next.js": ["next"],
|
|
451
|
+
"Remix": ["@remix-run/react", "@remix-run/node"],
|
|
452
|
+
"React": ["react", "react-dom"],
|
|
453
|
+
"Vue": ["vue"],
|
|
454
|
+
"Svelte": ["svelte"],
|
|
455
|
+
"SvelteKit": ["@sveltejs/kit"],
|
|
456
|
+
"Astro": ["astro"],
|
|
457
|
+
"Express": ["express"],
|
|
458
|
+
"Fastify": ["fastify"],
|
|
459
|
+
"Hono": ["hono"],
|
|
460
|
+
"tRPC": ["@trpc/server"],
|
|
461
|
+
"Prisma": ["@prisma/client"],
|
|
462
|
+
"Drizzle": ["drizzle-orm"],
|
|
463
|
+
"Vite": ["vite"],
|
|
464
|
+
"Vitest": ["vitest"],
|
|
465
|
+
"Jest": ["jest"]
|
|
466
|
+
};
|
|
467
|
+
var KEY_DEPS = [
|
|
468
|
+
"@nestjs/jwt",
|
|
469
|
+
"@nestjs/passport",
|
|
470
|
+
"passport-jwt",
|
|
471
|
+
"jsonwebtoken",
|
|
472
|
+
"bcrypt",
|
|
473
|
+
"bcryptjs",
|
|
474
|
+
"stripe",
|
|
475
|
+
"axios",
|
|
476
|
+
"socket.io",
|
|
477
|
+
"ws",
|
|
478
|
+
"redis",
|
|
479
|
+
"ioredis",
|
|
480
|
+
"pg",
|
|
481
|
+
"mysql2",
|
|
482
|
+
"mongodb",
|
|
483
|
+
"mongoose",
|
|
484
|
+
"zod",
|
|
485
|
+
"yup",
|
|
486
|
+
"class-validator",
|
|
487
|
+
"tailwindcss",
|
|
488
|
+
"shadcn",
|
|
489
|
+
"@radix-ui",
|
|
490
|
+
"@vercel/ai",
|
|
491
|
+
"ai",
|
|
492
|
+
"openai",
|
|
493
|
+
"@anthropic-ai/sdk",
|
|
494
|
+
"typescript"
|
|
495
|
+
];
|
|
496
|
+
function detectFrameworks(allDeps) {
|
|
497
|
+
const found = [];
|
|
498
|
+
for (const [fw, signals] of Object.entries(FRAMEWORK_SIGNALS)) {
|
|
499
|
+
if (signals.some((s) => allDeps[s] !== void 0)) found.push(fw);
|
|
500
|
+
}
|
|
501
|
+
return found;
|
|
502
|
+
}
|
|
503
|
+
function detectKeyDeps(allDeps) {
|
|
504
|
+
return KEY_DEPS.filter((d) => allDeps[d] !== void 0);
|
|
505
|
+
}
|
|
506
|
+
function detectLanguage(root) {
|
|
507
|
+
if (existsSync3(path4.join(root, "tsconfig.json"))) return "TypeScript";
|
|
508
|
+
if (existsSync3(path4.join(root, "pyproject.toml")) || existsSync3(path4.join(root, "setup.py"))) return "Python";
|
|
509
|
+
if (existsSync3(path4.join(root, "go.mod"))) return "Go";
|
|
510
|
+
if (existsSync3(path4.join(root, "pom.xml")) || existsSync3(path4.join(root, "build.gradle"))) return "Java/Kotlin";
|
|
511
|
+
if (existsSync3(path4.join(root, "Cargo.toml"))) return "Rust";
|
|
512
|
+
if (existsSync3(path4.join(root, "package.json"))) return "JavaScript";
|
|
513
|
+
return "Unknown";
|
|
514
|
+
}
|
|
515
|
+
function detectProjectType(frameworks, scripts) {
|
|
516
|
+
if (frameworks.includes("NestJS")) return "Backend API (NestJS)";
|
|
517
|
+
if (frameworks.includes("Next.js")) return "Full-stack web app (Next.js)";
|
|
518
|
+
if (frameworks.includes("Remix")) return "Full-stack web app (Remix)";
|
|
519
|
+
if (frameworks.includes("Express") || frameworks.includes("Fastify") || frameworks.includes("Hono")) return "Backend API";
|
|
520
|
+
if (frameworks.includes("React") || frameworks.includes("Vue") || frameworks.includes("Svelte")) return "Frontend SPA";
|
|
521
|
+
if (scripts["build"] && !scripts["dev"]) return "CLI tool / library";
|
|
522
|
+
if (existsSync3("pom.xml")) return "Java backend";
|
|
523
|
+
return "Application";
|
|
524
|
+
}
|
|
525
|
+
async function scanDirs(root, maxDepth = 2) {
|
|
526
|
+
const results = [];
|
|
527
|
+
async function walk(dir, depth) {
|
|
528
|
+
if (depth > maxDepth) return;
|
|
529
|
+
let entries;
|
|
530
|
+
try {
|
|
531
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
532
|
+
} catch {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
for (const entry of entries) {
|
|
536
|
+
if (!entry.isDirectory()) continue;
|
|
537
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
538
|
+
const rel = path4.relative(root, path4.join(dir, entry.name));
|
|
539
|
+
results.push(rel);
|
|
540
|
+
await walk(path4.join(dir, entry.name), depth + 1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
await walk(root, 0);
|
|
544
|
+
return results;
|
|
545
|
+
}
|
|
546
|
+
function inferModuleDescriptions(dirs) {
|
|
547
|
+
const known = {
|
|
548
|
+
"src": "main source directory",
|
|
549
|
+
"app": "application entrypoint / routes (Next.js App Router or similar)",
|
|
550
|
+
"pages": "file-based routing pages",
|
|
551
|
+
"components": "reusable UI components",
|
|
552
|
+
"lib": "shared utilities and helpers",
|
|
553
|
+
"utils": "utility functions",
|
|
554
|
+
"hooks": "React hooks",
|
|
555
|
+
"services": "business logic services",
|
|
556
|
+
"controllers": "HTTP controllers / route handlers",
|
|
557
|
+
"modules": "feature modules",
|
|
558
|
+
"middleware": "HTTP or business middleware",
|
|
559
|
+
"guards": "auth / access guards",
|
|
560
|
+
"prisma": "Prisma schema and migrations",
|
|
561
|
+
"migrations": "database migrations",
|
|
562
|
+
"config": "configuration files",
|
|
563
|
+
"types": "TypeScript type definitions",
|
|
564
|
+
"schemas": "validation schemas (Zod / class-validator)",
|
|
565
|
+
"test": "tests",
|
|
566
|
+
"tests": "tests",
|
|
567
|
+
"__tests__": "tests",
|
|
568
|
+
"e2e": "end-to-end tests",
|
|
569
|
+
"public": "static public assets",
|
|
570
|
+
"assets": "static assets",
|
|
571
|
+
"styles": "global CSS / style files",
|
|
572
|
+
"scripts": "build or utility scripts",
|
|
573
|
+
"docs": "documentation",
|
|
574
|
+
"docker": "Docker configuration",
|
|
575
|
+
"infra": "infrastructure / IaC",
|
|
576
|
+
"packages": "monorepo sub-packages",
|
|
577
|
+
"functions": "serverless / edge functions",
|
|
578
|
+
"api": "API routes or client",
|
|
579
|
+
"store": "state management (Redux / Zustand / Pinia)",
|
|
580
|
+
"context": "React contexts",
|
|
581
|
+
"server": "server-side code",
|
|
582
|
+
"client": "client-side code",
|
|
583
|
+
"features": "feature-based modules",
|
|
584
|
+
"routes": "route definitions",
|
|
585
|
+
"workers": "background workers / queues"
|
|
586
|
+
};
|
|
587
|
+
const top = dirs.filter((d) => !d.includes("/")).slice(0, 12);
|
|
588
|
+
return top.map((d) => {
|
|
589
|
+
const desc = known[d.toLowerCase()] ?? "module";
|
|
590
|
+
return `- \`${d}/\` \u2014 ${desc}`;
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function readmeExcerpt(readme) {
|
|
594
|
+
const lines = readme.split("\n");
|
|
595
|
+
let inContent = false;
|
|
596
|
+
const kept = [];
|
|
597
|
+
for (const line of lines) {
|
|
598
|
+
if (!inContent && line.trim().startsWith("#")) {
|
|
599
|
+
inContent = true;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!inContent) continue;
|
|
603
|
+
if (kept.length >= 6) break;
|
|
604
|
+
if (line.trim()) kept.push(line.trim());
|
|
605
|
+
}
|
|
606
|
+
return kept.join(" ").slice(0, 400);
|
|
607
|
+
}
|
|
608
|
+
async function generateBootstrapContext(root) {
|
|
609
|
+
let pkg = {};
|
|
610
|
+
const pkgPath = path4.join(root, "package.json");
|
|
611
|
+
if (existsSync3(pkgPath)) {
|
|
612
|
+
try {
|
|
613
|
+
pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
618
|
+
const frameworks = detectFrameworks(allDeps);
|
|
619
|
+
const keyDeps = detectKeyDeps(allDeps);
|
|
620
|
+
const language = detectLanguage(root);
|
|
621
|
+
const projectType = detectProjectType(frameworks, pkg.scripts ?? {});
|
|
622
|
+
const projectName = pkg.name ?? path4.basename(root);
|
|
623
|
+
const projectDesc = pkg.description ?? "";
|
|
624
|
+
let readmeSummary = "";
|
|
625
|
+
for (const name of ["README.md", "readme.md", "README"]) {
|
|
626
|
+
const p = path4.join(root, name);
|
|
627
|
+
if (existsSync3(p)) {
|
|
628
|
+
try {
|
|
629
|
+
const content = await readFile2(p, "utf8");
|
|
630
|
+
readmeSummary = readmeExcerpt(content);
|
|
631
|
+
break;
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const dirs = await scanDirs(root, 2);
|
|
637
|
+
const moduleLines = inferModuleDescriptions(dirs);
|
|
638
|
+
const scripts = pkg.scripts ?? {};
|
|
639
|
+
const scriptLines = Object.entries(scripts).filter(([k]) => ["build", "dev", "start", "test", "lint", "deploy"].includes(k)).map(([k, v]) => `- \`${k}\`: ${v}`).slice(0, 6);
|
|
640
|
+
const stackParts = [language];
|
|
641
|
+
if (frameworks.length) stackParts.push(...frameworks);
|
|
642
|
+
const techStack = stackParts.join(", ");
|
|
643
|
+
const notableDeps = Object.keys(allDeps).filter((d) => !d.startsWith("@types/") && !["typescript", "eslint", "prettier", "jest"].includes(d)).filter((d) => !["react", "react-dom", "next", "vue", "express"].includes(d)).slice(0, 10).map((d) => `\`${d}\``);
|
|
644
|
+
const lines = [
|
|
645
|
+
`# Project context \u2014 ${projectName}`,
|
|
646
|
+
"",
|
|
647
|
+
`> Auto-generated by \`haive init --bootstrap\`. Review and refine \u2014 especially the Architecture and Gotchas sections.`,
|
|
648
|
+
"",
|
|
649
|
+
`## Overview`,
|
|
650
|
+
`**Type:** ${projectType}`,
|
|
651
|
+
`**Tech stack:** ${techStack}`,
|
|
652
|
+
...projectDesc ? [`**Description:** ${projectDesc}`] : [],
|
|
653
|
+
...readmeSummary ? [`**From README:** ${readmeSummary}`] : [],
|
|
654
|
+
"",
|
|
655
|
+
`## Architecture`,
|
|
656
|
+
`TODO \u2014 fill in the high-level architecture (inferred structure below, verify manually):`,
|
|
657
|
+
"",
|
|
658
|
+
...moduleLines.length ? moduleLines : ["TODO \u2014 no clear structure detected."],
|
|
659
|
+
"",
|
|
660
|
+
`## Key modules`,
|
|
661
|
+
`TODO \u2014 describe the purpose of the main modules. The directory scan found:`,
|
|
662
|
+
...dirs.filter((d) => !d.includes("/")).slice(0, 8).map((d) => `- \`${d}/\``),
|
|
663
|
+
"",
|
|
664
|
+
`## Conventions`,
|
|
665
|
+
`TODO \u2014 fill in coding conventions (naming, patterns, file layout).`,
|
|
666
|
+
"",
|
|
667
|
+
...scriptLines.length ? [
|
|
668
|
+
`**Available scripts:**`,
|
|
669
|
+
...scriptLines,
|
|
670
|
+
""
|
|
671
|
+
] : [],
|
|
672
|
+
...keyDeps.length ? [
|
|
673
|
+
`**Key dependencies in use:** ${keyDeps.map((d) => `\`${d}\``).join(", ")}`,
|
|
674
|
+
""
|
|
675
|
+
] : [],
|
|
676
|
+
...notableDeps.length ? [
|
|
677
|
+
`**Other notable packages:** ${notableDeps.join(", ")}`,
|
|
678
|
+
""
|
|
679
|
+
] : [],
|
|
680
|
+
`## Glossary`,
|
|
681
|
+
`TODO \u2014 domain terms and what they mean here.`,
|
|
682
|
+
"",
|
|
683
|
+
`## Gotchas`,
|
|
684
|
+
`TODO \u2014 known traps, surprising behavior, things newcomers stub their toes on.`,
|
|
685
|
+
`(Run \`haive memory import-changelog\` or \`haive memory import README.md\` to seed these automatically.)`,
|
|
686
|
+
""
|
|
687
|
+
];
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/commands/init-mcp-setup.ts
|
|
692
|
+
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
693
|
+
import { existsSync as existsSync4 } from "fs";
|
|
694
|
+
import path5 from "path";
|
|
695
|
+
import os from "os";
|
|
696
|
+
var HOME = os.homedir();
|
|
697
|
+
var HAIVE_MCP_ENTRY = {
|
|
698
|
+
command: "haive-mcp",
|
|
699
|
+
args: []
|
|
700
|
+
};
|
|
701
|
+
function cursorMcpPath() {
|
|
702
|
+
return path5.join(HOME, ".cursor", "mcp.json");
|
|
703
|
+
}
|
|
704
|
+
async function configureCursor() {
|
|
705
|
+
const mcpPath = cursorMcpPath();
|
|
706
|
+
const cursorDir = path5.join(HOME, ".cursor");
|
|
707
|
+
if (!existsSync4(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
708
|
+
let config = {};
|
|
709
|
+
if (existsSync4(mcpPath)) {
|
|
710
|
+
try {
|
|
711
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
config.mcpServers ??= {};
|
|
716
|
+
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
717
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
718
|
+
await mkdir(cursorDir, { recursive: true });
|
|
719
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
720
|
+
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
721
|
+
}
|
|
722
|
+
function vscodeMcpPath() {
|
|
723
|
+
const candidates = [
|
|
724
|
+
path5.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
725
|
+
// Linux
|
|
726
|
+
path5.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
727
|
+
// macOS
|
|
728
|
+
path5.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
729
|
+
// Windows
|
|
730
|
+
path5.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
731
|
+
];
|
|
732
|
+
for (const c of candidates) {
|
|
733
|
+
if (existsSync4(path5.dirname(c))) return c;
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
async function configureVSCode() {
|
|
738
|
+
const mcpPath = vscodeMcpPath();
|
|
739
|
+
if (!mcpPath) return { client: "VS Code", status: "not_installed" };
|
|
740
|
+
let config = {};
|
|
741
|
+
if (existsSync4(mcpPath)) {
|
|
742
|
+
try {
|
|
743
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
744
|
+
} catch {
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
config.servers ??= {};
|
|
748
|
+
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
749
|
+
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
750
|
+
await mkdir(path5.dirname(mcpPath), { recursive: true });
|
|
751
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
752
|
+
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
753
|
+
}
|
|
754
|
+
function claudeConfigPath() {
|
|
755
|
+
const p = path5.join(HOME, ".claude.json");
|
|
756
|
+
if (existsSync4(p)) return p;
|
|
757
|
+
const p2 = path5.join(HOME, ".config", "claude", "claude.json");
|
|
758
|
+
if (existsSync4(path5.dirname(p2))) return p2;
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
async function configureClaude() {
|
|
762
|
+
const cfgPath = claudeConfigPath() ?? path5.join(HOME, ".claude.json");
|
|
763
|
+
if (!existsSync4(cfgPath) && !existsSync4(path5.join(HOME, ".claude"))) {
|
|
764
|
+
return { client: "Claude Code", status: "not_installed" };
|
|
765
|
+
}
|
|
766
|
+
let config = {};
|
|
767
|
+
if (existsSync4(cfgPath)) {
|
|
768
|
+
try {
|
|
769
|
+
config = JSON.parse(await readFile3(cfgPath, "utf8"));
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
config.mcpServers ??= {};
|
|
774
|
+
if (config.mcpServers["haive"]) return { client: "Claude Code", status: "already_configured" };
|
|
775
|
+
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
776
|
+
await writeFile(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
777
|
+
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
778
|
+
}
|
|
779
|
+
function windsurfMcpPath() {
|
|
780
|
+
const candidates = [
|
|
781
|
+
path5.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
782
|
+
path5.join(HOME, ".windsurf", "mcp.json")
|
|
783
|
+
];
|
|
784
|
+
for (const c of candidates) {
|
|
785
|
+
if (existsSync4(path5.dirname(c))) return c;
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
async function configureWindsurf() {
|
|
790
|
+
const mcpPath = windsurfMcpPath();
|
|
791
|
+
if (!mcpPath) return { client: "Windsurf", status: "not_installed" };
|
|
792
|
+
let config = {};
|
|
793
|
+
if (existsSync4(mcpPath)) {
|
|
794
|
+
try {
|
|
795
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
config.mcpServers ??= {};
|
|
800
|
+
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
801
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
802
|
+
await mkdir(path5.dirname(mcpPath), { recursive: true });
|
|
803
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
804
|
+
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
805
|
+
}
|
|
806
|
+
async function autoConfigureMcpClients() {
|
|
807
|
+
const results = [];
|
|
808
|
+
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
809
|
+
for (const fn of configurators) {
|
|
810
|
+
try {
|
|
811
|
+
results.push(await fn());
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const name = fn.name.replace("configure", "");
|
|
814
|
+
results.push({ client: name, status: "error", error: String(err) });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return results;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/commands/init-stack-packs.ts
|
|
821
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
822
|
+
import { existsSync as existsSync5 } from "fs";
|
|
823
|
+
import path6 from "path";
|
|
824
|
+
import {
|
|
825
|
+
buildFrontmatter,
|
|
826
|
+
memoryFilePath,
|
|
827
|
+
serializeMemory
|
|
828
|
+
} from "@hiveai/core";
|
|
829
|
+
var PACKS = {
|
|
830
|
+
nestjs: [
|
|
831
|
+
{
|
|
832
|
+
slug: "jwtmodule-requires-secret",
|
|
833
|
+
type: "gotcha",
|
|
834
|
+
tags: ["auth", "jwt", "nestjs"],
|
|
835
|
+
body: `JwtModule must be registered with an explicit secret \u2014 there is no default.
|
|
836
|
+
|
|
837
|
+
\`\`\`ts
|
|
838
|
+
JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '7d' } })
|
|
839
|
+
\`\`\`
|
|
840
|
+
|
|
841
|
+
Without a secret, tokens are signed with an empty string and any client can forge them.
|
|
842
|
+
Always load the secret from env and validate it is defined at startup.`
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
slug: "global-validation-pipe",
|
|
846
|
+
type: "convention",
|
|
847
|
+
tags: ["validation", "nestjs", "security"],
|
|
848
|
+
body: `Register ValidationPipe globally in main.ts, not per-controller.
|
|
849
|
+
|
|
850
|
+
\`\`\`ts
|
|
851
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
|
|
852
|
+
\`\`\`
|
|
853
|
+
|
|
854
|
+
- \`whitelist: true\` strips unknown properties silently
|
|
855
|
+
- \`forbidNonWhitelisted: true\` throws 400 on unknown fields (safer)
|
|
856
|
+
- Without this, NestJS passes unvalidated payloads to handlers.`
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
slug: "nestjs-no-direct-orm-in-controller",
|
|
860
|
+
type: "convention",
|
|
861
|
+
tags: ["architecture", "nestjs"],
|
|
862
|
+
body: `Controllers must never import Prisma/TypeORM directly \u2014 that belongs in Services.
|
|
863
|
+
|
|
864
|
+
Controller \u2192 Service \u2192 Repository (or direct ORM) is the required layering.
|
|
865
|
+
Direct ORM usage in controllers makes testing impossible and couples transport to persistence.`
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
slug: "nestjs-exception-filter-for-prisma",
|
|
869
|
+
type: "gotcha",
|
|
870
|
+
tags: ["error-handling", "nestjs", "prisma"],
|
|
871
|
+
body: `Prisma errors bubble up as unhandled 500s without a custom exception filter.
|
|
872
|
+
|
|
873
|
+
Create an \`AllExceptionsFilter\` or a specific \`PrismaClientExceptionFilter\` that maps:
|
|
874
|
+
- P2002 (unique constraint) \u2192 409 Conflict
|
|
875
|
+
- P2025 (record not found) \u2192 404 Not Found
|
|
876
|
+
- P2003 (foreign key) \u2192 422 Unprocessable
|
|
877
|
+
|
|
878
|
+
Without this, clients receive raw Prisma error messages which may leak schema info.`
|
|
879
|
+
}
|
|
880
|
+
],
|
|
881
|
+
nextjs: [
|
|
882
|
+
{
|
|
883
|
+
slug: "server-components-no-client-hooks",
|
|
884
|
+
type: "gotcha",
|
|
885
|
+
tags: ["nextjs", "react", "server-components"],
|
|
886
|
+
body: `Server Components cannot use useState, useEffect, or any browser APIs.
|
|
887
|
+
|
|
888
|
+
Add \`"use client"\` at the top of any component that needs hooks or event handlers.
|
|
889
|
+
The boundary propagates down \u2014 children of a client component don't need the directive.
|
|
890
|
+
|
|
891
|
+
Common mistake: importing a client-only library (e.g. framer-motion) in a server component
|
|
892
|
+
causes a cryptic runtime error. Check for browser globals (window, document, localStorage).`
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
slug: "nextjs-env-client-exposure",
|
|
896
|
+
type: "gotcha",
|
|
897
|
+
tags: ["security", "nextjs", "env"],
|
|
898
|
+
body: `Only environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser.
|
|
899
|
+
|
|
900
|
+
Never put secrets in NEXT_PUBLIC_* variables \u2014 they are bundled into the client JS.
|
|
901
|
+
Variables without the prefix are server-only and safe for API keys, database URLs, etc.`
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
slug: "nextjs-fetch-cache-defaults",
|
|
905
|
+
type: "gotcha",
|
|
906
|
+
tags: ["nextjs", "caching", "fetch"],
|
|
907
|
+
body: `In Next.js App Router, \`fetch()\` is cached indefinitely by default in Server Components.
|
|
908
|
+
|
|
909
|
+
Add \`{ cache: 'no-store' }\` for dynamic data, or \`{ next: { revalidate: 60 } }\` for ISR.
|
|
910
|
+
Forgetting this means stale data is returned after a deploy until the cache expires.`
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
slug: "nextjs-metadata-api",
|
|
914
|
+
type: "convention",
|
|
915
|
+
tags: ["nextjs", "seo"],
|
|
916
|
+
body: `Use the Metadata API (export const metadata / generateMetadata) instead of <Head>.
|
|
917
|
+
|
|
918
|
+
\`<Head>\` from next/head still works in pages/ but is not supported in the App Router.
|
|
919
|
+
Use \`generateMetadata\` for dynamic titles/descriptions based on route params.`
|
|
920
|
+
}
|
|
921
|
+
],
|
|
922
|
+
remix: [
|
|
923
|
+
{
|
|
924
|
+
slug: "remix-loader-vs-action",
|
|
925
|
+
type: "convention",
|
|
926
|
+
tags: ["remix", "architecture"],
|
|
927
|
+
body: `loader = GET data for rendering. action = handle form submissions / mutations.
|
|
928
|
+
|
|
929
|
+
- \`loader\` runs on every GET request (server-side, returns data for the component)
|
|
930
|
+
- \`action\` runs on POST/PUT/DELETE (mutations \u2014 redirect after success)
|
|
931
|
+
- Never fetch inside the component itself for route data \u2014 use the loader instead.`
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
slug: "remix-error-boundaries",
|
|
935
|
+
type: "gotcha",
|
|
936
|
+
tags: ["remix", "error-handling"],
|
|
937
|
+
body: `Each route should export an ErrorBoundary to catch loader/action errors gracefully.
|
|
938
|
+
|
|
939
|
+
Without it, errors bubble to the root boundary and replace the entire page.
|
|
940
|
+
Export \`export function ErrorBoundary() { ... }\` to scope errors to the route.`
|
|
941
|
+
}
|
|
942
|
+
],
|
|
943
|
+
react: [
|
|
944
|
+
{
|
|
945
|
+
slug: "useeffect-cleanup",
|
|
946
|
+
type: "gotcha",
|
|
947
|
+
tags: ["react", "memory-leak"],
|
|
948
|
+
body: `useEffect subscriptions, timers, and async operations need cleanup to avoid memory leaks.
|
|
949
|
+
|
|
950
|
+
\`\`\`ts
|
|
951
|
+
useEffect(() => {
|
|
952
|
+
const controller = new AbortController();
|
|
953
|
+
fetchData({ signal: controller.signal });
|
|
954
|
+
return () => controller.abort(); // cleanup
|
|
955
|
+
}, [dep]);
|
|
956
|
+
\`\`\`
|
|
957
|
+
|
|
958
|
+
Missing cleanup causes: state updates on unmounted components, duplicate subscriptions,
|
|
959
|
+
and event listeners that accumulate across re-renders.`
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
slug: "react-key-prop-in-lists",
|
|
963
|
+
type: "gotcha",
|
|
964
|
+
tags: ["react", "performance"],
|
|
965
|
+
body: `Keys must be stable, unique IDs \u2014 never use array index as key.
|
|
966
|
+
|
|
967
|
+
Using index as key causes React to re-render wrong items on reorder/filter,
|
|
968
|
+
corrupts form state, and triggers avoidable DOM mutations.
|
|
969
|
+
Use item.id or a stable hash \u2014 never Math.random().`
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
slug: "react-avoid-use-effect-for-derived-state",
|
|
973
|
+
type: "convention",
|
|
974
|
+
tags: ["react", "state"],
|
|
975
|
+
body: `Don't use useEffect to sync state from props \u2014 compute it during render instead.
|
|
976
|
+
|
|
977
|
+
\`\`\`ts
|
|
978
|
+
// \u274C Bad
|
|
979
|
+
const [fullName, setFullName] = useState('');
|
|
980
|
+
useEffect(() => { setFullName(first + ' ' + last); }, [first, last]);
|
|
981
|
+
|
|
982
|
+
// \u2705 Good
|
|
983
|
+
const fullName = first + ' ' + last; // derived during render
|
|
984
|
+
\`\`\``
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
express: [
|
|
988
|
+
{
|
|
989
|
+
slug: "express-missing-validation",
|
|
990
|
+
type: "gotcha",
|
|
991
|
+
tags: ["security", "express", "validation"],
|
|
992
|
+
body: `Express does not validate request bodies by default \u2014 always validate with zod, joi, or express-validator.
|
|
993
|
+
|
|
994
|
+
Without validation:
|
|
995
|
+
- req.body fields are \`any\` and may be missing, wrong type, or injected
|
|
996
|
+
- Downstream code crashes or processes malicious data
|
|
997
|
+
Add a validation middleware for every route that accepts user input.`
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
slug: "express-async-error-propagation",
|
|
1001
|
+
type: "gotcha",
|
|
1002
|
+
tags: ["express", "error-handling"],
|
|
1003
|
+
body: `Async route handlers don't propagate errors to error middleware without explicit next(err).
|
|
1004
|
+
|
|
1005
|
+
\`\`\`ts
|
|
1006
|
+
// \u274C Unhandled \u2014 Express never sees the rejection
|
|
1007
|
+
app.get('/', async (req, res) => { throw new Error('oops'); });
|
|
1008
|
+
|
|
1009
|
+
// \u2705 Correct
|
|
1010
|
+
app.get('/', async (req, res, next) => {
|
|
1011
|
+
try { await doWork(); }
|
|
1012
|
+
catch (err) { next(err); }
|
|
1013
|
+
});
|
|
1014
|
+
\`\`\`
|
|
1015
|
+
Or use express-async-errors / wrap helper.`
|
|
1016
|
+
}
|
|
1017
|
+
],
|
|
1018
|
+
fastify: [
|
|
1019
|
+
{
|
|
1020
|
+
slug: "fastify-schema-validation-required",
|
|
1021
|
+
type: "convention",
|
|
1022
|
+
tags: ["fastify", "validation", "security"],
|
|
1023
|
+
body: `Always define a JSON schema on routes \u2014 Fastify validates and coerces automatically.
|
|
1024
|
+
|
|
1025
|
+
\`\`\`ts
|
|
1026
|
+
fastify.post('/users', {
|
|
1027
|
+
schema: { body: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' } } } }
|
|
1028
|
+
}, handler)
|
|
1029
|
+
\`\`\`
|
|
1030
|
+
Routes without schema accept any body and bypass Fastify's fast-json-stringify serialization.`
|
|
1031
|
+
}
|
|
1032
|
+
],
|
|
1033
|
+
prisma: [
|
|
1034
|
+
{
|
|
1035
|
+
slug: "prisma-no-disconnect-in-lambda",
|
|
1036
|
+
type: "gotcha",
|
|
1037
|
+
tags: ["prisma", "serverless"],
|
|
1038
|
+
body: `Do NOT call prisma.$disconnect() inside Lambda/Edge function handlers.
|
|
1039
|
+
|
|
1040
|
+
Calling $disconnect() after each request wastes the warm connection pool.
|
|
1041
|
+
Create one PrismaClient per process (module-level singleton), not per request.
|
|
1042
|
+
Disconnecting is only needed when the process is shutting down.`
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
slug: "prisma-migrations-never-modify",
|
|
1046
|
+
type: "convention",
|
|
1047
|
+
tags: ["prisma", "database", "migrations"],
|
|
1048
|
+
body: `Never modify an existing migration file \u2014 create a new one instead.
|
|
1049
|
+
|
|
1050
|
+
Prisma tracks migration history by file hash. Editing a deployed migration
|
|
1051
|
+
causes \`migrate deploy\` to fail with a checksum mismatch in production.
|
|
1052
|
+
Always use \`npx prisma migrate dev --name <description>\` to create incremental migrations.`
|
|
1053
|
+
}
|
|
1054
|
+
],
|
|
1055
|
+
drizzle: [
|
|
1056
|
+
{
|
|
1057
|
+
slug: "drizzle-always-await-queries",
|
|
1058
|
+
type: "gotcha",
|
|
1059
|
+
tags: ["drizzle", "async"],
|
|
1060
|
+
body: `Drizzle queries are thenable but not auto-executed \u2014 always await them.
|
|
1061
|
+
|
|
1062
|
+
\`\`\`ts
|
|
1063
|
+
// \u274C Silently returns a query builder, never executes
|
|
1064
|
+
const rows = db.select().from(users).where(eq(users.id, id));
|
|
1065
|
+
|
|
1066
|
+
// \u2705 Correct
|
|
1067
|
+
const rows = await db.select().from(users).where(eq(users.id, id));
|
|
1068
|
+
\`\`\``
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
slug: "drizzle-schema-must-match-db",
|
|
1072
|
+
type: "gotcha",
|
|
1073
|
+
tags: ["drizzle", "migrations"],
|
|
1074
|
+
body: `Drizzle does NOT auto-sync the schema to the database \u2014 you must run migrations explicitly.
|
|
1075
|
+
|
|
1076
|
+
After changing schema.ts:
|
|
1077
|
+
1. \`npx drizzle-kit generate\` \u2014 creates migration SQL
|
|
1078
|
+
2. \`npx drizzle-kit migrate\` (or push in dev) \u2014 applies it
|
|
1079
|
+
|
|
1080
|
+
Without this, queries silently operate on stale column definitions and may return wrong data.`
|
|
1081
|
+
}
|
|
1082
|
+
]
|
|
1083
|
+
};
|
|
1084
|
+
var SUPPORTED_STACKS = Object.keys(PACKS);
|
|
1085
|
+
function isValidStack(name) {
|
|
1086
|
+
return name in PACKS;
|
|
1087
|
+
}
|
|
1088
|
+
function autoDetectStacks(deps) {
|
|
1089
|
+
const detected = [];
|
|
1090
|
+
const stackDetectors = [
|
|
1091
|
+
["nestjs", ["@nestjs/core"]],
|
|
1092
|
+
["nextjs", ["next"]],
|
|
1093
|
+
["remix", ["@remix-run/react", "@remix-run/node"]],
|
|
1094
|
+
["react", ["react"]],
|
|
1095
|
+
["express", ["express"]],
|
|
1096
|
+
["fastify", ["fastify"]],
|
|
1097
|
+
["prisma", ["@prisma/client", "prisma"]],
|
|
1098
|
+
["drizzle", ["drizzle-orm"]]
|
|
1099
|
+
];
|
|
1100
|
+
for (const [stack, signals] of stackDetectors) {
|
|
1101
|
+
if (signals.some((s) => s in deps)) detected.push(stack);
|
|
1102
|
+
}
|
|
1103
|
+
if (detected.includes("nextjs") || detected.includes("remix")) {
|
|
1104
|
+
return detected.filter((s) => s !== "react");
|
|
1105
|
+
}
|
|
1106
|
+
return detected;
|
|
1107
|
+
}
|
|
1108
|
+
async function seedStackPack(haivePaths, stack) {
|
|
1109
|
+
const memories = PACKS[stack];
|
|
1110
|
+
if (!memories) return 0;
|
|
1111
|
+
await mkdir2(haivePaths.teamDir, { recursive: true });
|
|
1112
|
+
let count = 0;
|
|
1113
|
+
for (const mem of memories) {
|
|
1114
|
+
const fm = buildFrontmatter({
|
|
1115
|
+
type: mem.type,
|
|
1116
|
+
slug: `${stack}-${mem.slug}`,
|
|
1117
|
+
scope: "team",
|
|
1118
|
+
status: "validated",
|
|
1119
|
+
tags: mem.tags
|
|
1120
|
+
});
|
|
1121
|
+
const filePath = memoryFilePath(haivePaths, "team", fm.id);
|
|
1122
|
+
if (existsSync5(filePath)) continue;
|
|
1123
|
+
const content = serializeMemory({ frontmatter: fm, body: mem.body });
|
|
1124
|
+
await mkdir2(path6.dirname(filePath), { recursive: true });
|
|
1125
|
+
await writeFile2(filePath, content, "utf8");
|
|
1126
|
+
count++;
|
|
1127
|
+
}
|
|
1128
|
+
return count;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/commands/init.ts
|
|
386
1132
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
387
1133
|
|
|
388
|
-
> Generated by \`haive init\`.
|
|
1134
|
+
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
1135
|
+
> or invoke the MCP prompt \`bootstrap_project\` in your AI client for a richer AI-generated version.
|
|
389
1136
|
|
|
390
1137
|
## Architecture
|
|
391
1138
|
TODO \u2014 high-level overview of the codebase.
|
|
@@ -543,23 +1290,46 @@ function registerInit(program2) {
|
|
|
543
1290
|
).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
|
|
544
1291
|
"--manual",
|
|
545
1292
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
1293
|
+
).option(
|
|
1294
|
+
"--bootstrap",
|
|
1295
|
+
"auto-generate .ai/project-context.md from package.json, README, and directory structure (no AI needed)"
|
|
1296
|
+
).option(
|
|
1297
|
+
"--stack <stacks>",
|
|
1298
|
+
`pre-seed validated memory packs for the given stacks (comma-separated).
|
|
1299
|
+
Supported: ${SUPPORTED_STACKS.join(", ")}.
|
|
1300
|
+
Use 'auto' to detect from package.json automatically.`
|
|
1301
|
+
).option(
|
|
1302
|
+
"--no-mcp-setup",
|
|
1303
|
+
"skip auto-configuring haive-mcp in Cursor / VS Code / Claude Code"
|
|
546
1304
|
).action(async (opts) => {
|
|
547
|
-
const root =
|
|
1305
|
+
const root = path7.resolve(opts.dir);
|
|
548
1306
|
const paths = resolveHaivePaths4(root);
|
|
549
1307
|
const autopilot = opts.manual !== true;
|
|
550
|
-
if (
|
|
1308
|
+
if (existsSync6(paths.haiveDir)) {
|
|
551
1309
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
552
1310
|
}
|
|
553
|
-
await
|
|
554
|
-
await
|
|
555
|
-
await
|
|
556
|
-
await
|
|
557
|
-
if (!
|
|
558
|
-
|
|
559
|
-
|
|
1311
|
+
await mkdir3(paths.personalDir, { recursive: true });
|
|
1312
|
+
await mkdir3(paths.teamDir, { recursive: true });
|
|
1313
|
+
await mkdir3(paths.moduleDir, { recursive: true });
|
|
1314
|
+
await mkdir3(paths.modulesContextDir, { recursive: true });
|
|
1315
|
+
if (!existsSync6(paths.projectContext)) {
|
|
1316
|
+
if (opts.bootstrap) {
|
|
1317
|
+
ui.info("Bootstrapping project context from local files\u2026");
|
|
1318
|
+
try {
|
|
1319
|
+
const context = await generateBootstrapContext(root);
|
|
1320
|
+
await writeFile3(paths.projectContext, context, "utf8");
|
|
1321
|
+
ui.success("Created .ai/project-context.md (auto-bootstrapped from local files)");
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
ui.warn(`Bootstrap failed (${String(err)}) \u2014 writing default template instead`);
|
|
1324
|
+
await writeFile3(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
await writeFile3(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
1328
|
+
ui.success(`Created ${path7.relative(root, paths.projectContext)}`);
|
|
1329
|
+
}
|
|
560
1330
|
}
|
|
561
|
-
const configExists =
|
|
562
|
-
|
|
1331
|
+
const configExists = existsSync6(
|
|
1332
|
+
path7.join(paths.haiveDir, "haive.config.json")
|
|
563
1333
|
);
|
|
564
1334
|
if (!configExists) {
|
|
565
1335
|
await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
|
|
@@ -570,17 +1340,35 @@ function registerInit(program2) {
|
|
|
570
1340
|
if (opts.bridges) {
|
|
571
1341
|
await writeBridge(root, "CLAUDE.md");
|
|
572
1342
|
await writeBridge(root, ".cursorrules");
|
|
573
|
-
await writeBridge(root,
|
|
1343
|
+
await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
|
|
1344
|
+
}
|
|
1345
|
+
const stacksToSeed = await resolveStacksToSeed(root, opts.stack);
|
|
1346
|
+
if (stacksToSeed.length > 0) {
|
|
1347
|
+
let totalSeeded = 0;
|
|
1348
|
+
for (const stack of stacksToSeed) {
|
|
1349
|
+
const count = await seedStackPack(paths, stack);
|
|
1350
|
+
if (count > 0) {
|
|
1351
|
+
ui.success(`Seeded ${count} memories for stack: ${stack}`);
|
|
1352
|
+
totalSeeded += count;
|
|
1353
|
+
} else {
|
|
1354
|
+
ui.info(`Stack pack '${stack}': all memories already exist \u2014 skipped`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (totalSeeded > 0) {
|
|
1358
|
+
ui.success(
|
|
1359
|
+
`${totalSeeded} validated team memories pre-seeded \u2014 haive is useful from J+0`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
574
1362
|
}
|
|
575
1363
|
const wantCi = opts.withCi || autopilot;
|
|
576
1364
|
if (wantCi) {
|
|
577
|
-
const ciPath =
|
|
578
|
-
if (
|
|
1365
|
+
const ciPath = path7.join(root, ".github", "workflows", "haive-sync.yml");
|
|
1366
|
+
if (existsSync6(ciPath)) {
|
|
579
1367
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
580
1368
|
} else {
|
|
581
|
-
await
|
|
582
|
-
await
|
|
583
|
-
ui.success(`Created ${
|
|
1369
|
+
await mkdir3(path7.dirname(ciPath), { recursive: true });
|
|
1370
|
+
await writeFile3(ciPath, CI_WORKFLOW, "utf8");
|
|
1371
|
+
ui.success(`Created ${path7.relative(root, ciPath)}`);
|
|
584
1372
|
}
|
|
585
1373
|
}
|
|
586
1374
|
if (autopilot) {
|
|
@@ -604,6 +1392,27 @@ function registerInit(program2) {
|
|
|
604
1392
|
ui.warn("Code-map build failed \u2014 run `haive index code` manually");
|
|
605
1393
|
}
|
|
606
1394
|
}
|
|
1395
|
+
if (opts.mcpSetup !== false) {
|
|
1396
|
+
const mcpResults = await autoConfigureMcpClients();
|
|
1397
|
+
const configured = mcpResults.filter((r) => r.status === "configured");
|
|
1398
|
+
const alreadyOk = mcpResults.filter((r) => r.status === "already_configured");
|
|
1399
|
+
for (const r of configured) {
|
|
1400
|
+
ui.success(`haive-mcp configured in ${r.client} (${r.path})`);
|
|
1401
|
+
}
|
|
1402
|
+
for (const r of alreadyOk) {
|
|
1403
|
+
ui.info(`haive-mcp already configured in ${r.client} \u2014 skipped`);
|
|
1404
|
+
}
|
|
1405
|
+
if (configured.length === 0 && alreadyOk.length === 0) {
|
|
1406
|
+
ui.warn(
|
|
1407
|
+
"No supported AI client detected (Cursor, VS Code, Claude Code, Windsurf).\n Configure manually: add haive-mcp to your client's MCP config.\n See: https://github.com/Doucs91/hAIve#mcp-setup"
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
if (configured.length > 0) {
|
|
1411
|
+
ui.info(
|
|
1412
|
+
ui.dim(" \u2192 Restart your AI client for MCP changes to take effect.")
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
607
1416
|
ui.success(`hAIve initialized at ${root}${autopilot ? " (autopilot mode)" : ""}`);
|
|
608
1417
|
console.log();
|
|
609
1418
|
if (autopilot) {
|
|
@@ -614,45 +1423,74 @@ function registerInit(program2) {
|
|
|
614
1423
|
console.log(ui.dim(" \u2713 Code-map refreshes automatically after every pull"));
|
|
615
1424
|
console.log(ui.dim(" \u2713 Git hooks installed (auto-sync after pull/merge)"));
|
|
616
1425
|
console.log(ui.dim(" \u2713 CI workflow created (pr-stale-check + sync-on-merge)"));
|
|
1426
|
+
if (stacksToSeed.length > 0) {
|
|
1427
|
+
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
1428
|
+
}
|
|
1429
|
+
console.log();
|
|
1430
|
+
if (!opts.bootstrap) {
|
|
1431
|
+
console.log(ui.bold("One remaining step (optional but recommended):"));
|
|
1432
|
+
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 fill project-context.md without AI"));
|
|
1433
|
+
console.log(" " + ui.dim("Or in your AI client: invoke the MCP prompt ") + ui.bold("bootstrap_project"));
|
|
1434
|
+
} else {
|
|
1435
|
+
console.log(ui.bold("Project context bootstrapped from local files."));
|
|
1436
|
+
console.log(ui.dim(" Review .ai/project-context.md and fill in the TODO sections."));
|
|
1437
|
+
console.log(ui.dim(" Or invoke the MCP prompt `bootstrap_project` for a richer AI-generated version."));
|
|
1438
|
+
}
|
|
617
1439
|
console.log();
|
|
618
|
-
console.log(ui.
|
|
619
|
-
console.log("
|
|
620
|
-
console.log(ui.dim("
|
|
1440
|
+
console.log(ui.dim(" Seed more memories instantly:"));
|
|
1441
|
+
console.log(ui.dim(" haive memory import-changelog \u2014 from CHANGELOG.md"));
|
|
1442
|
+
console.log(ui.dim(" haive memory import README.md \u2014 from README / docs"));
|
|
621
1443
|
} else {
|
|
622
1444
|
console.log(ui.bold("Next steps:"));
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1445
|
+
if (!opts.bootstrap) {
|
|
1446
|
+
console.log(ui.dim(" 1. Fill project context (pick one):"));
|
|
1447
|
+
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 instant, no AI needed"));
|
|
1448
|
+
console.log(" or invoke the MCP prompt " + ui.bold("bootstrap_project") + ui.dim(" in your AI client"));
|
|
1449
|
+
} else {
|
|
1450
|
+
console.log(ui.dim(" 1. Review .ai/project-context.md and fill in the TODO sections."));
|
|
1451
|
+
}
|
|
629
1452
|
console.log();
|
|
630
|
-
console.log(ui.dim("
|
|
1453
|
+
console.log(ui.dim(" 2. Start every AI session with:"));
|
|
631
1454
|
console.log(" " + ui.bold("get_briefing({ task: '\u2026what you are about to do\u2026' })"));
|
|
632
1455
|
console.log();
|
|
633
1456
|
console.log(ui.dim(" Tip: run `haive init` (without --manual) for zero-friction autopilot mode."));
|
|
634
1457
|
}
|
|
635
1458
|
});
|
|
636
1459
|
}
|
|
1460
|
+
async function resolveStacksToSeed(root, stackOpt) {
|
|
1461
|
+
if (!stackOpt) return [];
|
|
1462
|
+
if (stackOpt === "auto") {
|
|
1463
|
+
const pkgPath = path7.join(root, "package.json");
|
|
1464
|
+
if (!existsSync6(pkgPath)) return [];
|
|
1465
|
+
try {
|
|
1466
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
1467
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1468
|
+
return autoDetectStacks(allDeps);
|
|
1469
|
+
} catch {
|
|
1470
|
+
return [];
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return stackOpt.split(",").map((s) => s.trim().toLowerCase()).filter(isValidStack);
|
|
1474
|
+
}
|
|
637
1475
|
async function writeBridge(root, relPath) {
|
|
638
|
-
const target =
|
|
639
|
-
if (
|
|
1476
|
+
const target = path7.join(root, relPath);
|
|
1477
|
+
if (existsSync6(target)) {
|
|
640
1478
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
641
1479
|
return;
|
|
642
1480
|
}
|
|
643
|
-
await
|
|
644
|
-
await
|
|
1481
|
+
await mkdir3(path7.dirname(target), { recursive: true });
|
|
1482
|
+
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
645
1483
|
ui.success(`Created bridge ${relPath}`);
|
|
646
1484
|
}
|
|
647
1485
|
|
|
648
1486
|
// src/commands/install-hooks.ts
|
|
649
|
-
import { mkdir as
|
|
650
|
-
import { existsSync as
|
|
651
|
-
import
|
|
1487
|
+
import { mkdir as mkdir4, writeFile as writeFile4, chmod, readFile as readFile5 } from "fs/promises";
|
|
1488
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1489
|
+
import path8 from "path";
|
|
652
1490
|
import "commander";
|
|
653
1491
|
import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
654
1492
|
var HOOK_MARKER = "# hAIve auto-generated";
|
|
655
|
-
var
|
|
1493
|
+
var POST_MERGE_BODY = `#!/bin/sh
|
|
656
1494
|
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
657
1495
|
|
|
658
1496
|
# After a merge or pull, refresh memory anchors and auto-promote eligible
|
|
@@ -663,46 +1501,83 @@ elif [ -x ./node_modules/.bin/haive ]; then
|
|
|
663
1501
|
./node_modules/.bin/haive sync --quiet --since ORIG_HEAD || true
|
|
664
1502
|
fi
|
|
665
1503
|
`;
|
|
666
|
-
var
|
|
1504
|
+
var PRE_PUSH_BODY = `#!/bin/sh
|
|
1505
|
+
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
1506
|
+
|
|
1507
|
+
# Before pushing, run haive precommit to surface known anti-patterns and stale memories.
|
|
1508
|
+
# Exit 0 always \u2014 this is advisory only (set HAIVE_BLOCK=1 to make it blocking).
|
|
1509
|
+
HAIVE_BLOCK=\${HAIVE_BLOCK:-0}
|
|
1510
|
+
|
|
1511
|
+
_haive() {
|
|
1512
|
+
if command -v haive >/dev/null 2>&1; then haive "$@"
|
|
1513
|
+
elif [ -x ./node_modules/.bin/haive ]; then ./node_modules/.bin/haive "$@"
|
|
1514
|
+
else return 0
|
|
1515
|
+
fi
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
# Run pre-commit check on diff between local and remote
|
|
1519
|
+
LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
1520
|
+
REMOTE_SHA=$(git rev-parse --verify "@{u}" 2>/dev/null || echo "")
|
|
1521
|
+
if [ -n "$REMOTE_SHA" ]; then
|
|
1522
|
+
DIFF=$(git diff "$REMOTE_SHA"..HEAD 2>/dev/null || "")
|
|
1523
|
+
if [ -n "$DIFF" ]; then
|
|
1524
|
+
_haive precommit --quiet 2>/dev/null || true
|
|
1525
|
+
fi
|
|
1526
|
+
fi
|
|
1527
|
+
|
|
1528
|
+
# Remind agent to save session recap if env var is set
|
|
1529
|
+
if [ "$HAIVE_SESSION_REMINDER" = "1" ]; then
|
|
1530
|
+
echo "haive: session active \u2014 remember to call mem_session_end before closing." >&2
|
|
1531
|
+
fi
|
|
1532
|
+
|
|
1533
|
+
exit 0
|
|
1534
|
+
`;
|
|
1535
|
+
var HOOKS = [
|
|
1536
|
+
{ name: "post-merge", body: POST_MERGE_BODY },
|
|
1537
|
+
{ name: "post-rewrite", body: POST_MERGE_BODY },
|
|
1538
|
+
{ name: "pre-push", body: PRE_PUSH_BODY }
|
|
1539
|
+
];
|
|
667
1540
|
function registerInstallHooks(program2) {
|
|
668
1541
|
program2.command("install-hooks").description(
|
|
669
|
-
"Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs
|
|
1542
|
+
"Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs:\n post-merge / post-rewrite \u2014 runs haive sync after every pull/merge\n pre-push \u2014 runs haive precommit before every push (advisory)\n\n Installed automatically by haive init (autopilot mode).\n Use --force to overwrite existing hooks.\n"
|
|
670
1543
|
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
671
1544
|
const root = findProjectRoot6(opts.dir);
|
|
672
|
-
const gitDir =
|
|
673
|
-
if (!
|
|
1545
|
+
const gitDir = path8.join(root, ".git");
|
|
1546
|
+
if (!existsSync7(gitDir)) {
|
|
674
1547
|
ui.error(`No .git directory at ${root}.`);
|
|
675
1548
|
process.exitCode = 1;
|
|
676
1549
|
return;
|
|
677
1550
|
}
|
|
678
|
-
const hooksDir =
|
|
679
|
-
await
|
|
1551
|
+
const hooksDir = path8.join(gitDir, "hooks");
|
|
1552
|
+
await mkdir4(hooksDir, { recursive: true });
|
|
680
1553
|
let installed = 0;
|
|
681
1554
|
let skipped = 0;
|
|
682
|
-
for (const name of HOOKS) {
|
|
683
|
-
const file =
|
|
684
|
-
if (
|
|
685
|
-
const existing = await
|
|
1555
|
+
for (const { name, body } of HOOKS) {
|
|
1556
|
+
const file = path8.join(hooksDir, name);
|
|
1557
|
+
if (existsSync7(file) && !opts.force) {
|
|
1558
|
+
const existing = await readFile5(file, "utf8");
|
|
686
1559
|
if (!existing.includes(HOOK_MARKER)) {
|
|
687
1560
|
ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
|
|
688
1561
|
skipped++;
|
|
689
1562
|
continue;
|
|
690
1563
|
}
|
|
691
1564
|
}
|
|
692
|
-
await
|
|
1565
|
+
await writeFile4(file, body, "utf8");
|
|
693
1566
|
await chmod(file, 493);
|
|
694
1567
|
installed++;
|
|
695
1568
|
}
|
|
696
1569
|
ui.success(`Installed ${installed} hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
|
|
697
|
-
ui.info("
|
|
1570
|
+
ui.info("post-merge: haive sync runs after every pull/merge.");
|
|
1571
|
+
ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
|
|
1572
|
+
ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
|
|
698
1573
|
});
|
|
699
1574
|
}
|
|
700
1575
|
|
|
701
1576
|
// src/commands/mcp.ts
|
|
702
1577
|
import { spawn } from "child_process";
|
|
703
|
-
import { existsSync as
|
|
1578
|
+
import { existsSync as existsSync8 } from "fs";
|
|
704
1579
|
import { createRequire } from "module";
|
|
705
|
-
import
|
|
1580
|
+
import path9 from "path";
|
|
706
1581
|
import { fileURLToPath } from "url";
|
|
707
1582
|
import "commander";
|
|
708
1583
|
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
@@ -740,26 +1615,26 @@ function registerMcp(program2) {
|
|
|
740
1615
|
function locateMcpBin() {
|
|
741
1616
|
try {
|
|
742
1617
|
const pkgPath = require2.resolve("@hiveai/mcp/package.json");
|
|
743
|
-
const pkgDir =
|
|
744
|
-
const candidate =
|
|
745
|
-
if (
|
|
1618
|
+
const pkgDir = path9.dirname(pkgPath);
|
|
1619
|
+
const candidate = path9.join(pkgDir, "dist", "index.js");
|
|
1620
|
+
if (existsSync8(candidate)) return candidate;
|
|
746
1621
|
} catch {
|
|
747
1622
|
}
|
|
748
|
-
const here =
|
|
749
|
-
const sibling =
|
|
750
|
-
if (
|
|
1623
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
1624
|
+
const sibling = path9.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
|
|
1625
|
+
if (existsSync8(sibling)) return sibling;
|
|
751
1626
|
return null;
|
|
752
1627
|
}
|
|
753
1628
|
|
|
754
1629
|
// src/commands/sync.ts
|
|
755
1630
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
756
|
-
import { readFile as
|
|
757
|
-
import { existsSync as
|
|
758
|
-
import
|
|
1631
|
+
import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
1632
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1633
|
+
import path10 from "path";
|
|
759
1634
|
import "commander";
|
|
760
1635
|
import {
|
|
761
1636
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
762
|
-
buildFrontmatter,
|
|
1637
|
+
buildFrontmatter as buildFrontmatter2,
|
|
763
1638
|
findProjectRoot as findProjectRoot8,
|
|
764
1639
|
getUsage,
|
|
765
1640
|
isAutoPromoteEligible,
|
|
@@ -771,7 +1646,7 @@ import {
|
|
|
771
1646
|
pullCrossRepoSources,
|
|
772
1647
|
resolveHaivePaths as resolveHaivePaths5,
|
|
773
1648
|
resolveManifestFiles,
|
|
774
|
-
serializeMemory,
|
|
1649
|
+
serializeMemory as serializeMemory2,
|
|
775
1650
|
trackDependencies,
|
|
776
1651
|
verifyAnchor,
|
|
777
1652
|
watchContracts
|
|
@@ -790,7 +1665,7 @@ function registerSync(program2) {
|
|
|
790
1665
|
).option("--bridge-file <path>", "bridge file to inject into (default: CLAUDE.md)").option("--bridge-max-memories <n>", "max memories to inject into bridge file", "5").option("--embed", "rebuild embeddings index after sync (requires @haive/embeddings)").option("--no-cross-repo", "skip cross-repo memory pull even if crossRepoSources is configured").option("--no-deps", "skip dependency version tracking").option("--no-contracts", "skip contract file diff checking").action(async (opts) => {
|
|
791
1666
|
const root = findProjectRoot8(opts.dir);
|
|
792
1667
|
const paths = resolveHaivePaths5(root);
|
|
793
|
-
if (!
|
|
1668
|
+
if (!existsSync9(paths.memoriesDir)) {
|
|
794
1669
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
795
1670
|
process.exitCode = 1;
|
|
796
1671
|
return;
|
|
@@ -810,9 +1685,9 @@ function registerSync(program2) {
|
|
|
810
1685
|
for (const { memory: memory2, filePath } of memories) {
|
|
811
1686
|
if (memory2.frontmatter.type === "session_recap") {
|
|
812
1687
|
if (memory2.frontmatter.status === "stale") {
|
|
813
|
-
await
|
|
1688
|
+
await writeFile5(
|
|
814
1689
|
filePath,
|
|
815
|
-
|
|
1690
|
+
serializeMemory2({
|
|
816
1691
|
frontmatter: {
|
|
817
1692
|
...memory2.frontmatter,
|
|
818
1693
|
status: "validated",
|
|
@@ -833,9 +1708,9 @@ function registerSync(program2) {
|
|
|
833
1708
|
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
834
1709
|
if (result.stale) {
|
|
835
1710
|
if (memory2.frontmatter.status !== "stale") {
|
|
836
|
-
await
|
|
1711
|
+
await writeFile5(
|
|
837
1712
|
filePath,
|
|
838
|
-
|
|
1713
|
+
serializeMemory2({
|
|
839
1714
|
frontmatter: {
|
|
840
1715
|
...memory2.frontmatter,
|
|
841
1716
|
status: "stale",
|
|
@@ -849,9 +1724,9 @@ function registerSync(program2) {
|
|
|
849
1724
|
staleMarked++;
|
|
850
1725
|
}
|
|
851
1726
|
} else if (memory2.frontmatter.status === "stale") {
|
|
852
|
-
await
|
|
1727
|
+
await writeFile5(
|
|
853
1728
|
filePath,
|
|
854
|
-
|
|
1729
|
+
serializeMemory2({
|
|
855
1730
|
frontmatter: {
|
|
856
1731
|
...memory2.frontmatter,
|
|
857
1732
|
status: "validated",
|
|
@@ -877,9 +1752,9 @@ function registerSync(program2) {
|
|
|
877
1752
|
minReads: autoPromoteMinReads,
|
|
878
1753
|
maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
|
|
879
1754
|
})) {
|
|
880
|
-
await
|
|
1755
|
+
await writeFile5(
|
|
881
1756
|
filePath,
|
|
882
|
-
|
|
1757
|
+
serializeMemory2({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
883
1758
|
"utf8"
|
|
884
1759
|
);
|
|
885
1760
|
promoted++;
|
|
@@ -888,9 +1763,9 @@ function registerSync(program2) {
|
|
|
888
1763
|
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
889
1764
|
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
890
1765
|
if (ageHours >= autoApproveDelayHours) {
|
|
891
|
-
await
|
|
1766
|
+
await writeFile5(
|
|
892
1767
|
filePath,
|
|
893
|
-
|
|
1768
|
+
serializeMemory2({
|
|
894
1769
|
frontmatter: {
|
|
895
1770
|
...fm,
|
|
896
1771
|
status: "validated",
|
|
@@ -922,7 +1797,7 @@ function registerSync(program2) {
|
|
|
922
1797
|
);
|
|
923
1798
|
}
|
|
924
1799
|
if (opts.injectBridge) {
|
|
925
|
-
const bridgeFile = opts.bridgeFile ?
|
|
1800
|
+
const bridgeFile = opts.bridgeFile ? path10.resolve(opts.bridgeFile) : path10.join(root, "CLAUDE.md");
|
|
926
1801
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
927
1802
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
928
1803
|
}
|
|
@@ -1019,7 +1894,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1019
1894
|
**Prochaines \xE9tapes (si confirm\xE9) :**
|
|
1020
1895
|
- Consulter le CHANGELOG : \`haive memory import-changelog --from node_modules/<pkg>/CHANGELOG.md\`
|
|
1021
1896
|
- V\xE9rifier les m\xE9moires ancr\xE9es : \`haive memory verify\``;
|
|
1022
|
-
const fm =
|
|
1897
|
+
const fm = buildFrontmatter2({
|
|
1023
1898
|
type: "gotcha",
|
|
1024
1899
|
slug,
|
|
1025
1900
|
scope: "team",
|
|
@@ -1028,11 +1903,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1028
1903
|
paths: [result.file],
|
|
1029
1904
|
topic: `dep-bump-${slugParts}`
|
|
1030
1905
|
});
|
|
1031
|
-
const teamDir =
|
|
1032
|
-
await
|
|
1033
|
-
await
|
|
1034
|
-
|
|
1035
|
-
|
|
1906
|
+
const teamDir = path10.join(paths.memoriesDir, "team");
|
|
1907
|
+
await mkdir5(teamDir, { recursive: true });
|
|
1908
|
+
await writeFile5(
|
|
1909
|
+
path10.join(teamDir, `${fm.id}.md`),
|
|
1910
|
+
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1036
1911
|
"utf8"
|
|
1037
1912
|
);
|
|
1038
1913
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -1086,7 +1961,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1086
1961
|
**Prochaines \xE9tapes (si confirm\xE9) :**
|
|
1087
1962
|
- Rechercher les usages : \`haive memory for-files <fichiers concern\xE9s>\`
|
|
1088
1963
|
- V\xE9rifier les m\xE9moires li\xE9es : \`haive memory query ${diff.contract}\``;
|
|
1089
|
-
const fm =
|
|
1964
|
+
const fm = buildFrontmatter2({
|
|
1090
1965
|
type: "gotcha",
|
|
1091
1966
|
slug,
|
|
1092
1967
|
scope: "team",
|
|
@@ -1095,11 +1970,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1095
1970
|
paths: [diff.file],
|
|
1096
1971
|
topic: `contract-breaking-${diff.contract}`
|
|
1097
1972
|
});
|
|
1098
|
-
const teamDir =
|
|
1099
|
-
await
|
|
1100
|
-
await
|
|
1101
|
-
|
|
1102
|
-
|
|
1973
|
+
const teamDir = path10.join(paths.memoriesDir, "team");
|
|
1974
|
+
await mkdir5(teamDir, { recursive: true });
|
|
1975
|
+
await writeFile5(
|
|
1976
|
+
path10.join(teamDir, `${fm.id}.md`),
|
|
1977
|
+
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1103
1978
|
"utf8"
|
|
1104
1979
|
);
|
|
1105
1980
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -1159,7 +2034,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1159
2034
|
});
|
|
1160
2035
|
}
|
|
1161
2036
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
1162
|
-
if (!
|
|
2037
|
+
if (!existsSync9(memoriesDir)) return;
|
|
1163
2038
|
const all = await loadMemoriesFromDir2(memoriesDir);
|
|
1164
2039
|
const top = all.filter(({ memory: memory2 }) => {
|
|
1165
2040
|
const s = memory2.frontmatter.status;
|
|
@@ -1184,17 +2059,17 @@ ${m.memory.body.trim()}`;
|
|
|
1184
2059
|
` + block + `
|
|
1185
2060
|
|
|
1186
2061
|
${BRIDGE_END}`;
|
|
1187
|
-
const fileExists =
|
|
1188
|
-
let existing = fileExists ? await
|
|
2062
|
+
const fileExists = existsSync9(bridgeFile);
|
|
2063
|
+
let existing = fileExists ? await readFile6(bridgeFile, "utf8") : "";
|
|
1189
2064
|
existing = existing.replace(/\r\n/g, "\n");
|
|
1190
2065
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
1191
2066
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
1192
2067
|
if (startIdx !== -1 && endIdx === -1) {
|
|
1193
|
-
ui.warn(`${
|
|
2068
|
+
ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
1194
2069
|
return;
|
|
1195
2070
|
}
|
|
1196
2071
|
if (startIdx === -1 && endIdx !== -1) {
|
|
1197
|
-
ui.warn(`${
|
|
2072
|
+
ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
1198
2073
|
return;
|
|
1199
2074
|
}
|
|
1200
2075
|
let updated;
|
|
@@ -1202,14 +2077,14 @@ ${BRIDGE_END}`;
|
|
|
1202
2077
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
1203
2078
|
} else {
|
|
1204
2079
|
if (!fileExists && !quiet) {
|
|
1205
|
-
ui.info(`Creating ${
|
|
2080
|
+
ui.info(`Creating ${path10.relative(root, bridgeFile)} with haive memory block.`);
|
|
1206
2081
|
}
|
|
1207
2082
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
1208
2083
|
}
|
|
1209
|
-
await
|
|
2084
|
+
await writeFile5(bridgeFile, updated, "utf8");
|
|
1210
2085
|
if (!quiet) {
|
|
1211
2086
|
console.log(
|
|
1212
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
2087
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path10.relative(root, bridgeFile)}`)
|
|
1213
2088
|
);
|
|
1214
2089
|
}
|
|
1215
2090
|
}
|
|
@@ -1234,18 +2109,18 @@ function collectSinceChanges(root, ref) {
|
|
|
1234
2109
|
|
|
1235
2110
|
// src/commands/memory-add.ts
|
|
1236
2111
|
import { createHash } from "crypto";
|
|
1237
|
-
import { mkdir as
|
|
1238
|
-
import { existsSync as
|
|
1239
|
-
import
|
|
2112
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
|
|
2113
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2114
|
+
import path11 from "path";
|
|
1240
2115
|
import "commander";
|
|
1241
2116
|
import {
|
|
1242
|
-
buildFrontmatter as
|
|
2117
|
+
buildFrontmatter as buildFrontmatter3,
|
|
1243
2118
|
findProjectRoot as findProjectRoot9,
|
|
1244
2119
|
inferModulesFromPaths,
|
|
1245
2120
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
1246
|
-
memoryFilePath,
|
|
2121
|
+
memoryFilePath as memoryFilePath2,
|
|
1247
2122
|
resolveHaivePaths as resolveHaivePaths6,
|
|
1248
|
-
serializeMemory as
|
|
2123
|
+
serializeMemory as serializeMemory3
|
|
1249
2124
|
} from "@hiveai/core";
|
|
1250
2125
|
function registerMemoryAdd(memory2) {
|
|
1251
2126
|
memory2.command("add").description(
|
|
@@ -1274,7 +2149,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1274
2149
|
).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: personal, or team in autopilot)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1275
2150
|
const root = findProjectRoot9(opts.dir);
|
|
1276
2151
|
const paths = resolveHaivePaths6(root);
|
|
1277
|
-
if (!
|
|
2152
|
+
if (!existsSync10(paths.haiveDir)) {
|
|
1278
2153
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
1279
2154
|
process.exitCode = 1;
|
|
1280
2155
|
return;
|
|
@@ -1285,7 +2160,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1285
2160
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
|
|
1286
2161
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
1287
2162
|
if (anchorPaths.length > 0) {
|
|
1288
|
-
const missing = anchorPaths.filter((p) => !
|
|
2163
|
+
const missing = anchorPaths.filter((p) => !existsSync10(path11.resolve(root, p)));
|
|
1289
2164
|
if (missing.length > 0) {
|
|
1290
2165
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
1291
2166
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -1297,12 +2172,12 @@ function registerMemoryAdd(memory2) {
|
|
|
1297
2172
|
const title = opts.title ?? opts.slug;
|
|
1298
2173
|
let body;
|
|
1299
2174
|
if (opts.bodyFile !== void 0) {
|
|
1300
|
-
if (!
|
|
2175
|
+
if (!existsSync10(opts.bodyFile)) {
|
|
1301
2176
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
1302
2177
|
process.exitCode = 1;
|
|
1303
2178
|
return;
|
|
1304
2179
|
}
|
|
1305
|
-
const fileContent = await
|
|
2180
|
+
const fileContent = await readFile7(opts.bodyFile, "utf8");
|
|
1306
2181
|
body = opts.title ? `# ${opts.title}
|
|
1307
2182
|
|
|
1308
2183
|
${fileContent.trim()}
|
|
@@ -1318,7 +2193,7 @@ TODO \u2014 write the memory body.
|
|
|
1318
2193
|
`;
|
|
1319
2194
|
}
|
|
1320
2195
|
const scope = opts.scope ?? "personal";
|
|
1321
|
-
if (
|
|
2196
|
+
if (existsSync10(paths.memoriesDir)) {
|
|
1322
2197
|
const incomingHash = createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
1323
2198
|
const allForHash = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1324
2199
|
const hashDup = allForHash.find(
|
|
@@ -1331,7 +2206,7 @@ TODO \u2014 write the memory body.
|
|
|
1331
2206
|
return;
|
|
1332
2207
|
}
|
|
1333
2208
|
}
|
|
1334
|
-
if (opts.topic &&
|
|
2209
|
+
if (opts.topic && existsSync10(paths.memoriesDir)) {
|
|
1335
2210
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1336
2211
|
const topicMatch = existing.find(
|
|
1337
2212
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
@@ -1349,13 +2224,13 @@ TODO \u2014 write the memory body.
|
|
|
1349
2224
|
symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
|
|
1350
2225
|
}
|
|
1351
2226
|
};
|
|
1352
|
-
await
|
|
1353
|
-
ui.success(`Updated (topic upsert) ${
|
|
2227
|
+
await writeFile6(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2228
|
+
ui.success(`Updated (topic upsert) ${path11.relative(root, topicMatch.filePath)}`);
|
|
1354
2229
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
1355
2230
|
return;
|
|
1356
2231
|
}
|
|
1357
2232
|
}
|
|
1358
|
-
const frontmatter =
|
|
2233
|
+
const frontmatter = buildFrontmatter3({
|
|
1359
2234
|
type: opts.type,
|
|
1360
2235
|
slug: opts.slug,
|
|
1361
2236
|
scope,
|
|
@@ -1368,14 +2243,14 @@ TODO \u2014 write the memory body.
|
|
|
1368
2243
|
commit: opts.commit,
|
|
1369
2244
|
topic: opts.topic
|
|
1370
2245
|
});
|
|
1371
|
-
const file =
|
|
1372
|
-
await
|
|
1373
|
-
if (
|
|
2246
|
+
const file = memoryFilePath2(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2247
|
+
await mkdir6(path11.dirname(file), { recursive: true });
|
|
2248
|
+
if (existsSync10(file)) {
|
|
1374
2249
|
ui.error(`Memory already exists at ${file}`);
|
|
1375
2250
|
process.exitCode = 1;
|
|
1376
2251
|
return;
|
|
1377
2252
|
}
|
|
1378
|
-
if (
|
|
2253
|
+
if (existsSync10(paths.memoriesDir)) {
|
|
1379
2254
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1380
2255
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
1381
2256
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
@@ -1387,8 +2262,8 @@ TODO \u2014 write the memory body.
|
|
|
1387
2262
|
ui.warn("Consider updating one of these with `haive memory update` instead.");
|
|
1388
2263
|
}
|
|
1389
2264
|
}
|
|
1390
|
-
await
|
|
1391
|
-
ui.success(`Created ${
|
|
2265
|
+
await writeFile6(file, serializeMemory3({ frontmatter, body }), "utf8");
|
|
2266
|
+
ui.success(`Created ${path11.relative(root, file)}`);
|
|
1392
2267
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
1393
2268
|
if (inferredTags.length > 0) {
|
|
1394
2269
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -1418,8 +2293,8 @@ function parseCsv2(value) {
|
|
|
1418
2293
|
}
|
|
1419
2294
|
|
|
1420
2295
|
// src/commands/memory-list.ts
|
|
1421
|
-
import { existsSync as
|
|
1422
|
-
import
|
|
2296
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2297
|
+
import path12 from "path";
|
|
1423
2298
|
import "commander";
|
|
1424
2299
|
import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
|
|
1425
2300
|
|
|
@@ -1435,7 +2310,7 @@ function registerMemoryList(memory2) {
|
|
|
1435
2310
|
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1436
2311
|
const root = findProjectRoot10(opts.dir);
|
|
1437
2312
|
const paths = resolveHaivePaths7(root);
|
|
1438
|
-
if (!
|
|
2313
|
+
if (!existsSync11(paths.memoriesDir)) {
|
|
1439
2314
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
1440
2315
|
process.exitCode = 1;
|
|
1441
2316
|
return;
|
|
@@ -1467,7 +2342,7 @@ function registerMemoryList(memory2) {
|
|
|
1467
2342
|
console.log(
|
|
1468
2343
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
1469
2344
|
);
|
|
1470
|
-
console.log(` ${ui.dim(
|
|
2345
|
+
console.log(` ${ui.dim(path12.relative(root, filePath))}`);
|
|
1471
2346
|
}
|
|
1472
2347
|
console.log(ui.dim(`
|
|
1473
2348
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -1502,21 +2377,21 @@ function matchesFilters(loaded, opts) {
|
|
|
1502
2377
|
}
|
|
1503
2378
|
|
|
1504
2379
|
// src/commands/memory-promote.ts
|
|
1505
|
-
import { mkdir as
|
|
1506
|
-
import { existsSync as
|
|
1507
|
-
import
|
|
2380
|
+
import { mkdir as mkdir7, unlink, writeFile as writeFile7 } from "fs/promises";
|
|
2381
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2382
|
+
import path13 from "path";
|
|
1508
2383
|
import "commander";
|
|
1509
2384
|
import {
|
|
1510
2385
|
findProjectRoot as findProjectRoot11,
|
|
1511
|
-
memoryFilePath as
|
|
2386
|
+
memoryFilePath as memoryFilePath3,
|
|
1512
2387
|
resolveHaivePaths as resolveHaivePaths8,
|
|
1513
|
-
serializeMemory as
|
|
2388
|
+
serializeMemory as serializeMemory4
|
|
1514
2389
|
} from "@hiveai/core";
|
|
1515
2390
|
function registerMemoryPromote(memory2) {
|
|
1516
2391
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
1517
2392
|
const root = findProjectRoot11(opts.dir);
|
|
1518
2393
|
const paths = resolveHaivePaths8(root);
|
|
1519
|
-
if (!
|
|
2394
|
+
if (!existsSync12(paths.memoriesDir)) {
|
|
1520
2395
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
1521
2396
|
process.exitCode = 1;
|
|
1522
2397
|
return;
|
|
@@ -1550,31 +2425,31 @@ function registerMemoryPromote(memory2) {
|
|
|
1550
2425
|
},
|
|
1551
2426
|
body: found.memory.body
|
|
1552
2427
|
};
|
|
1553
|
-
const newPath =
|
|
1554
|
-
await
|
|
1555
|
-
await
|
|
2428
|
+
const newPath = memoryFilePath3(paths, "team", updated.frontmatter.id);
|
|
2429
|
+
await mkdir7(path13.dirname(newPath), { recursive: true });
|
|
2430
|
+
await writeFile7(newPath, serializeMemory4(updated), "utf8");
|
|
1556
2431
|
await unlink(found.filePath);
|
|
1557
2432
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
1558
|
-
ui.info(`Now at ${
|
|
2433
|
+
ui.info(`Now at ${path13.relative(root, newPath)}`);
|
|
1559
2434
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
1560
2435
|
});
|
|
1561
2436
|
}
|
|
1562
2437
|
|
|
1563
2438
|
// src/commands/memory-approve.ts
|
|
1564
|
-
import { existsSync as
|
|
1565
|
-
import { writeFile as
|
|
1566
|
-
import
|
|
2439
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2440
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2441
|
+
import path14 from "path";
|
|
1567
2442
|
import "commander";
|
|
1568
2443
|
import {
|
|
1569
2444
|
findProjectRoot as findProjectRoot12,
|
|
1570
2445
|
resolveHaivePaths as resolveHaivePaths9,
|
|
1571
|
-
serializeMemory as
|
|
2446
|
+
serializeMemory as serializeMemory5
|
|
1572
2447
|
} from "@hiveai/core";
|
|
1573
2448
|
function registerMemoryApprove(memory2) {
|
|
1574
2449
|
memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
1575
2450
|
const root = findProjectRoot12(opts.dir);
|
|
1576
2451
|
const paths = resolveHaivePaths9(root);
|
|
1577
|
-
if (!
|
|
2452
|
+
if (!existsSync13(paths.memoriesDir)) {
|
|
1578
2453
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1579
2454
|
process.exitCode = 1;
|
|
1580
2455
|
return;
|
|
@@ -1596,7 +2471,7 @@ function registerMemoryApprove(memory2) {
|
|
|
1596
2471
|
frontmatter: { ...found2.memory.frontmatter, status: "validated" },
|
|
1597
2472
|
body: found2.memory.body
|
|
1598
2473
|
};
|
|
1599
|
-
await
|
|
2474
|
+
await writeFile8(found2.filePath, serializeMemory5(next2), "utf8");
|
|
1600
2475
|
count++;
|
|
1601
2476
|
}
|
|
1602
2477
|
ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
|
|
@@ -1625,27 +2500,27 @@ function registerMemoryApprove(memory2) {
|
|
|
1625
2500
|
frontmatter: { ...found.memory.frontmatter, status: "validated" },
|
|
1626
2501
|
body: found.memory.body
|
|
1627
2502
|
};
|
|
1628
|
-
await
|
|
2503
|
+
await writeFile8(found.filePath, serializeMemory5(next), "utf8");
|
|
1629
2504
|
ui.success(`Approved ${id} (status=validated)`);
|
|
1630
|
-
ui.info(
|
|
2505
|
+
ui.info(path14.relative(root, found.filePath));
|
|
1631
2506
|
});
|
|
1632
2507
|
}
|
|
1633
2508
|
|
|
1634
2509
|
// src/commands/memory-update.ts
|
|
1635
|
-
import { writeFile as
|
|
1636
|
-
import { existsSync as
|
|
1637
|
-
import
|
|
2510
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2511
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2512
|
+
import path15 from "path";
|
|
1638
2513
|
import "commander";
|
|
1639
2514
|
import {
|
|
1640
2515
|
findProjectRoot as findProjectRoot13,
|
|
1641
2516
|
resolveHaivePaths as resolveHaivePaths10,
|
|
1642
|
-
serializeMemory as
|
|
2517
|
+
serializeMemory as serializeMemory6
|
|
1643
2518
|
} from "@hiveai/core";
|
|
1644
2519
|
function registerMemoryUpdate(memory2) {
|
|
1645
2520
|
memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
1646
2521
|
const root = findProjectRoot13(opts.dir);
|
|
1647
2522
|
const paths = resolveHaivePaths10(root);
|
|
1648
|
-
if (!
|
|
2523
|
+
if (!existsSync14(paths.memoriesDir)) {
|
|
1649
2524
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
1650
2525
|
process.exitCode = 1;
|
|
1651
2526
|
return;
|
|
@@ -1692,12 +2567,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
1692
2567
|
ui.warn("Nothing to update \u2014 provide at least one option.");
|
|
1693
2568
|
return;
|
|
1694
2569
|
}
|
|
1695
|
-
await
|
|
2570
|
+
await writeFile9(
|
|
1696
2571
|
loaded.filePath,
|
|
1697
|
-
|
|
2572
|
+
serializeMemory6({ frontmatter: newFrontmatter, body: newBody }),
|
|
1698
2573
|
"utf8"
|
|
1699
2574
|
);
|
|
1700
|
-
ui.success(`Updated ${
|
|
2575
|
+
ui.success(`Updated ${path15.relative(root, loaded.filePath)}`);
|
|
1701
2576
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
1702
2577
|
});
|
|
1703
2578
|
}
|
|
@@ -1716,9 +2591,9 @@ function parseCsv3(value) {
|
|
|
1716
2591
|
}
|
|
1717
2592
|
|
|
1718
2593
|
// src/commands/memory-auto-promote.ts
|
|
1719
|
-
import { writeFile as
|
|
1720
|
-
import { existsSync as
|
|
1721
|
-
import
|
|
2594
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
2595
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2596
|
+
import path16 from "path";
|
|
1722
2597
|
import "commander";
|
|
1723
2598
|
import {
|
|
1724
2599
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
@@ -1727,7 +2602,7 @@ import {
|
|
|
1727
2602
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
1728
2603
|
loadUsageIndex as loadUsageIndex2,
|
|
1729
2604
|
resolveHaivePaths as resolveHaivePaths11,
|
|
1730
|
-
serializeMemory as
|
|
2605
|
+
serializeMemory as serializeMemory7
|
|
1731
2606
|
} from "@hiveai/core";
|
|
1732
2607
|
function registerMemoryAutoPromote(memory2) {
|
|
1733
2608
|
memory2.command("auto-promote").description("Promote eligible 'proposed' memories to 'validated' based on usage").option("--min-reads <n>", "minimum read_count to qualify", String(DEFAULT_AUTO_PROMOTE_RULE2.minReads)).option(
|
|
@@ -1737,7 +2612,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1737
2612
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1738
2613
|
const root = findProjectRoot14(opts.dir);
|
|
1739
2614
|
const paths = resolveHaivePaths11(root);
|
|
1740
|
-
if (!
|
|
2615
|
+
if (!existsSync15(paths.memoriesDir)) {
|
|
1741
2616
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1742
2617
|
process.exitCode = 1;
|
|
1743
2618
|
return;
|
|
@@ -1763,13 +2638,13 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1763
2638
|
console.log(
|
|
1764
2639
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
1765
2640
|
);
|
|
1766
|
-
console.log(` ${ui.dim(
|
|
2641
|
+
console.log(` ${ui.dim(path16.relative(root, filePath))}`);
|
|
1767
2642
|
if (opts.apply) {
|
|
1768
2643
|
const next = {
|
|
1769
2644
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
1770
2645
|
body: mem.body
|
|
1771
2646
|
};
|
|
1772
|
-
await
|
|
2647
|
+
await writeFile10(filePath, serializeMemory7(next), "utf8");
|
|
1773
2648
|
written++;
|
|
1774
2649
|
}
|
|
1775
2650
|
}
|
|
@@ -1780,9 +2655,9 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1780
2655
|
|
|
1781
2656
|
// src/commands/memory-edit.ts
|
|
1782
2657
|
import { spawn as spawn2 } from "child_process";
|
|
1783
|
-
import { existsSync as
|
|
1784
|
-
import { readFile as
|
|
1785
|
-
import
|
|
2658
|
+
import { existsSync as existsSync16 } from "fs";
|
|
2659
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2660
|
+
import path17 from "path";
|
|
1786
2661
|
import "commander";
|
|
1787
2662
|
import {
|
|
1788
2663
|
findProjectRoot as findProjectRoot15,
|
|
@@ -1793,7 +2668,7 @@ function registerMemoryEdit(memory2) {
|
|
|
1793
2668
|
memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
1794
2669
|
const root = findProjectRoot15(opts.dir);
|
|
1795
2670
|
const paths = resolveHaivePaths12(root);
|
|
1796
|
-
if (!
|
|
2671
|
+
if (!existsSync16(paths.memoriesDir)) {
|
|
1797
2672
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1798
2673
|
process.exitCode = 1;
|
|
1799
2674
|
return;
|
|
@@ -1806,13 +2681,13 @@ function registerMemoryEdit(memory2) {
|
|
|
1806
2681
|
return;
|
|
1807
2682
|
}
|
|
1808
2683
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
1809
|
-
ui.info(`Opening ${
|
|
2684
|
+
ui.info(`Opening ${path17.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
1810
2685
|
const code = await runEditor(editor, found.filePath);
|
|
1811
2686
|
if (code !== 0) {
|
|
1812
2687
|
ui.warn(`Editor exited with status ${code}.`);
|
|
1813
2688
|
}
|
|
1814
2689
|
try {
|
|
1815
|
-
const fresh = await
|
|
2690
|
+
const fresh = await readFile8(found.filePath, "utf8");
|
|
1816
2691
|
parseMemory(fresh);
|
|
1817
2692
|
ui.success("Memory still parses cleanly.");
|
|
1818
2693
|
} catch (err) {
|
|
@@ -1833,8 +2708,8 @@ function runEditor(editor, file) {
|
|
|
1833
2708
|
}
|
|
1834
2709
|
|
|
1835
2710
|
// src/commands/memory-for-files.ts
|
|
1836
|
-
import { existsSync as
|
|
1837
|
-
import
|
|
2711
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2712
|
+
import path18 from "path";
|
|
1838
2713
|
import "commander";
|
|
1839
2714
|
import {
|
|
1840
2715
|
deriveConfidence,
|
|
@@ -1849,7 +2724,7 @@ function registerMemoryForFiles(memory2) {
|
|
|
1849
2724
|
memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
|
|
1850
2725
|
const root = findProjectRoot16(opts.dir);
|
|
1851
2726
|
const paths = resolveHaivePaths13(root);
|
|
1852
|
-
if (!
|
|
2727
|
+
if (!existsSync17(paths.memoriesDir)) {
|
|
1853
2728
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1854
2729
|
process.exitCode = 1;
|
|
1855
2730
|
return;
|
|
@@ -1956,13 +2831,13 @@ function printGroup(root, label, loaded, usage) {
|
|
|
1956
2831
|
const u = getUsage3(usage, fm.id);
|
|
1957
2832
|
const conf = deriveConfidence(fm, u);
|
|
1958
2833
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
1959
|
-
console.log(` ${ui.dim(
|
|
2834
|
+
console.log(` ${ui.dim(path18.relative(root, filePath))}`);
|
|
1960
2835
|
}
|
|
1961
2836
|
}
|
|
1962
2837
|
|
|
1963
2838
|
// src/commands/memory-hot.ts
|
|
1964
|
-
import { existsSync as
|
|
1965
|
-
import
|
|
2839
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2840
|
+
import path19 from "path";
|
|
1966
2841
|
import "commander";
|
|
1967
2842
|
import {
|
|
1968
2843
|
findProjectRoot as findProjectRoot17,
|
|
@@ -1974,7 +2849,7 @@ function registerMemoryHot(memory2) {
|
|
|
1974
2849
|
memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1975
2850
|
const root = findProjectRoot17(opts.dir);
|
|
1976
2851
|
const paths = resolveHaivePaths14(root);
|
|
1977
|
-
if (!
|
|
2852
|
+
if (!existsSync18(paths.memoriesDir)) {
|
|
1978
2853
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1979
2854
|
process.exitCode = 1;
|
|
1980
2855
|
return;
|
|
@@ -2002,7 +2877,7 @@ function registerMemoryHot(memory2) {
|
|
|
2002
2877
|
console.log(
|
|
2003
2878
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2004
2879
|
);
|
|
2005
|
-
console.log(` ${ui.dim(
|
|
2880
|
+
console.log(` ${ui.dim(path19.relative(root, filePath))}`);
|
|
2006
2881
|
}
|
|
2007
2882
|
ui.info(
|
|
2008
2883
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -2011,16 +2886,16 @@ function registerMemoryHot(memory2) {
|
|
|
2011
2886
|
}
|
|
2012
2887
|
|
|
2013
2888
|
// src/commands/memory-tried.ts
|
|
2014
|
-
import { mkdir as
|
|
2015
|
-
import { existsSync as
|
|
2016
|
-
import
|
|
2889
|
+
import { mkdir as mkdir8, writeFile as writeFile11 } from "fs/promises";
|
|
2890
|
+
import { existsSync as existsSync19 } from "fs";
|
|
2891
|
+
import path20 from "path";
|
|
2017
2892
|
import "commander";
|
|
2018
2893
|
import {
|
|
2019
|
-
buildFrontmatter as
|
|
2894
|
+
buildFrontmatter as buildFrontmatter4,
|
|
2020
2895
|
findProjectRoot as findProjectRoot18,
|
|
2021
|
-
memoryFilePath as
|
|
2896
|
+
memoryFilePath as memoryFilePath4,
|
|
2022
2897
|
resolveHaivePaths as resolveHaivePaths15,
|
|
2023
|
-
serializeMemory as
|
|
2898
|
+
serializeMemory as serializeMemory8
|
|
2024
2899
|
} from "@hiveai/core";
|
|
2025
2900
|
function registerMemoryTried(memory2) {
|
|
2026
2901
|
memory2.command("tried").description(
|
|
@@ -2041,13 +2916,13 @@ function registerMemoryTried(memory2) {
|
|
|
2041
2916
|
).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2042
2917
|
const root = findProjectRoot18(opts.dir);
|
|
2043
2918
|
const paths = resolveHaivePaths15(root);
|
|
2044
|
-
if (!
|
|
2919
|
+
if (!existsSync19(paths.haiveDir)) {
|
|
2045
2920
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2046
2921
|
process.exitCode = 1;
|
|
2047
2922
|
return;
|
|
2048
2923
|
}
|
|
2049
2924
|
const slug = opts.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
|
|
2050
|
-
const baseFm =
|
|
2925
|
+
const baseFm = buildFrontmatter4({
|
|
2051
2926
|
type: "attempt",
|
|
2052
2927
|
slug,
|
|
2053
2928
|
scope: opts.scope,
|
|
@@ -2063,15 +2938,15 @@ function registerMemoryTried(memory2) {
|
|
|
2063
2938
|
lines.push("", `**Instead, use:** ${opts.instead}`);
|
|
2064
2939
|
}
|
|
2065
2940
|
const body = lines.join("\n") + "\n";
|
|
2066
|
-
const file =
|
|
2067
|
-
await
|
|
2068
|
-
if (
|
|
2941
|
+
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2942
|
+
await mkdir8(path20.dirname(file), { recursive: true });
|
|
2943
|
+
if (existsSync19(file)) {
|
|
2069
2944
|
ui.error(`Memory already exists at ${file}`);
|
|
2070
2945
|
process.exitCode = 1;
|
|
2071
2946
|
return;
|
|
2072
2947
|
}
|
|
2073
|
-
await
|
|
2074
|
-
ui.success(`Recorded: ${
|
|
2948
|
+
await writeFile11(file, serializeMemory8({ frontmatter, body }), "utf8");
|
|
2949
|
+
ui.success(`Recorded: ${path20.relative(root, file)}`);
|
|
2075
2950
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
2076
2951
|
});
|
|
2077
2952
|
}
|
|
@@ -2081,8 +2956,8 @@ function parseCsv4(value) {
|
|
|
2081
2956
|
}
|
|
2082
2957
|
|
|
2083
2958
|
// src/commands/memory-pending.ts
|
|
2084
|
-
import { existsSync as
|
|
2085
|
-
import
|
|
2959
|
+
import { existsSync as existsSync20 } from "fs";
|
|
2960
|
+
import path21 from "path";
|
|
2086
2961
|
import "commander";
|
|
2087
2962
|
import {
|
|
2088
2963
|
findProjectRoot as findProjectRoot19,
|
|
@@ -2094,7 +2969,7 @@ function registerMemoryPending(memory2) {
|
|
|
2094
2969
|
memory2.command("pending").description("List 'proposed' memories awaiting review (sorted by reads desc)").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2095
2970
|
const root = findProjectRoot19(opts.dir);
|
|
2096
2971
|
const paths = resolveHaivePaths16(root);
|
|
2097
|
-
if (!
|
|
2972
|
+
if (!existsSync20(paths.memoriesDir)) {
|
|
2098
2973
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2099
2974
|
process.exitCode = 1;
|
|
2100
2975
|
return;
|
|
@@ -2122,15 +2997,15 @@ function registerMemoryPending(memory2) {
|
|
|
2122
2997
|
console.log(
|
|
2123
2998
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2124
2999
|
);
|
|
2125
|
-
console.log(` ${ui.dim(
|
|
3000
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
2126
3001
|
}
|
|
2127
3002
|
ui.info(`${proposed.length} pending`);
|
|
2128
3003
|
});
|
|
2129
3004
|
}
|
|
2130
3005
|
|
|
2131
3006
|
// src/commands/memory-query.ts
|
|
2132
|
-
import { existsSync as
|
|
2133
|
-
import
|
|
3007
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3008
|
+
import path22 from "path";
|
|
2134
3009
|
import "commander";
|
|
2135
3010
|
import {
|
|
2136
3011
|
extractSnippet,
|
|
@@ -2146,7 +3021,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2146
3021
|
memory2.command("query <text>").description("Search memories by id, tag, or substring (AND, OR fallback)").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
|
|
2147
3022
|
const root = findProjectRoot20(opts.dir);
|
|
2148
3023
|
const paths = resolveHaivePaths17(root);
|
|
2149
|
-
if (!
|
|
3024
|
+
if (!existsSync21(paths.memoriesDir)) {
|
|
2150
3025
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
2151
3026
|
process.exitCode = 1;
|
|
2152
3027
|
return;
|
|
@@ -2187,7 +3062,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2187
3062
|
const fm = mem.frontmatter;
|
|
2188
3063
|
const statusBadge = ui.statusBadge(fm.status);
|
|
2189
3064
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
2190
|
-
console.log(` ${ui.dim(
|
|
3065
|
+
console.log(` ${ui.dim(path22.relative(root, filePath))}`);
|
|
2191
3066
|
const snippet = extractSnippet(mem.body, snippetNeedle);
|
|
2192
3067
|
if (snippet) console.log(` ${snippet}`);
|
|
2193
3068
|
}
|
|
@@ -2204,8 +3079,8 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
2204
3079
|
}
|
|
2205
3080
|
|
|
2206
3081
|
// src/commands/memory-reject.ts
|
|
2207
|
-
import { writeFile as
|
|
2208
|
-
import { existsSync as
|
|
3082
|
+
import { writeFile as writeFile12 } from "fs/promises";
|
|
3083
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2209
3084
|
import "commander";
|
|
2210
3085
|
import {
|
|
2211
3086
|
findProjectRoot as findProjectRoot21,
|
|
@@ -2213,13 +3088,13 @@ import {
|
|
|
2213
3088
|
recordRejection,
|
|
2214
3089
|
resolveHaivePaths as resolveHaivePaths18,
|
|
2215
3090
|
saveUsageIndex,
|
|
2216
|
-
serializeMemory as
|
|
3091
|
+
serializeMemory as serializeMemory9
|
|
2217
3092
|
} from "@hiveai/core";
|
|
2218
3093
|
function registerMemoryReject(memory2) {
|
|
2219
3094
|
memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2220
3095
|
const root = findProjectRoot21(opts.dir);
|
|
2221
3096
|
const paths = resolveHaivePaths18(root);
|
|
2222
|
-
if (!
|
|
3097
|
+
if (!existsSync22(paths.memoriesDir)) {
|
|
2223
3098
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2224
3099
|
process.exitCode = 1;
|
|
2225
3100
|
return;
|
|
@@ -2231,9 +3106,9 @@ function registerMemoryReject(memory2) {
|
|
|
2231
3106
|
process.exitCode = 1;
|
|
2232
3107
|
return;
|
|
2233
3108
|
}
|
|
2234
|
-
await
|
|
3109
|
+
await writeFile12(
|
|
2235
3110
|
loaded.filePath,
|
|
2236
|
-
|
|
3111
|
+
serializeMemory9({
|
|
2237
3112
|
frontmatter: {
|
|
2238
3113
|
...loaded.memory.frontmatter,
|
|
2239
3114
|
status: "rejected",
|
|
@@ -2255,9 +3130,9 @@ function registerMemoryReject(memory2) {
|
|
|
2255
3130
|
}
|
|
2256
3131
|
|
|
2257
3132
|
// src/commands/memory-rm.ts
|
|
2258
|
-
import { existsSync as
|
|
3133
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2259
3134
|
import { unlink as unlink2 } from "fs/promises";
|
|
2260
|
-
import
|
|
3135
|
+
import path23 from "path";
|
|
2261
3136
|
import { createInterface } from "readline/promises";
|
|
2262
3137
|
import "commander";
|
|
2263
3138
|
import {
|
|
@@ -2270,7 +3145,7 @@ function registerMemoryRm(memory2) {
|
|
|
2270
3145
|
memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2271
3146
|
const root = findProjectRoot22(opts.dir);
|
|
2272
3147
|
const paths = resolveHaivePaths19(root);
|
|
2273
|
-
if (!
|
|
3148
|
+
if (!existsSync23(paths.memoriesDir)) {
|
|
2274
3149
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2275
3150
|
process.exitCode = 1;
|
|
2276
3151
|
return;
|
|
@@ -2282,7 +3157,7 @@ function registerMemoryRm(memory2) {
|
|
|
2282
3157
|
process.exitCode = 1;
|
|
2283
3158
|
return;
|
|
2284
3159
|
}
|
|
2285
|
-
const rel =
|
|
3160
|
+
const rel = path23.relative(root, found.filePath);
|
|
2286
3161
|
if (!opts.yes) {
|
|
2287
3162
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2288
3163
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -2306,9 +3181,9 @@ function registerMemoryRm(memory2) {
|
|
|
2306
3181
|
}
|
|
2307
3182
|
|
|
2308
3183
|
// src/commands/memory-show.ts
|
|
2309
|
-
import { existsSync as
|
|
2310
|
-
import { readFile as
|
|
2311
|
-
import
|
|
3184
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3185
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3186
|
+
import path24 from "path";
|
|
2312
3187
|
import "commander";
|
|
2313
3188
|
import {
|
|
2314
3189
|
deriveConfidence as deriveConfidence2,
|
|
@@ -2321,7 +3196,7 @@ function registerMemoryShow(memory2) {
|
|
|
2321
3196
|
memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2322
3197
|
const root = findProjectRoot23(opts.dir);
|
|
2323
3198
|
const paths = resolveHaivePaths20(root);
|
|
2324
|
-
if (!
|
|
3199
|
+
if (!existsSync24(paths.memoriesDir)) {
|
|
2325
3200
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2326
3201
|
process.exitCode = 1;
|
|
2327
3202
|
return;
|
|
@@ -2334,7 +3209,7 @@ function registerMemoryShow(memory2) {
|
|
|
2334
3209
|
return;
|
|
2335
3210
|
}
|
|
2336
3211
|
if (opts.raw) {
|
|
2337
|
-
console.log(await
|
|
3212
|
+
console.log(await readFile9(found.filePath, "utf8"));
|
|
2338
3213
|
return;
|
|
2339
3214
|
}
|
|
2340
3215
|
const fm = found.memory.frontmatter;
|
|
@@ -2350,7 +3225,7 @@ function registerMemoryShow(memory2) {
|
|
|
2350
3225
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
2351
3226
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
2352
3227
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
2353
|
-
console.log(`${ui.dim("file:")} ${
|
|
3228
|
+
console.log(`${ui.dim("file:")} ${path24.relative(root, found.filePath)}`);
|
|
2354
3229
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
2355
3230
|
console.log(ui.dim("anchor:"));
|
|
2356
3231
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -2365,8 +3240,8 @@ function registerMemoryShow(memory2) {
|
|
|
2365
3240
|
}
|
|
2366
3241
|
|
|
2367
3242
|
// src/commands/memory-stats.ts
|
|
2368
|
-
import { existsSync as
|
|
2369
|
-
import
|
|
3243
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3244
|
+
import path25 from "path";
|
|
2370
3245
|
import "commander";
|
|
2371
3246
|
import {
|
|
2372
3247
|
deriveConfidence as deriveConfidence3,
|
|
@@ -2379,7 +3254,7 @@ function registerMemoryStats(memory2) {
|
|
|
2379
3254
|
memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2380
3255
|
const root = findProjectRoot24(opts.dir);
|
|
2381
3256
|
const paths = resolveHaivePaths21(root);
|
|
2382
|
-
if (!
|
|
3257
|
+
if (!existsSync25(paths.memoriesDir)) {
|
|
2383
3258
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2384
3259
|
process.exitCode = 1;
|
|
2385
3260
|
return;
|
|
@@ -2404,20 +3279,20 @@ function registerMemoryStats(memory2) {
|
|
|
2404
3279
|
console.log(
|
|
2405
3280
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
2406
3281
|
);
|
|
2407
|
-
console.log(` ${ui.dim(
|
|
3282
|
+
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
2408
3283
|
}
|
|
2409
3284
|
});
|
|
2410
3285
|
}
|
|
2411
3286
|
|
|
2412
3287
|
// src/commands/memory-verify.ts
|
|
2413
|
-
import { writeFile as
|
|
2414
|
-
import { existsSync as
|
|
2415
|
-
import
|
|
3288
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
3289
|
+
import { existsSync as existsSync26 } from "fs";
|
|
3290
|
+
import path26 from "path";
|
|
2416
3291
|
import "commander";
|
|
2417
3292
|
import {
|
|
2418
3293
|
findProjectRoot as findProjectRoot25,
|
|
2419
3294
|
resolveHaivePaths as resolveHaivePaths22,
|
|
2420
|
-
serializeMemory as
|
|
3295
|
+
serializeMemory as serializeMemory10,
|
|
2421
3296
|
verifyAnchor as verifyAnchor2
|
|
2422
3297
|
} from "@hiveai/core";
|
|
2423
3298
|
function registerMemoryVerify(memory2) {
|
|
@@ -2426,7 +3301,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2426
3301
|
).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2427
3302
|
const root = findProjectRoot25(opts.dir);
|
|
2428
3303
|
const paths = resolveHaivePaths22(root);
|
|
2429
|
-
if (!
|
|
3304
|
+
if (!existsSync26(paths.memoriesDir)) {
|
|
2430
3305
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2431
3306
|
process.exitCode = 1;
|
|
2432
3307
|
return;
|
|
@@ -2449,7 +3324,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2449
3324
|
anchorlessIds.push(mem.frontmatter.id);
|
|
2450
3325
|
continue;
|
|
2451
3326
|
}
|
|
2452
|
-
const rel =
|
|
3327
|
+
const rel = path26.relative(root, filePath);
|
|
2453
3328
|
if (result.stale) {
|
|
2454
3329
|
staleCount++;
|
|
2455
3330
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -2464,7 +3339,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2464
3339
|
}
|
|
2465
3340
|
if (opts.update) {
|
|
2466
3341
|
const next = applyVerification(mem, result);
|
|
2467
|
-
await
|
|
3342
|
+
await writeFile13(filePath, serializeMemory10(next), "utf8");
|
|
2468
3343
|
updated++;
|
|
2469
3344
|
}
|
|
2470
3345
|
}
|
|
@@ -2512,8 +3387,8 @@ function applyVerification(mem, result) {
|
|
|
2512
3387
|
}
|
|
2513
3388
|
|
|
2514
3389
|
// src/commands/memory-import.ts
|
|
2515
|
-
import { readFile as
|
|
2516
|
-
import { existsSync as
|
|
3390
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3391
|
+
import { existsSync as existsSync27 } from "fs";
|
|
2517
3392
|
import "commander";
|
|
2518
3393
|
import {
|
|
2519
3394
|
findProjectRoot as findProjectRoot26,
|
|
@@ -2525,17 +3400,17 @@ function registerMemoryImport(memory2) {
|
|
|
2525
3400
|
).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2526
3401
|
const root = findProjectRoot26(opts.dir);
|
|
2527
3402
|
const paths = resolveHaivePaths23(root);
|
|
2528
|
-
if (!
|
|
3403
|
+
if (!existsSync27(paths.haiveDir)) {
|
|
2529
3404
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2530
3405
|
process.exitCode = 1;
|
|
2531
3406
|
return;
|
|
2532
3407
|
}
|
|
2533
|
-
if (!
|
|
3408
|
+
if (!existsSync27(opts.from)) {
|
|
2534
3409
|
ui.error(`File not found: ${opts.from}`);
|
|
2535
3410
|
process.exitCode = 1;
|
|
2536
3411
|
return;
|
|
2537
3412
|
}
|
|
2538
|
-
const content = await
|
|
3413
|
+
const content = await readFile10(opts.from, "utf8");
|
|
2539
3414
|
const scope = opts.scope ?? "team";
|
|
2540
3415
|
ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
|
|
2541
3416
|
ui.info(`Content length: ${content.length} chars`);
|
|
@@ -2563,15 +3438,15 @@ function registerMemoryImport(memory2) {
|
|
|
2563
3438
|
}
|
|
2564
3439
|
|
|
2565
3440
|
// src/commands/memory-import-changelog.ts
|
|
2566
|
-
import { existsSync as
|
|
2567
|
-
import { readFile as
|
|
2568
|
-
import
|
|
3441
|
+
import { existsSync as existsSync28 } from "fs";
|
|
3442
|
+
import { readFile as readFile11, mkdir as mkdir9, writeFile as writeFile14 } from "fs/promises";
|
|
3443
|
+
import path27 from "path";
|
|
2569
3444
|
import "commander";
|
|
2570
3445
|
import {
|
|
2571
|
-
buildFrontmatter as
|
|
3446
|
+
buildFrontmatter as buildFrontmatter5,
|
|
2572
3447
|
findProjectRoot as findProjectRoot27,
|
|
2573
3448
|
resolveHaivePaths as resolveHaivePaths24,
|
|
2574
|
-
serializeMemory as
|
|
3449
|
+
serializeMemory as serializeMemory11
|
|
2575
3450
|
} from "@hiveai/core";
|
|
2576
3451
|
function parseChangelog(content) {
|
|
2577
3452
|
const entries = [];
|
|
@@ -2636,13 +3511,13 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2636
3511
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2637
3512
|
const root = findProjectRoot27(opts.dir);
|
|
2638
3513
|
const paths = resolveHaivePaths24(root);
|
|
2639
|
-
const changelogPath =
|
|
2640
|
-
if (!
|
|
3514
|
+
const changelogPath = path27.resolve(root, opts.fromChangelog);
|
|
3515
|
+
if (!existsSync28(changelogPath)) {
|
|
2641
3516
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
2642
3517
|
process.exitCode = 1;
|
|
2643
3518
|
return;
|
|
2644
3519
|
}
|
|
2645
|
-
const content = await
|
|
3520
|
+
const content = await readFile11(changelogPath, "utf8");
|
|
2646
3521
|
let entries = parseChangelog(content);
|
|
2647
3522
|
if (entries.length === 0) {
|
|
2648
3523
|
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
@@ -2656,10 +3531,10 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2656
3531
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
2657
3532
|
}
|
|
2658
3533
|
}
|
|
2659
|
-
const pkgName = opts.package ??
|
|
3534
|
+
const pkgName = opts.package ?? path27.basename(path27.dirname(changelogPath));
|
|
2660
3535
|
const scope = opts.scope ?? "team";
|
|
2661
|
-
const teamDir =
|
|
2662
|
-
await
|
|
3536
|
+
const teamDir = path27.join(paths.memoriesDir, scope);
|
|
3537
|
+
await mkdir9(teamDir, { recursive: true });
|
|
2663
3538
|
let saved = 0;
|
|
2664
3539
|
for (const entry of entries) {
|
|
2665
3540
|
const lines = [];
|
|
@@ -2681,11 +3556,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2681
3556
|
lines.push("");
|
|
2682
3557
|
}
|
|
2683
3558
|
lines.push(
|
|
2684
|
-
`**Source:** \`${
|
|
3559
|
+
`**Source:** \`${path27.relative(root, changelogPath)}\`
|
|
2685
3560
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
2686
3561
|
);
|
|
2687
3562
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
2688
|
-
const fm =
|
|
3563
|
+
const fm = buildFrontmatter5({
|
|
2689
3564
|
type: "gotcha",
|
|
2690
3565
|
slug,
|
|
2691
3566
|
scope,
|
|
@@ -2696,12 +3571,12 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2696
3571
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
2697
3572
|
`v${entry.version}`
|
|
2698
3573
|
],
|
|
2699
|
-
paths: [
|
|
3574
|
+
paths: [path27.relative(root, changelogPath)],
|
|
2700
3575
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
2701
3576
|
});
|
|
2702
|
-
await
|
|
2703
|
-
|
|
2704
|
-
|
|
3577
|
+
await writeFile14(
|
|
3578
|
+
path27.join(teamDir, `${fm.id}.md`),
|
|
3579
|
+
serializeMemory11({ frontmatter: fm, body: lines.join("\n") }),
|
|
2705
3580
|
"utf8"
|
|
2706
3581
|
);
|
|
2707
3582
|
console.log(ui.green(` \u2713 ${fm.id}`));
|
|
@@ -2723,9 +3598,9 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
2723
3598
|
}
|
|
2724
3599
|
|
|
2725
3600
|
// src/commands/memory-digest.ts
|
|
2726
|
-
import { existsSync as
|
|
2727
|
-
import { writeFile as
|
|
2728
|
-
import
|
|
3601
|
+
import { existsSync as existsSync29 } from "fs";
|
|
3602
|
+
import { writeFile as writeFile15 } from "fs/promises";
|
|
3603
|
+
import path28 from "path";
|
|
2729
3604
|
import "commander";
|
|
2730
3605
|
import {
|
|
2731
3606
|
deriveConfidence as deriveConfidence4,
|
|
@@ -2748,7 +3623,7 @@ function registerMemoryDigest(program2) {
|
|
|
2748
3623
|
).option("--days <n>", "look-back window in days (default: 7)", "7").option("--scope <scope>", "personal | team | module | all (default: team)", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2749
3624
|
const root = findProjectRoot28(opts.dir);
|
|
2750
3625
|
const paths = resolveHaivePaths25(root);
|
|
2751
|
-
if (!
|
|
3626
|
+
if (!existsSync29(paths.memoriesDir)) {
|
|
2752
3627
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
2753
3628
|
process.exitCode = 1;
|
|
2754
3629
|
return;
|
|
@@ -2820,8 +3695,8 @@ function registerMemoryDigest(program2) {
|
|
|
2820
3695
|
);
|
|
2821
3696
|
const digest = lines.join("\n");
|
|
2822
3697
|
if (opts.out) {
|
|
2823
|
-
const outPath =
|
|
2824
|
-
await
|
|
3698
|
+
const outPath = path28.resolve(process.cwd(), opts.out);
|
|
3699
|
+
await writeFile15(outPath, digest, "utf8");
|
|
2825
3700
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
2826
3701
|
} else {
|
|
2827
3702
|
console.log(digest);
|
|
@@ -2830,17 +3705,17 @@ function registerMemoryDigest(program2) {
|
|
|
2830
3705
|
}
|
|
2831
3706
|
|
|
2832
3707
|
// src/commands/session-end.ts
|
|
2833
|
-
import { writeFile as
|
|
2834
|
-
import { existsSync as
|
|
2835
|
-
import
|
|
3708
|
+
import { writeFile as writeFile16, mkdir as mkdir10 } from "fs/promises";
|
|
3709
|
+
import { existsSync as existsSync30 } from "fs";
|
|
3710
|
+
import path29 from "path";
|
|
2836
3711
|
import "commander";
|
|
2837
3712
|
import {
|
|
2838
|
-
buildFrontmatter as
|
|
3713
|
+
buildFrontmatter as buildFrontmatter6,
|
|
2839
3714
|
findProjectRoot as findProjectRoot29,
|
|
2840
3715
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
2841
|
-
memoryFilePath as
|
|
3716
|
+
memoryFilePath as memoryFilePath5,
|
|
2842
3717
|
resolveHaivePaths as resolveHaivePaths26,
|
|
2843
|
-
serializeMemory as
|
|
3718
|
+
serializeMemory as serializeMemory12
|
|
2844
3719
|
} from "@hiveai/core";
|
|
2845
3720
|
function buildRecapBody(opts) {
|
|
2846
3721
|
const lines = [];
|
|
@@ -2891,7 +3766,7 @@ function registerSessionEnd(session2) {
|
|
|
2891
3766
|
).requiredOption("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").requiredOption("--accomplished <text>", "what was actually done (bullet list recommended)").option("--discoveries <text>", "bugs, surprises, or inconsistencies found during this session").option("--files <csv>", "key files touched, comma-separated (used as anchor for staleness detection)").option("--next <text>", "what should happen next (for the next session or a teammate)").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2892
3767
|
const root = findProjectRoot29(opts.dir);
|
|
2893
3768
|
const paths = resolveHaivePaths26(root);
|
|
2894
|
-
if (!
|
|
3769
|
+
if (!existsSync30(paths.haiveDir)) {
|
|
2895
3770
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2896
3771
|
process.exitCode = 1;
|
|
2897
3772
|
return;
|
|
@@ -2900,12 +3775,12 @@ function registerSessionEnd(session2) {
|
|
|
2900
3775
|
const body = buildRecapBody(opts);
|
|
2901
3776
|
const topic = recapTopic(scope, opts.module);
|
|
2902
3777
|
const filesTouched = parseCsv5(opts.files);
|
|
2903
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
3778
|
+
const missingPaths = filesTouched.filter((p) => !existsSync30(path29.resolve(root, p)));
|
|
2904
3779
|
if (missingPaths.length > 0) {
|
|
2905
3780
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
2906
3781
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
2907
3782
|
}
|
|
2908
|
-
if (
|
|
3783
|
+
if (existsSync30(paths.memoriesDir)) {
|
|
2909
3784
|
const existing = await loadMemoriesFromDir6(paths.memoriesDir);
|
|
2910
3785
|
const topicMatch = existing.find(
|
|
2911
3786
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
@@ -2921,13 +3796,13 @@ function registerSessionEnd(session2) {
|
|
|
2921
3796
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
2922
3797
|
}
|
|
2923
3798
|
};
|
|
2924
|
-
await
|
|
3799
|
+
await writeFile16(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2925
3800
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
2926
|
-
ui.info(`id=${fm.id} file=${
|
|
3801
|
+
ui.info(`id=${fm.id} file=${path29.relative(root, topicMatch.filePath)}`);
|
|
2927
3802
|
return;
|
|
2928
3803
|
}
|
|
2929
3804
|
}
|
|
2930
|
-
const frontmatter =
|
|
3805
|
+
const frontmatter = buildFrontmatter6({
|
|
2931
3806
|
type: "session_recap",
|
|
2932
3807
|
slug: "recap",
|
|
2933
3808
|
scope,
|
|
@@ -2937,11 +3812,11 @@ function registerSessionEnd(session2) {
|
|
|
2937
3812
|
topic,
|
|
2938
3813
|
status: "validated"
|
|
2939
3814
|
});
|
|
2940
|
-
const file =
|
|
2941
|
-
await
|
|
2942
|
-
await
|
|
3815
|
+
const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
3816
|
+
await mkdir10(path29.dirname(file), { recursive: true });
|
|
3817
|
+
await writeFile16(file, serializeMemory12({ frontmatter, body }), "utf8");
|
|
2943
3818
|
ui.success(`Session recap created`);
|
|
2944
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
3819
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path29.relative(root, file)}`);
|
|
2945
3820
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
2946
3821
|
});
|
|
2947
3822
|
}
|
|
@@ -2951,9 +3826,9 @@ function parseCsv5(value) {
|
|
|
2951
3826
|
}
|
|
2952
3827
|
|
|
2953
3828
|
// src/commands/snapshot.ts
|
|
2954
|
-
import { existsSync as
|
|
2955
|
-
import { readdir } from "fs/promises";
|
|
2956
|
-
import
|
|
3829
|
+
import { existsSync as existsSync31 } from "fs";
|
|
3830
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
3831
|
+
import path30 from "path";
|
|
2957
3832
|
import "commander";
|
|
2958
3833
|
import {
|
|
2959
3834
|
diffContract,
|
|
@@ -2986,18 +3861,18 @@ function registerSnapshot(program2) {
|
|
|
2986
3861
|
).option("--diff", "compare the contract against its stored snapshot").option("--list", "list all stored contract snapshots").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2987
3862
|
const root = findProjectRoot30(opts.dir);
|
|
2988
3863
|
const paths = resolveHaivePaths27(root);
|
|
2989
|
-
if (!
|
|
3864
|
+
if (!existsSync31(paths.haiveDir)) {
|
|
2990
3865
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
2991
3866
|
process.exitCode = 1;
|
|
2992
3867
|
return;
|
|
2993
3868
|
}
|
|
2994
3869
|
if (opts.list) {
|
|
2995
|
-
const contractsDir =
|
|
2996
|
-
if (!
|
|
3870
|
+
const contractsDir = path30.join(paths.haiveDir, "contracts");
|
|
3871
|
+
if (!existsSync31(contractsDir)) {
|
|
2997
3872
|
console.log(ui.dim("No contract snapshots found."));
|
|
2998
3873
|
return;
|
|
2999
3874
|
}
|
|
3000
|
-
const files = (await
|
|
3875
|
+
const files = (await readdir2(contractsDir)).filter(
|
|
3001
3876
|
(f) => f.endsWith(".lock") && !f.startsWith("deps-")
|
|
3002
3877
|
);
|
|
3003
3878
|
if (files.length === 0) {
|
|
@@ -3048,7 +3923,7 @@ function registerSnapshot(program2) {
|
|
|
3048
3923
|
return;
|
|
3049
3924
|
}
|
|
3050
3925
|
const contractPath = opts.contract;
|
|
3051
|
-
const name = opts.name ??
|
|
3926
|
+
const name = opts.name ?? path30.basename(contractPath, path30.extname(contractPath));
|
|
3052
3927
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
3053
3928
|
const contract = { name, path: contractPath, format };
|
|
3054
3929
|
try {
|
|
@@ -3103,8 +3978,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
3103
3978
|
}
|
|
3104
3979
|
}
|
|
3105
3980
|
function detectFormat(filePath) {
|
|
3106
|
-
const ext =
|
|
3107
|
-
const base =
|
|
3981
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
3982
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
3108
3983
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
3109
3984
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
3110
3985
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -3117,9 +3992,9 @@ function detectFormat(filePath) {
|
|
|
3117
3992
|
}
|
|
3118
3993
|
|
|
3119
3994
|
// src/commands/hub.ts
|
|
3120
|
-
import { existsSync as
|
|
3121
|
-
import { mkdir as
|
|
3122
|
-
import
|
|
3995
|
+
import { existsSync as existsSync32 } from "fs";
|
|
3996
|
+
import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile17, copyFile } from "fs/promises";
|
|
3997
|
+
import path31 from "path";
|
|
3123
3998
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3124
3999
|
import "commander";
|
|
3125
4000
|
import {
|
|
@@ -3128,7 +4003,7 @@ import {
|
|
|
3128
4003
|
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
3129
4004
|
resolveHaivePaths as resolveHaivePaths28,
|
|
3130
4005
|
saveConfig as saveConfig2,
|
|
3131
|
-
serializeMemory as
|
|
4006
|
+
serializeMemory as serializeMemory13
|
|
3132
4007
|
} from "@hiveai/core";
|
|
3133
4008
|
function registerHub(program2) {
|
|
3134
4009
|
const hub = program2.command("hub").description(
|
|
@@ -3138,8 +4013,8 @@ function registerHub(program2) {
|
|
|
3138
4013
|
hub.command("init <hubPath>").description(
|
|
3139
4014
|
"Initialize a new team-knowledge hub repo at <hubPath>.\n\n Creates a git repo with a .ai/ directory structure ready for shared memories.\n\n Example:\n haive hub init ../team-hub\n haive hub init /srv/git/team-knowledge\n"
|
|
3140
4015
|
).action(async (hubPath) => {
|
|
3141
|
-
const absPath =
|
|
3142
|
-
await
|
|
4016
|
+
const absPath = path31.resolve(hubPath);
|
|
4017
|
+
await mkdir11(absPath, { recursive: true });
|
|
3143
4018
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
3144
4019
|
if (gitCheck.status !== 0) {
|
|
3145
4020
|
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
@@ -3149,10 +4024,10 @@ function registerHub(program2) {
|
|
|
3149
4024
|
return;
|
|
3150
4025
|
}
|
|
3151
4026
|
}
|
|
3152
|
-
const sharedDir =
|
|
3153
|
-
await
|
|
3154
|
-
await
|
|
3155
|
-
|
|
4027
|
+
const sharedDir = path31.join(absPath, ".ai", "memories", "shared");
|
|
4028
|
+
await mkdir11(sharedDir, { recursive: true });
|
|
4029
|
+
await writeFile17(
|
|
4030
|
+
path31.join(absPath, ".ai", "README.md"),
|
|
3156
4031
|
`# hAIve Team Knowledge Hub
|
|
3157
4032
|
|
|
3158
4033
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -3173,8 +4048,8 @@ haive hub pull # import into a project
|
|
|
3173
4048
|
`,
|
|
3174
4049
|
"utf8"
|
|
3175
4050
|
);
|
|
3176
|
-
await
|
|
3177
|
-
|
|
4051
|
+
await writeFile17(
|
|
4052
|
+
path31.join(absPath, ".gitignore"),
|
|
3178
4053
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
3179
4054
|
"utf8"
|
|
3180
4055
|
);
|
|
@@ -3189,7 +4064,7 @@ haive hub pull # import into a project
|
|
|
3189
4064
|
`
|
|
3190
4065
|
Next steps:
|
|
3191
4066
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
3192
|
-
{ "hubPath": "${
|
|
4067
|
+
{ "hubPath": "${path31.relative(process.cwd(), absPath)}" }
|
|
3193
4068
|
2. Run \`haive hub push\` to publish your shared memories
|
|
3194
4069
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
3195
4070
|
`
|
|
@@ -3218,15 +4093,15 @@ Next steps:
|
|
|
3218
4093
|
process.exitCode = 1;
|
|
3219
4094
|
return;
|
|
3220
4095
|
}
|
|
3221
|
-
const hubRoot =
|
|
3222
|
-
if (!
|
|
4096
|
+
const hubRoot = path31.resolve(root, config.hubPath);
|
|
4097
|
+
if (!existsSync32(hubRoot)) {
|
|
3223
4098
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
3224
4099
|
process.exitCode = 1;
|
|
3225
4100
|
return;
|
|
3226
4101
|
}
|
|
3227
|
-
const projectName =
|
|
3228
|
-
const destDir =
|
|
3229
|
-
await
|
|
4102
|
+
const projectName = path31.basename(root);
|
|
4103
|
+
const destDir = path31.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
4104
|
+
await mkdir11(destDir, { recursive: true });
|
|
3230
4105
|
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3231
4106
|
const shared = all.filter(
|
|
3232
4107
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -3244,15 +4119,15 @@ Next steps:
|
|
|
3244
4119
|
for (const { memory: memory2 } of shared) {
|
|
3245
4120
|
const fm = memory2.frontmatter;
|
|
3246
4121
|
const fileName = `${fm.id}.md`;
|
|
3247
|
-
const destPath =
|
|
3248
|
-
await
|
|
4122
|
+
const destPath = path31.join(destDir, fileName);
|
|
4123
|
+
await writeFile17(destPath, serializeMemory13(memory2), "utf8");
|
|
3249
4124
|
pushed++;
|
|
3250
4125
|
}
|
|
3251
4126
|
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
3252
4127
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
3253
4128
|
if (opts.commit) {
|
|
3254
4129
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
3255
|
-
spawnSync3("git", ["add",
|
|
4130
|
+
spawnSync3("git", ["add", path31.join(".ai", "memories", "shared", projectName)], {
|
|
3256
4131
|
cwd: hubRoot
|
|
3257
4132
|
});
|
|
3258
4133
|
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
@@ -3287,15 +4162,15 @@ Next steps:
|
|
|
3287
4162
|
process.exitCode = 1;
|
|
3288
4163
|
return;
|
|
3289
4164
|
}
|
|
3290
|
-
const hubRoot =
|
|
3291
|
-
const hubSharedDir =
|
|
3292
|
-
if (!
|
|
4165
|
+
const hubRoot = path31.resolve(root, config.hubPath);
|
|
4166
|
+
const hubSharedDir = path31.join(hubRoot, ".ai", "memories", "shared");
|
|
4167
|
+
if (!existsSync32(hubSharedDir)) {
|
|
3293
4168
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
3294
4169
|
return;
|
|
3295
4170
|
}
|
|
3296
|
-
const projectName =
|
|
3297
|
-
const { readdir:
|
|
3298
|
-
const projectDirs = (await
|
|
4171
|
+
const projectName = path31.basename(root);
|
|
4172
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
4173
|
+
const projectDirs = (await readdir3(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
3299
4174
|
if (projectDirs.length === 0) {
|
|
3300
4175
|
console.log(ui.dim("No other projects have pushed to the hub yet."));
|
|
3301
4176
|
return;
|
|
@@ -3303,17 +4178,17 @@ Next steps:
|
|
|
3303
4178
|
let totalImported = 0;
|
|
3304
4179
|
let totalUpdated = 0;
|
|
3305
4180
|
for (const sourceName of projectDirs) {
|
|
3306
|
-
const sourceDir =
|
|
3307
|
-
const destDir =
|
|
3308
|
-
await
|
|
3309
|
-
const sourceFiles = (await
|
|
4181
|
+
const sourceDir = path31.join(hubSharedDir, sourceName);
|
|
4182
|
+
const destDir = path31.join(paths.memoriesDir, "shared", sourceName);
|
|
4183
|
+
await mkdir11(destDir, { recursive: true });
|
|
4184
|
+
const sourceFiles = (await readdir3(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
3310
4185
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
3311
4186
|
const existingInDest = await loadDir(destDir);
|
|
3312
4187
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
3313
4188
|
for (const file of sourceFiles) {
|
|
3314
|
-
const srcPath =
|
|
3315
|
-
const destPath =
|
|
3316
|
-
const fileContent = await
|
|
4189
|
+
const srcPath = path31.join(sourceDir, file);
|
|
4190
|
+
const destPath = path31.join(destDir, file);
|
|
4191
|
+
const fileContent = await readFile12(srcPath, "utf8");
|
|
3317
4192
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
3318
4193
|
if (!alreadyTagged) {
|
|
3319
4194
|
await copyFile(srcPath, destPath);
|
|
@@ -3343,14 +4218,14 @@ Next steps:
|
|
|
3343
4218
|
console.log(
|
|
3344
4219
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
3345
4220
|
);
|
|
3346
|
-
const sharedDir =
|
|
3347
|
-
if (
|
|
3348
|
-
const { readdir:
|
|
3349
|
-
const sources = (await
|
|
4221
|
+
const sharedDir = path31.join(paths.memoriesDir, "shared");
|
|
4222
|
+
if (existsSync32(sharedDir)) {
|
|
4223
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
4224
|
+
const sources = (await readdir3(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3350
4225
|
console.log(`
|
|
3351
4226
|
Imported from ${sources.length} source(s):`);
|
|
3352
4227
|
for (const src of sources) {
|
|
3353
|
-
const files = (await
|
|
4228
|
+
const files = (await readdir3(path31.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
3354
4229
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
3355
4230
|
}
|
|
3356
4231
|
} else {
|
|
@@ -3365,8 +4240,8 @@ Next steps:
|
|
|
3365
4240
|
if (outgoing.length > 0) {
|
|
3366
4241
|
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
3367
4242
|
}
|
|
3368
|
-
void
|
|
3369
|
-
void
|
|
4243
|
+
void readFile12;
|
|
4244
|
+
void writeFile17;
|
|
3370
4245
|
void saveConfig2;
|
|
3371
4246
|
});
|
|
3372
4247
|
}
|
|
@@ -3568,13 +4443,20 @@ function summarize(name, t0, payload, notes) {
|
|
|
3568
4443
|
}
|
|
3569
4444
|
|
|
3570
4445
|
// src/commands/memory-suggest.ts
|
|
4446
|
+
import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
|
|
4447
|
+
import { existsSync as existsSync33 } from "fs";
|
|
4448
|
+
import path32 from "path";
|
|
3571
4449
|
import "commander";
|
|
3572
4450
|
import {
|
|
3573
4451
|
aggregateUsage as aggregateUsage2,
|
|
4452
|
+
buildFrontmatter as buildFrontmatter7,
|
|
3574
4453
|
findProjectRoot as findProjectRoot34,
|
|
4454
|
+
loadMemoriesFromDir as loadMemoriesFromDir8,
|
|
4455
|
+
memoryFilePath as memoryFilePath6,
|
|
3575
4456
|
parseSince as parseSince2,
|
|
3576
4457
|
readUsageEvents as readUsageEvents2,
|
|
3577
|
-
resolveHaivePaths as resolveHaivePaths31
|
|
4458
|
+
resolveHaivePaths as resolveHaivePaths31,
|
|
4459
|
+
serializeMemory as serializeMemory14
|
|
3578
4460
|
} from "@hiveai/core";
|
|
3579
4461
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
3580
4462
|
"mem_search",
|
|
@@ -3583,7 +4465,9 @@ var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
3583
4465
|
"get_briefing"
|
|
3584
4466
|
]);
|
|
3585
4467
|
function registerMemorySuggest(memory2) {
|
|
3586
|
-
memory2.command("suggest").description(
|
|
4468
|
+
memory2.command("suggest").description(
|
|
4469
|
+
"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."
|
|
4470
|
+
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3587
4471
|
const root = findProjectRoot34(opts.dir);
|
|
3588
4472
|
const paths = resolveHaivePaths31(root);
|
|
3589
4473
|
const events = await readUsageEvents2(paths);
|
|
@@ -3618,8 +4502,67 @@ function registerMemorySuggest(memory2) {
|
|
|
3618
4502
|
count: v.count,
|
|
3619
4503
|
tools: [...v.tools].sort(),
|
|
3620
4504
|
last_used: v.last,
|
|
3621
|
-
reason: chooseReason(v.tools, v.count)
|
|
4505
|
+
reason: chooseReason(v.tools, v.count),
|
|
4506
|
+
inferred_type: inferType(v.tools, query)
|
|
3622
4507
|
})).sort((a, b) => b.count - a.count);
|
|
4508
|
+
if (opts.autoSave) {
|
|
4509
|
+
const topN = Math.max(1, parseInt(opts.topN ?? "3", 10));
|
|
4510
|
+
const scope = opts.scope === "team" ? "team" : "personal";
|
|
4511
|
+
const top = suggestions.slice(0, topN);
|
|
4512
|
+
if (top.length === 0) {
|
|
4513
|
+
ui.warn(`No suggestions met --min=${minCount} \u2014 nothing to draft.`);
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
const created = [];
|
|
4517
|
+
const skipped = [];
|
|
4518
|
+
const existing = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
|
|
4519
|
+
for (const s of top) {
|
|
4520
|
+
const slug = slugify(s.query);
|
|
4521
|
+
if (!slug) {
|
|
4522
|
+
skipped.push({ query: s.query, reason: "could not derive a slug" });
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
const dup = existing.find(({ memory: memory3 }) => memory3.frontmatter.id.endsWith(`-${slug}`));
|
|
4526
|
+
if (dup) {
|
|
4527
|
+
skipped.push({ query: s.query, reason: `similar memory already exists (${dup.memory.frontmatter.id})` });
|
|
4528
|
+
continue;
|
|
4529
|
+
}
|
|
4530
|
+
const fm = buildFrontmatter7({
|
|
4531
|
+
type: s.inferred_type,
|
|
4532
|
+
slug,
|
|
4533
|
+
scope,
|
|
4534
|
+
tags: ["auto-suggested", ...s.tools],
|
|
4535
|
+
paths: [],
|
|
4536
|
+
symbols: []
|
|
4537
|
+
});
|
|
4538
|
+
fm.status = "draft";
|
|
4539
|
+
const body = renderTemplate(s);
|
|
4540
|
+
const file = memoryFilePath6(paths, fm.scope, fm.id, fm.module);
|
|
4541
|
+
await mkdir12(path32.dirname(file), { recursive: true });
|
|
4542
|
+
if (existsSync33(file)) {
|
|
4543
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path32.relative(root, file)}` });
|
|
4544
|
+
continue;
|
|
4545
|
+
}
|
|
4546
|
+
await writeFile18(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
|
|
4547
|
+
created.push({ id: fm.id, file: path32.relative(root, file), query: s.query });
|
|
4548
|
+
}
|
|
4549
|
+
if (opts.json) {
|
|
4550
|
+
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
for (const c of created) {
|
|
4554
|
+
ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
|
|
4555
|
+
console.log(` ${ui.dim("from query:")} ${truncate(c.query, 60)}`);
|
|
4556
|
+
}
|
|
4557
|
+
for (const s of skipped) {
|
|
4558
|
+
ui.warn(`Skipped: ${truncate(s.query, 50)} \u2014 ${s.reason}`);
|
|
4559
|
+
}
|
|
4560
|
+
if (created.length > 0) {
|
|
4561
|
+
console.log();
|
|
4562
|
+
ui.info("Drafts are status=draft \u2014 edit them, then `haive memory promote` to validate.");
|
|
4563
|
+
}
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
3623
4566
|
if (opts.json) {
|
|
3624
4567
|
console.log(JSON.stringify({ window: opts.since, suggestions }, null, 2));
|
|
3625
4568
|
return;
|
|
@@ -3640,6 +4583,8 @@ function registerMemorySuggest(memory2) {
|
|
|
3640
4583
|
);
|
|
3641
4584
|
console.log(` ${ui.dim("\u2192")} ${s.reason}`);
|
|
3642
4585
|
}
|
|
4586
|
+
console.log();
|
|
4587
|
+
ui.info("Run with --auto-save to draft the top-3 as draft memories.");
|
|
3643
4588
|
});
|
|
3644
4589
|
}
|
|
3645
4590
|
function chooseReason(tools, count) {
|
|
@@ -3651,14 +4596,594 @@ function chooseReason(tools, count) {
|
|
|
3651
4596
|
}
|
|
3652
4597
|
return `${count} agents asked the briefing for this \u2014 consider promoting the answer to a team memory.`;
|
|
3653
4598
|
}
|
|
4599
|
+
function inferType(tools, query) {
|
|
4600
|
+
const q = query.toLowerCase();
|
|
4601
|
+
if (q.includes("bug") || q.includes("error") || q.includes("crash") || q.includes("trap")) return "gotcha";
|
|
4602
|
+
if (q.includes("decid") || q.includes("why") || q.includes("choose") || q.includes("vs ")) return "decision";
|
|
4603
|
+
if (tools.has("code_search") && (q.includes("where") || q.includes("location") || q.includes("structure"))) {
|
|
4604
|
+
return "architecture";
|
|
4605
|
+
}
|
|
4606
|
+
return "convention";
|
|
4607
|
+
}
|
|
4608
|
+
function renderTemplate(s) {
|
|
4609
|
+
return [
|
|
4610
|
+
`# Auto-drafted from recurring searches`,
|
|
4611
|
+
``,
|
|
4612
|
+
`> This memory was drafted by \`haive memory suggest --auto-save\` because`,
|
|
4613
|
+
`> agents searched for this **${s.count} times** in the recent window`,
|
|
4614
|
+
`> via ${s.tools.join(", ")}.`,
|
|
4615
|
+
``,
|
|
4616
|
+
`## Query`,
|
|
4617
|
+
``,
|
|
4618
|
+
`> ${s.query}`,
|
|
4619
|
+
``,
|
|
4620
|
+
`## What to fill in`,
|
|
4621
|
+
``,
|
|
4622
|
+
`Replace this section with the actual answer the team keeps re-discovering:`,
|
|
4623
|
+
``,
|
|
4624
|
+
`- **What** \u2014 the convention / decision / gotcha (1-3 sentences)`,
|
|
4625
|
+
`- **Why** \u2014 the rationale or root cause`,
|
|
4626
|
+
`- **How to apply** \u2014 what an agent should do when this comes up again`,
|
|
4627
|
+
``,
|
|
4628
|
+
`Then run \`haive memory promote ${truncate(s.query, 30)}\` to mark it validated.`
|
|
4629
|
+
].join("\n");
|
|
4630
|
+
}
|
|
4631
|
+
function slugify(s) {
|
|
4632
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
4633
|
+
}
|
|
3654
4634
|
function truncate(text, max) {
|
|
3655
4635
|
if (text.length <= max) return text;
|
|
3656
4636
|
return text.slice(0, max - 1) + "\u2026";
|
|
3657
4637
|
}
|
|
3658
4638
|
|
|
4639
|
+
// src/commands/memory-archive.ts
|
|
4640
|
+
import { existsSync as existsSync34 } from "fs";
|
|
4641
|
+
import { writeFile as writeFile19 } from "fs/promises";
|
|
4642
|
+
import path33 from "path";
|
|
4643
|
+
import "commander";
|
|
4644
|
+
import {
|
|
4645
|
+
findProjectRoot as findProjectRoot35,
|
|
4646
|
+
getUsage as getUsage9,
|
|
4647
|
+
loadMemoriesFromDir as loadMemoriesFromDir9,
|
|
4648
|
+
loadUsageIndex as loadUsageIndex11,
|
|
4649
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
4650
|
+
serializeMemory as serializeMemory15
|
|
4651
|
+
} from "@hiveai/core";
|
|
4652
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4653
|
+
function registerMemoryArchive(memory2) {
|
|
4654
|
+
memory2.command("archive").description(
|
|
4655
|
+
"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."
|
|
4656
|
+
).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) => {
|
|
4657
|
+
const root = findProjectRoot35(opts.dir);
|
|
4658
|
+
const paths = resolveHaivePaths32(root);
|
|
4659
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
4660
|
+
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
4661
|
+
process.exitCode = 1;
|
|
4662
|
+
return;
|
|
4663
|
+
}
|
|
4664
|
+
const minDays = parseDays(opts.since ?? "180d");
|
|
4665
|
+
if (minDays === null) {
|
|
4666
|
+
ui.error(`Invalid --since value: ${opts.since}. Use formats like '180d', '6m', '1y'.`);
|
|
4667
|
+
process.exitCode = 1;
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
const cutoff = Date.now() - minDays * MS_PER_DAY;
|
|
4671
|
+
const all = await loadMemoriesFromDir9(paths.memoriesDir);
|
|
4672
|
+
const usage = await loadUsageIndex11(paths);
|
|
4673
|
+
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
4674
|
+
const candidates = [];
|
|
4675
|
+
for (const { memory: mem, filePath } of all) {
|
|
4676
|
+
const fm = mem.frontmatter;
|
|
4677
|
+
if (typeFilter && fm.type !== typeFilter) continue;
|
|
4678
|
+
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
4679
|
+
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
4680
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync34(path33.join(paths.root, p)));
|
|
4681
|
+
const isAnchorless = !hasAnyAnchor;
|
|
4682
|
+
if (!isAnchorless && !allPathsGone) continue;
|
|
4683
|
+
const u = getUsage9(usage, fm.id);
|
|
4684
|
+
const lastSeen = u.last_read_at ?? fm.created_at;
|
|
4685
|
+
if (Date.parse(lastSeen) >= cutoff) continue;
|
|
4686
|
+
candidates.push({
|
|
4687
|
+
id: fm.id,
|
|
4688
|
+
type: fm.type,
|
|
4689
|
+
status: fm.status,
|
|
4690
|
+
last_seen: lastSeen,
|
|
4691
|
+
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)}`,
|
|
4692
|
+
filePath
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
if (opts.json) {
|
|
4696
|
+
console.log(JSON.stringify({
|
|
4697
|
+
dry_run: !opts.apply,
|
|
4698
|
+
window_days: minDays,
|
|
4699
|
+
candidates: candidates.length,
|
|
4700
|
+
archived: opts.apply ? candidates.length : 0,
|
|
4701
|
+
items: candidates
|
|
4702
|
+
}, null, 2));
|
|
4703
|
+
} else {
|
|
4704
|
+
const header = opts.apply ? "Archiving" : "Would archive";
|
|
4705
|
+
console.log(ui.bold(`${header} ${candidates.length} memor${candidates.length === 1 ? "y" : "ies"} (older than ${minDays}d, type=${typeFilter ?? "all"})`));
|
|
4706
|
+
if (candidates.length === 0) {
|
|
4707
|
+
ui.info("Nothing to archive \u2014 all memories are anchored or read recently.");
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
for (const c of candidates) {
|
|
4711
|
+
console.log(` ${ui.dim(c.last_seen.slice(0, 10))} ${c.id} ${ui.dim(`(${c.type})`)} \u2014 ${c.reason}`);
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
if (!opts.apply) {
|
|
4715
|
+
if (!opts.json) {
|
|
4716
|
+
console.log();
|
|
4717
|
+
ui.info("Dry run \u2014 pass --apply to mark these as deprecated on disk.");
|
|
4718
|
+
}
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
let archived = 0;
|
|
4722
|
+
let failed = 0;
|
|
4723
|
+
for (const c of candidates) {
|
|
4724
|
+
const found = all.find(({ filePath }) => filePath === c.filePath);
|
|
4725
|
+
if (!found) continue;
|
|
4726
|
+
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
4727
|
+
try {
|
|
4728
|
+
await writeFile19(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
4729
|
+
archived++;
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
if (!opts.json) {
|
|
4732
|
+
ui.error(`Failed to archive ${c.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4733
|
+
}
|
|
4734
|
+
failed++;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (!opts.json) {
|
|
4738
|
+
ui.success(`Archived ${archived} memor${archived === 1 ? "y" : "ies"}${failed > 0 ? ` (${failed} failed)` : ""}`);
|
|
4739
|
+
}
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
function parseDays(input) {
|
|
4743
|
+
const m = input.match(/^(\d+)([dmy])$/);
|
|
4744
|
+
if (!m) return null;
|
|
4745
|
+
const n = parseInt(m[1] ?? "0", 10);
|
|
4746
|
+
const unit = m[2] ?? "d";
|
|
4747
|
+
if (unit === "d") return n;
|
|
4748
|
+
if (unit === "m") return n * 30;
|
|
4749
|
+
if (unit === "y") return n * 365;
|
|
4750
|
+
return null;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
// src/commands/doctor.ts
|
|
4754
|
+
import { existsSync as existsSync35 } from "fs";
|
|
4755
|
+
import { stat } from "fs/promises";
|
|
4756
|
+
import "path";
|
|
4757
|
+
import "commander";
|
|
4758
|
+
import {
|
|
4759
|
+
codeMapPath as codeMapPath2,
|
|
4760
|
+
findProjectRoot as findProjectRoot36,
|
|
4761
|
+
getUsage as getUsage10,
|
|
4762
|
+
loadCodeMap as loadCodeMap3,
|
|
4763
|
+
loadConfig as loadConfig4,
|
|
4764
|
+
loadMemoriesFromDir as loadMemoriesFromDir10,
|
|
4765
|
+
loadUsageIndex as loadUsageIndex12,
|
|
4766
|
+
readUsageEvents as readUsageEvents3,
|
|
4767
|
+
resolveHaivePaths as resolveHaivePaths33
|
|
4768
|
+
} from "@hiveai/core";
|
|
4769
|
+
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
4770
|
+
function registerDoctor(program2) {
|
|
4771
|
+
program2.command("doctor").description(
|
|
4772
|
+
"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."
|
|
4773
|
+
).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) => {
|
|
4774
|
+
const root = findProjectRoot36(opts.dir);
|
|
4775
|
+
const paths = resolveHaivePaths33(root);
|
|
4776
|
+
const findings = [];
|
|
4777
|
+
if (!existsSync35(paths.haiveDir)) {
|
|
4778
|
+
findings.push({
|
|
4779
|
+
severity: "error",
|
|
4780
|
+
code: "not-initialized",
|
|
4781
|
+
message: ".ai/ directory missing \u2014 haive is not initialized in this project.",
|
|
4782
|
+
fix: "haive init"
|
|
4783
|
+
});
|
|
4784
|
+
return emit(findings, opts);
|
|
4785
|
+
}
|
|
4786
|
+
if (!existsSync35(paths.projectContext)) {
|
|
4787
|
+
findings.push({
|
|
4788
|
+
severity: "warn",
|
|
4789
|
+
code: "no-project-context",
|
|
4790
|
+
message: ".ai/project-context.md is missing.",
|
|
4791
|
+
fix: "haive init"
|
|
4792
|
+
});
|
|
4793
|
+
} else {
|
|
4794
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
4795
|
+
const content = await readFile13(paths.projectContext, "utf8");
|
|
4796
|
+
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
4797
|
+
if (isTemplate) {
|
|
4798
|
+
findings.push({
|
|
4799
|
+
severity: "warn",
|
|
4800
|
+
code: "template-context",
|
|
4801
|
+
message: "project-context.md still contains the default template \u2014 get_briefing returns little value until filled.",
|
|
4802
|
+
fix: "Invoke the bootstrap_project MCP prompt from your AI client."
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
const memories = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
|
|
4807
|
+
const now = Date.now();
|
|
4808
|
+
if (memories.length === 0) {
|
|
4809
|
+
findings.push({
|
|
4810
|
+
severity: "info",
|
|
4811
|
+
code: "no-memories",
|
|
4812
|
+
message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
|
|
4813
|
+
});
|
|
4814
|
+
} else {
|
|
4815
|
+
const usage = await loadUsageIndex12(paths);
|
|
4816
|
+
const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
|
|
4817
|
+
if (stale.length > 0) {
|
|
4818
|
+
findings.push({
|
|
4819
|
+
severity: "warn",
|
|
4820
|
+
code: "stale-memories",
|
|
4821
|
+
message: `${stale.length} memor${stale.length === 1 ? "y" : "ies"} marked stale (anchored code drifted).`,
|
|
4822
|
+
fix: "haive memory verify --update # re-check anchors\nhaive memory edit <id> # manually refresh body"
|
|
4823
|
+
});
|
|
4824
|
+
}
|
|
4825
|
+
const proposed = memories.filter((m) => m.memory.frontmatter.status === "proposed");
|
|
4826
|
+
if (proposed.length > 0) {
|
|
4827
|
+
findings.push({
|
|
4828
|
+
severity: "info",
|
|
4829
|
+
code: "pending-review",
|
|
4830
|
+
message: `${proposed.length} memor${proposed.length === 1 ? "y is" : "ies are"} proposed and awaiting validation.`,
|
|
4831
|
+
fix: "haive memory pending # list them\nhaive memory auto-promote # promote those with high read_count"
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
const anchorless = memories.filter(
|
|
4835
|
+
(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"
|
|
4836
|
+
);
|
|
4837
|
+
if (anchorless.length / Math.max(memories.length, 1) > 0.3) {
|
|
4838
|
+
findings.push({
|
|
4839
|
+
severity: "warn",
|
|
4840
|
+
code: "anchorless-majority",
|
|
4841
|
+
message: `${anchorless.length}/${memories.length} memories have no anchor path/symbol \u2014 staleness undetectable.`,
|
|
4842
|
+
fix: "Add `paths:` + `symbols:` to mem_save calls to enable haive memory verify."
|
|
4843
|
+
});
|
|
4844
|
+
}
|
|
4845
|
+
const decayCandidates = memories.filter((m) => {
|
|
4846
|
+
if (m.memory.frontmatter.status !== "validated") return false;
|
|
4847
|
+
const u = getUsage10(usage, m.memory.frontmatter.id);
|
|
4848
|
+
const last = u.last_read_at ?? m.memory.frontmatter.created_at;
|
|
4849
|
+
return (now - Date.parse(last)) / MS_PER_DAY2 > 180;
|
|
4850
|
+
});
|
|
4851
|
+
if (decayCandidates.length > 0) {
|
|
4852
|
+
findings.push({
|
|
4853
|
+
severity: "info",
|
|
4854
|
+
code: "decay-candidates",
|
|
4855
|
+
message: `${decayCandidates.length} validated memor${decayCandidates.length === 1 ? "y has" : "ies have"} not been read in 180+ days \u2014 confidence is decaying.`,
|
|
4856
|
+
fix: "haive memory archive --type all --since 365d # dry run"
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
const codeMap = await loadCodeMap3(paths);
|
|
4861
|
+
if (!codeMap) {
|
|
4862
|
+
findings.push({
|
|
4863
|
+
severity: "warn",
|
|
4864
|
+
code: "no-code-map",
|
|
4865
|
+
message: "No code-map found \u2014 code_map MCP tool and symbol_locations are unavailable.",
|
|
4866
|
+
fix: "haive index code"
|
|
4867
|
+
});
|
|
4868
|
+
} else {
|
|
4869
|
+
const cmFile = codeMapPath2(paths);
|
|
4870
|
+
const cmStat = await stat(cmFile);
|
|
4871
|
+
const ageDays = (now - cmStat.mtimeMs) / MS_PER_DAY2;
|
|
4872
|
+
if (ageDays > 14) {
|
|
4873
|
+
findings.push({
|
|
4874
|
+
severity: "warn",
|
|
4875
|
+
code: "stale-code-map",
|
|
4876
|
+
message: `code-map is ${Math.round(ageDays)} days old (${Object.keys(codeMap.files).length} files indexed).`,
|
|
4877
|
+
fix: "haive index code # or rely on the post-merge git hook"
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
const events = await readUsageEvents3(paths);
|
|
4882
|
+
if (events.length === 0) {
|
|
4883
|
+
findings.push({
|
|
4884
|
+
severity: "info",
|
|
4885
|
+
code: "no-usage-log",
|
|
4886
|
+
message: "No usage log entries \u2014 MCP server hasn't recorded any calls yet, or this project hasn't been used by an agent."
|
|
4887
|
+
});
|
|
4888
|
+
} else {
|
|
4889
|
+
const queryRepeats = /* @__PURE__ */ new Map();
|
|
4890
|
+
for (const e of events) {
|
|
4891
|
+
if (!isSearchTool(e.tool)) continue;
|
|
4892
|
+
const key = (e.summary ?? "").toLowerCase().trim();
|
|
4893
|
+
if (!key) continue;
|
|
4894
|
+
queryRepeats.set(key, (queryRepeats.get(key) ?? 0) + 1);
|
|
4895
|
+
}
|
|
4896
|
+
const repeated = [...queryRepeats.entries()].filter(([, n]) => n >= 3);
|
|
4897
|
+
if (repeated.length > 0) {
|
|
4898
|
+
findings.push({
|
|
4899
|
+
severity: "info",
|
|
4900
|
+
code: "recurring-searches",
|
|
4901
|
+
message: `${repeated.length} query${repeated.length === 1 ? "" : "ies"} repeated 3+ times \u2014 agents keep asking the same things.`,
|
|
4902
|
+
fix: `haive memory suggest --auto-save --top-n ${Math.min(5, repeated.length)}`
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
const codeMapCalls = events.filter((e) => e.tool === "code_map").length;
|
|
4906
|
+
const briefingCalls = events.filter((e) => e.tool === "get_briefing").length;
|
|
4907
|
+
if (codeMapCalls > 0 && memories.length > 0) {
|
|
4908
|
+
findings.push({
|
|
4909
|
+
severity: "info",
|
|
4910
|
+
code: "tool-mix",
|
|
4911
|
+
message: `${briefingCalls} get_briefing call${briefingCalls === 1 ? "" : "s"}, ${codeMapCalls} code_map call${codeMapCalls === 1 ? "" : "s"} recorded.`
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
const config = await loadConfig4(paths);
|
|
4916
|
+
if (!config.autoSessionEnd) {
|
|
4917
|
+
findings.push({
|
|
4918
|
+
severity: "info",
|
|
4919
|
+
code: "no-autopilot",
|
|
4920
|
+
message: "Autopilot is OFF \u2014 session recaps are not auto-saved on shutdown.",
|
|
4921
|
+
fix: "Edit .ai/haive.config.json: set autoSessionEnd: true (or re-run `haive init` without --manual)."
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4924
|
+
emit(findings, opts);
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
function emit(findings, opts) {
|
|
4928
|
+
if (opts.json) {
|
|
4929
|
+
console.log(JSON.stringify({ findings }, null, 2));
|
|
4930
|
+
return;
|
|
4931
|
+
}
|
|
4932
|
+
if (findings.length === 0) {
|
|
4933
|
+
ui.success("hAIve doctor \u2014 no issues found.");
|
|
4934
|
+
return;
|
|
4935
|
+
}
|
|
4936
|
+
console.log(ui.bold(`hAIve doctor \u2014 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
|
|
4937
|
+
console.log();
|
|
4938
|
+
const order = ["error", "warn", "info"];
|
|
4939
|
+
for (const sev of order) {
|
|
4940
|
+
for (const f of findings.filter((x) => x.severity === sev)) {
|
|
4941
|
+
const icon = sev === "error" ? ui.red("\u2717") : sev === "warn" ? ui.yellow("\u26A0") : ui.dim("\u2139");
|
|
4942
|
+
console.log(`${icon} ${ui.bold(f.code)} ${f.message}`);
|
|
4943
|
+
if (opts.fix && f.fix) {
|
|
4944
|
+
for (const line of f.fix.split("\n")) {
|
|
4945
|
+
console.log(` ${ui.dim("$")} ${line}`);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
if (!opts.fix && findings.some((f) => f.fix)) {
|
|
4951
|
+
console.log();
|
|
4952
|
+
ui.info("Re-run with --fix to see suggested commands.");
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
function isSearchTool(name) {
|
|
4956
|
+
return ["mem_search", "code_search", "mem_relevant_to", "get_briefing"].includes(name);
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
// src/commands/playback.ts
|
|
4960
|
+
import { existsSync as existsSync36 } from "fs";
|
|
4961
|
+
import "commander";
|
|
4962
|
+
import {
|
|
4963
|
+
findProjectRoot as findProjectRoot37,
|
|
4964
|
+
loadMemoriesFromDir as loadMemoriesFromDir11,
|
|
4965
|
+
parseSince as parseSince3,
|
|
4966
|
+
readUsageEvents as readUsageEvents4,
|
|
4967
|
+
resolveHaivePaths as resolveHaivePaths34
|
|
4968
|
+
} from "@hiveai/core";
|
|
4969
|
+
var MS_PER_MINUTE = 6e4;
|
|
4970
|
+
function registerPlayback(program2) {
|
|
4971
|
+
program2.command("playback").description(
|
|
4972
|
+
"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?'"
|
|
4973
|
+
).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) => {
|
|
4974
|
+
const root = findProjectRoot37(opts.dir);
|
|
4975
|
+
const paths = resolveHaivePaths34(root);
|
|
4976
|
+
const events = await readUsageEvents4(paths);
|
|
4977
|
+
if (events.length === 0) {
|
|
4978
|
+
if (opts.json) {
|
|
4979
|
+
console.log(JSON.stringify({ sessions: [] }));
|
|
4980
|
+
return;
|
|
4981
|
+
}
|
|
4982
|
+
ui.warn("No usage log entries yet.");
|
|
4983
|
+
return;
|
|
4984
|
+
}
|
|
4985
|
+
const since = parseSince3(opts.since);
|
|
4986
|
+
const cutoff = since ? since.getTime() : 0;
|
|
4987
|
+
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
4988
|
+
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
4989
|
+
const sessions = bucketSessions(filtered, gapMs);
|
|
4990
|
+
const all = existsSync36(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
|
|
4991
|
+
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);
|
|
4992
|
+
const enriched = sessions.map((s, i) => {
|
|
4993
|
+
const startMs = Date.parse(s.start);
|
|
4994
|
+
const newer = memByCreatedAt.filter((m) => m.at > startMs);
|
|
4995
|
+
return {
|
|
4996
|
+
index: i,
|
|
4997
|
+
start: s.start,
|
|
4998
|
+
end: s.end,
|
|
4999
|
+
duration_minutes: (Date.parse(s.end) - startMs) / MS_PER_MINUTE,
|
|
5000
|
+
events: s.events.length,
|
|
5001
|
+
tools_count: countTools(s.events),
|
|
5002
|
+
briefing_tasks: s.events.filter((e) => e.tool === "get_briefing" && e.summary).map((e) => e.summary).slice(0, 5),
|
|
5003
|
+
memories_created_since: newer.length,
|
|
5004
|
+
new_memories: newer.slice(0, 5).map((m) => m.id)
|
|
5005
|
+
};
|
|
5006
|
+
});
|
|
5007
|
+
enriched.sort((a, b) => Date.parse(b.start) - Date.parse(a.start));
|
|
5008
|
+
const limit = Math.max(1, parseInt(opts.limit ?? "10", 10));
|
|
5009
|
+
const shown = enriched.slice(0, limit);
|
|
5010
|
+
if (opts.json) {
|
|
5011
|
+
console.log(JSON.stringify({
|
|
5012
|
+
window: opts.since,
|
|
5013
|
+
session_gap_minutes: gapMs / MS_PER_MINUTE,
|
|
5014
|
+
total_sessions: enriched.length,
|
|
5015
|
+
sessions: shown
|
|
5016
|
+
}, null, 2));
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
console.log(ui.bold(`hAIve playback \u2014 ${enriched.length} session(s) over ${opts.since ?? "all time"}`));
|
|
5020
|
+
console.log();
|
|
5021
|
+
for (const s of shown) {
|
|
5022
|
+
console.log(
|
|
5023
|
+
`${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"})`)}`
|
|
5024
|
+
);
|
|
5025
|
+
const toolList = Object.entries(s.tools_count).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, n]) => `${t}\xD7${n}`).join(", ");
|
|
5026
|
+
if (toolList) console.log(` ${ui.dim("tools:")} ${toolList}`);
|
|
5027
|
+
if (s.briefing_tasks.length > 0) {
|
|
5028
|
+
console.log(` ${ui.dim("briefings asked:")}`);
|
|
5029
|
+
for (const t of s.briefing_tasks) {
|
|
5030
|
+
console.log(` \u2022 ${truncate2(t, 80)}`);
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
if (s.memories_created_since > 0) {
|
|
5034
|
+
console.log(
|
|
5035
|
+
` ${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`)
|
|
5036
|
+
);
|
|
5037
|
+
for (const id of s.new_memories) {
|
|
5038
|
+
console.log(` + ${ui.dim(id)}`);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
console.log();
|
|
5042
|
+
}
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
function bucketSessions(events, gapMs) {
|
|
5046
|
+
if (events.length === 0) return [];
|
|
5047
|
+
const sorted = [...events].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
5048
|
+
const buckets = [];
|
|
5049
|
+
let current = null;
|
|
5050
|
+
for (const e of sorted) {
|
|
5051
|
+
if (!current) {
|
|
5052
|
+
current = { start: e.at, end: e.at, events: [e] };
|
|
5053
|
+
continue;
|
|
5054
|
+
}
|
|
5055
|
+
if (Date.parse(e.at) - Date.parse(current.end) > gapMs) {
|
|
5056
|
+
buckets.push(current);
|
|
5057
|
+
current = { start: e.at, end: e.at, events: [e] };
|
|
5058
|
+
} else {
|
|
5059
|
+
current.events.push(e);
|
|
5060
|
+
current.end = e.at;
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
if (current) buckets.push(current);
|
|
5064
|
+
return buckets;
|
|
5065
|
+
}
|
|
5066
|
+
function countTools(events) {
|
|
5067
|
+
const out = {};
|
|
5068
|
+
for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
|
|
5069
|
+
return out;
|
|
5070
|
+
}
|
|
5071
|
+
function truncate2(text, max) {
|
|
5072
|
+
if (text.length <= max) return text;
|
|
5073
|
+
return text.slice(0, max - 1) + "\u2026";
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
// src/commands/precommit.ts
|
|
5077
|
+
import { spawn as spawn3 } from "child_process";
|
|
5078
|
+
import "commander";
|
|
5079
|
+
import {
|
|
5080
|
+
findProjectRoot as findProjectRoot38,
|
|
5081
|
+
resolveHaivePaths as resolveHaivePaths35
|
|
5082
|
+
} from "@hiveai/core";
|
|
5083
|
+
import { preCommitCheck } from "@hiveai/mcp";
|
|
5084
|
+
function registerPrecommit(program2) {
|
|
5085
|
+
program2.command("precommit").description(
|
|
5086
|
+
"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"
|
|
5087
|
+
).option(
|
|
5088
|
+
"--block-on <mode>",
|
|
5089
|
+
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
5090
|
+
"high-confidence"
|
|
5091
|
+
).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) => {
|
|
5092
|
+
const root = findProjectRoot38(opts.dir);
|
|
5093
|
+
const paths = resolveHaivePaths35(root);
|
|
5094
|
+
const ctx = { paths };
|
|
5095
|
+
let diff = "";
|
|
5096
|
+
let touchedPaths = opts.paths ?? [];
|
|
5097
|
+
if (touchedPaths.length === 0) {
|
|
5098
|
+
try {
|
|
5099
|
+
diff = await runCommand("git", ["diff", "--cached"], root);
|
|
5100
|
+
if (!diff.trim()) {
|
|
5101
|
+
ui.warn("No staged changes \u2014 nothing to check. Stage with `git add` first.");
|
|
5102
|
+
return;
|
|
5103
|
+
}
|
|
5104
|
+
const nameOnly = await runCommand("git", ["diff", "--cached", "--name-only"], root);
|
|
5105
|
+
touchedPaths = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
5106
|
+
} catch (err) {
|
|
5107
|
+
ui.error(`git diff failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5108
|
+
process.exit(1);
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
const result = await preCommitCheck({
|
|
5112
|
+
diff: diff || void 0,
|
|
5113
|
+
paths: touchedPaths,
|
|
5114
|
+
block_on: opts.blockOn ?? "high-confidence",
|
|
5115
|
+
semantic: opts.noSemantic ? false : true
|
|
5116
|
+
}, ctx);
|
|
5117
|
+
if (opts.json) {
|
|
5118
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5119
|
+
process.exit(result.should_block ? 1 : 0);
|
|
5120
|
+
}
|
|
5121
|
+
console.log(ui.bold(`hAIve precommit \u2014 ${touchedPaths.length} file(s)`));
|
|
5122
|
+
console.log(
|
|
5123
|
+
ui.dim(
|
|
5124
|
+
` anti-patterns: ${result.summary.anti_patterns} relevant memories: ${result.summary.relevant_memories} stale anchors: ${result.summary.stale_anchors}`
|
|
5125
|
+
)
|
|
5126
|
+
);
|
|
5127
|
+
console.log();
|
|
5128
|
+
if (result.warnings.length > 0) {
|
|
5129
|
+
console.log(ui.bold("\u26A0 Anti-patterns matched:"));
|
|
5130
|
+
for (const w of result.warnings.slice(0, 10)) {
|
|
5131
|
+
console.log(` ${ui.yellow("\u26A0")} ${w.id} ${ui.dim(`(${w.type}, ${w.confidence})`)}`);
|
|
5132
|
+
for (const line of w.body_preview.split("\n").slice(0, 3)) {
|
|
5133
|
+
console.log(` ${ui.dim(line)}`);
|
|
5134
|
+
}
|
|
5135
|
+
console.log(` ${ui.dim("reasons:")} ${w.reasons.join(", ")}`);
|
|
5136
|
+
}
|
|
5137
|
+
console.log();
|
|
5138
|
+
}
|
|
5139
|
+
if (result.relevant_memories.length > 0) {
|
|
5140
|
+
console.log(ui.bold("\u{1F4CC} Relevant conventions/decisions:"));
|
|
5141
|
+
for (const m of result.relevant_memories) {
|
|
5142
|
+
console.log(` \u2022 ${m.id} ${ui.dim(`(${m.type}, ${m.confidence})`)}`);
|
|
5143
|
+
}
|
|
5144
|
+
console.log();
|
|
5145
|
+
}
|
|
5146
|
+
if (result.stale_anchors.length > 0) {
|
|
5147
|
+
console.log(ui.bold("\u{1F552} Stale anchored memories:"));
|
|
5148
|
+
for (const s of result.stale_anchors) {
|
|
5149
|
+
console.log(` \u2022 ${s.id}`);
|
|
5150
|
+
if (s.body_preview) console.log(` ${ui.dim(s.body_preview)}`);
|
|
5151
|
+
}
|
|
5152
|
+
console.log();
|
|
5153
|
+
}
|
|
5154
|
+
if (result.should_block) {
|
|
5155
|
+
ui.error(`Blocking commit (block_on=${opts.blockOn ?? "high-confidence"}). Address the warnings above or pass --block-on never to bypass.`);
|
|
5156
|
+
process.exit(1);
|
|
5157
|
+
}
|
|
5158
|
+
if (result.warnings.length === 0 && result.stale_anchors.length === 0) {
|
|
5159
|
+
ui.success("No anti-patterns or stale anchors found.");
|
|
5160
|
+
} else {
|
|
5161
|
+
ui.success("Check passed (block_on threshold not met).");
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
function runCommand(cmd, args, cwd) {
|
|
5166
|
+
return new Promise((resolve, reject) => {
|
|
5167
|
+
const proc = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
5168
|
+
let stdout = "";
|
|
5169
|
+
let stderr = "";
|
|
5170
|
+
proc.stdout.on("data", (chunk) => {
|
|
5171
|
+
stdout += chunk.toString();
|
|
5172
|
+
});
|
|
5173
|
+
proc.stderr.on("data", (chunk) => {
|
|
5174
|
+
stderr += chunk.toString();
|
|
5175
|
+
});
|
|
5176
|
+
proc.on("error", reject);
|
|
5177
|
+
proc.on("close", (code) => {
|
|
5178
|
+
if (code === 0) resolve(stdout);
|
|
5179
|
+
else reject(new Error(stderr || `${cmd} exited with code ${code}`));
|
|
5180
|
+
});
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
|
|
3659
5184
|
// src/index.ts
|
|
3660
|
-
var program = new
|
|
3661
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
5185
|
+
var program = new Command39();
|
|
5186
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.0");
|
|
3662
5187
|
registerInit(program);
|
|
3663
5188
|
registerMcp(program);
|
|
3664
5189
|
registerBriefing(program);
|
|
@@ -3689,12 +5214,16 @@ registerMemoryImport(memory);
|
|
|
3689
5214
|
registerMemoryImportChangelog(memory);
|
|
3690
5215
|
registerMemoryDigest(memory);
|
|
3691
5216
|
registerMemorySuggest(memory);
|
|
5217
|
+
registerMemoryArchive(memory);
|
|
3692
5218
|
var session = program.command("session").description("Manage session lifecycle");
|
|
3693
5219
|
registerSessionEnd(session);
|
|
3694
5220
|
registerSnapshot(program);
|
|
3695
5221
|
registerHub(program);
|
|
3696
5222
|
registerStats(program);
|
|
3697
5223
|
registerBench(program);
|
|
5224
|
+
registerDoctor(program);
|
|
5225
|
+
registerPlayback(program);
|
|
5226
|
+
registerPrecommit(program);
|
|
3698
5227
|
program.parseAsync(process.argv).catch((err) => {
|
|
3699
5228
|
if (isZodError(err)) {
|
|
3700
5229
|
for (const issue of err.issues) {
|