@hiveai/cli 0.6.0 → 0.7.1
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 +1561 -341
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -408,9 +408,9 @@ function registerIndexCode(program2) {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
// src/commands/init.ts
|
|
411
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
412
|
-
import { existsSync as
|
|
413
|
-
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";
|
|
414
414
|
import { spawnSync } from "child_process";
|
|
415
415
|
import "commander";
|
|
416
416
|
import {
|
|
@@ -420,9 +420,1101 @@ import {
|
|
|
420
420
|
saveCodeMap as saveCodeMap2,
|
|
421
421
|
saveConfig
|
|
422
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", "@trpc/client"],
|
|
461
|
+
"Prisma": ["@prisma/client"],
|
|
462
|
+
"Drizzle": ["drizzle-orm"],
|
|
463
|
+
"Redux Toolkit": ["@reduxjs/toolkit"],
|
|
464
|
+
"Zustand": ["zustand"],
|
|
465
|
+
"TanStack Query": ["@tanstack/react-query", "react-query"],
|
|
466
|
+
"Mongoose": ["mongoose"],
|
|
467
|
+
"Apollo": ["@apollo/client", "@apollo/server", "apollo-server"],
|
|
468
|
+
"GraphQL": ["graphql"],
|
|
469
|
+
"Vite": ["vite"],
|
|
470
|
+
"Vitest": ["vitest"],
|
|
471
|
+
"Jest": ["jest"]
|
|
472
|
+
};
|
|
473
|
+
var KEY_DEPS = [
|
|
474
|
+
"@nestjs/jwt",
|
|
475
|
+
"@nestjs/passport",
|
|
476
|
+
"passport-jwt",
|
|
477
|
+
"jsonwebtoken",
|
|
478
|
+
"bcrypt",
|
|
479
|
+
"bcryptjs",
|
|
480
|
+
"stripe",
|
|
481
|
+
"axios",
|
|
482
|
+
"socket.io",
|
|
483
|
+
"ws",
|
|
484
|
+
"redis",
|
|
485
|
+
"ioredis",
|
|
486
|
+
"pg",
|
|
487
|
+
"mysql2",
|
|
488
|
+
"mongodb",
|
|
489
|
+
"mongoose",
|
|
490
|
+
"zod",
|
|
491
|
+
"yup",
|
|
492
|
+
"class-validator",
|
|
493
|
+
"tailwindcss",
|
|
494
|
+
"shadcn",
|
|
495
|
+
"@radix-ui",
|
|
496
|
+
"@vercel/ai",
|
|
497
|
+
"ai",
|
|
498
|
+
"openai",
|
|
499
|
+
"@anthropic-ai/sdk",
|
|
500
|
+
"typescript"
|
|
501
|
+
];
|
|
502
|
+
function detectFrameworks(allDeps) {
|
|
503
|
+
const found = [];
|
|
504
|
+
for (const [fw, signals] of Object.entries(FRAMEWORK_SIGNALS)) {
|
|
505
|
+
if (signals.some((s) => allDeps[s] !== void 0)) found.push(fw);
|
|
506
|
+
}
|
|
507
|
+
return found;
|
|
508
|
+
}
|
|
509
|
+
function detectKeyDeps(allDeps) {
|
|
510
|
+
return KEY_DEPS.filter((d) => allDeps[d] !== void 0);
|
|
511
|
+
}
|
|
512
|
+
function detectLanguage(root) {
|
|
513
|
+
if (existsSync3(path4.join(root, "tsconfig.json"))) return "TypeScript";
|
|
514
|
+
if (existsSync3(path4.join(root, "pyproject.toml")) || existsSync3(path4.join(root, "setup.py"))) return "Python";
|
|
515
|
+
if (existsSync3(path4.join(root, "go.mod"))) return "Go";
|
|
516
|
+
if (existsSync3(path4.join(root, "pom.xml")) || existsSync3(path4.join(root, "build.gradle"))) return "Java/Kotlin";
|
|
517
|
+
if (existsSync3(path4.join(root, "Cargo.toml"))) return "Rust";
|
|
518
|
+
if (existsSync3(path4.join(root, "package.json"))) return "JavaScript";
|
|
519
|
+
return "Unknown";
|
|
520
|
+
}
|
|
521
|
+
function detectProjectType(frameworks, scripts, isMonorepo) {
|
|
522
|
+
if (isMonorepo) {
|
|
523
|
+
if (frameworks.includes("NestJS")) return "Monorepo (NestJS backend)";
|
|
524
|
+
if (frameworks.includes("Next.js")) return "Monorepo (Next.js)";
|
|
525
|
+
if (frameworks.includes("React")) return "Multi-package monorepo (React)";
|
|
526
|
+
if (frameworks.length > 0) return `Multi-package monorepo (${frameworks.slice(0, 2).join(", ")})`;
|
|
527
|
+
return "Multi-package monorepo";
|
|
528
|
+
}
|
|
529
|
+
if (frameworks.includes("NestJS")) return "Backend API (NestJS)";
|
|
530
|
+
if (frameworks.includes("Next.js")) return "Full-stack web app (Next.js)";
|
|
531
|
+
if (frameworks.includes("Remix")) return "Full-stack web app (Remix)";
|
|
532
|
+
if (frameworks.includes("Express") || frameworks.includes("Fastify") || frameworks.includes("Hono")) return "Backend API";
|
|
533
|
+
if (frameworks.includes("React") || frameworks.includes("Vue") || frameworks.includes("Svelte")) return "Frontend SPA";
|
|
534
|
+
if (scripts["build"] && !scripts["dev"]) return "CLI tool / library";
|
|
535
|
+
if (existsSync3("pom.xml")) return "Java backend";
|
|
536
|
+
return "Application";
|
|
537
|
+
}
|
|
538
|
+
async function scanDirs(root, maxDepth = 2) {
|
|
539
|
+
const results = [];
|
|
540
|
+
async function walk(dir, depth) {
|
|
541
|
+
if (depth > maxDepth) return;
|
|
542
|
+
let entries;
|
|
543
|
+
try {
|
|
544
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
545
|
+
} catch {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
for (const entry of entries) {
|
|
549
|
+
if (!entry.isDirectory()) continue;
|
|
550
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
551
|
+
const rel = path4.relative(root, path4.join(dir, entry.name));
|
|
552
|
+
results.push(rel);
|
|
553
|
+
await walk(path4.join(dir, entry.name), depth + 1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
await walk(root, 0);
|
|
557
|
+
return results;
|
|
558
|
+
}
|
|
559
|
+
function inferModuleDescriptions(dirs, frameworks = []) {
|
|
560
|
+
const known = {
|
|
561
|
+
"src": "main source directory",
|
|
562
|
+
"app": "application entrypoint / routes (Next.js App Router or similar)",
|
|
563
|
+
"pages": "file-based routing pages",
|
|
564
|
+
"components": "reusable UI components",
|
|
565
|
+
"lib": "shared utilities and helpers",
|
|
566
|
+
"utils": "utility functions",
|
|
567
|
+
"hooks": "React hooks",
|
|
568
|
+
"services": "business logic services",
|
|
569
|
+
"controllers": "HTTP controllers / route handlers",
|
|
570
|
+
"modules": "feature modules",
|
|
571
|
+
"middleware": "HTTP or business middleware",
|
|
572
|
+
"guards": "auth / access guards",
|
|
573
|
+
"decorators": "custom decorators",
|
|
574
|
+
"interceptors": "NestJS interceptors",
|
|
575
|
+
"filters": "exception filters",
|
|
576
|
+
"pipes": "validation / transformation pipes",
|
|
577
|
+
"dto": "Data Transfer Objects",
|
|
578
|
+
"entities": "ORM entities / database models",
|
|
579
|
+
"prisma": "Prisma schema and migrations",
|
|
580
|
+
"migrations": "database migrations",
|
|
581
|
+
"config": "configuration files",
|
|
582
|
+
"types": "TypeScript type definitions",
|
|
583
|
+
"schemas": "validation schemas (Zod / class-validator)",
|
|
584
|
+
"test": "tests",
|
|
585
|
+
"tests": "tests",
|
|
586
|
+
"__tests__": "tests",
|
|
587
|
+
"e2e": "end-to-end tests",
|
|
588
|
+
"public": "static public assets",
|
|
589
|
+
"assets": "static assets",
|
|
590
|
+
"styles": "global CSS / style files",
|
|
591
|
+
"scripts": "build or utility scripts",
|
|
592
|
+
"docs": "documentation",
|
|
593
|
+
"docker": "Docker configuration",
|
|
594
|
+
"infra": "infrastructure / IaC",
|
|
595
|
+
"packages": "monorepo sub-packages",
|
|
596
|
+
"functions": "serverless / edge functions",
|
|
597
|
+
"api": "API routes or client",
|
|
598
|
+
"store": "state management (Redux / Zustand / Pinia)",
|
|
599
|
+
"context": "React contexts",
|
|
600
|
+
"server": "server-side code",
|
|
601
|
+
"client": "client-side code",
|
|
602
|
+
"features": "feature-based modules",
|
|
603
|
+
"routes": "route definitions",
|
|
604
|
+
"workers": "background workers / queues",
|
|
605
|
+
"auth": "authentication / authorization",
|
|
606
|
+
"users": "user management",
|
|
607
|
+
"products": "product catalog",
|
|
608
|
+
"orders": "order management",
|
|
609
|
+
"common": "shared / common utilities",
|
|
610
|
+
"shared": "shared code across modules"
|
|
611
|
+
};
|
|
612
|
+
const isNestJS = frameworks.includes("NestJS");
|
|
613
|
+
const srcSubdirs = dirs.filter((d) => d.startsWith("src/") && d.split("/").length === 2);
|
|
614
|
+
if (isNestJS && srcSubdirs.length >= 2) {
|
|
615
|
+
const result = [`- \`src/\` \u2014 main source (NestJS feature modules)`];
|
|
616
|
+
for (const d of srcSubdirs.slice(0, 12)) {
|
|
617
|
+
const name = d.split("/")[1];
|
|
618
|
+
const desc = known[name.toLowerCase()] ?? "feature module";
|
|
619
|
+
result.push(` - \`${name}/\` \u2014 ${desc}`);
|
|
620
|
+
}
|
|
621
|
+
const otherTopLevel = dirs.filter((d) => !d.includes("/") && d !== "src").slice(0, 6);
|
|
622
|
+
for (const d of otherTopLevel) {
|
|
623
|
+
const desc = known[d.toLowerCase()] ?? "module";
|
|
624
|
+
result.push(`- \`${d}/\` \u2014 ${desc}`);
|
|
625
|
+
}
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
const isMonorepo = dirs.some((d) => d === "packages") && dirs.some((d) => d.startsWith("packages/") && d.split("/").length === 2);
|
|
629
|
+
if (isMonorepo) {
|
|
630
|
+
const packageSubdirs = dirs.filter((d) => d.startsWith("packages/") && d.split("/").length === 2);
|
|
631
|
+
const result = [`- \`packages/\` \u2014 monorepo sub-packages`];
|
|
632
|
+
for (const d of packageSubdirs.slice(0, 10)) {
|
|
633
|
+
const name = d.split("/")[1];
|
|
634
|
+
const desc = known[name.toLowerCase()] ?? "sub-package";
|
|
635
|
+
result.push(` - \`${name}/\` \u2014 ${desc}`);
|
|
636
|
+
}
|
|
637
|
+
const otherTopLevel = dirs.filter((d) => !d.includes("/") && d !== "packages").slice(0, 5);
|
|
638
|
+
for (const d of otherTopLevel) {
|
|
639
|
+
const desc = known[d.toLowerCase()] ?? "module";
|
|
640
|
+
result.push(`- \`${d}/\` \u2014 ${desc}`);
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
const top = dirs.filter((d) => !d.includes("/")).slice(0, 12);
|
|
645
|
+
return top.map((d) => {
|
|
646
|
+
const desc = known[d.toLowerCase()] ?? "module";
|
|
647
|
+
return `- \`${d}/\` \u2014 ${desc}`;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function readmeExcerpt(readme) {
|
|
651
|
+
const lines = readme.split("\n");
|
|
652
|
+
let inContent = false;
|
|
653
|
+
const kept = [];
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
if (!inContent && line.trim().startsWith("#")) {
|
|
656
|
+
inContent = true;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (!inContent) continue;
|
|
660
|
+
if (kept.length >= 6) break;
|
|
661
|
+
if (line.trim()) kept.push(line.trim());
|
|
662
|
+
}
|
|
663
|
+
return kept.join(" ").slice(0, 400);
|
|
664
|
+
}
|
|
665
|
+
async function generateBootstrapContext(root) {
|
|
666
|
+
let pkg = {};
|
|
667
|
+
const pkgPath = path4.join(root, "package.json");
|
|
668
|
+
if (existsSync3(pkgPath)) {
|
|
669
|
+
try {
|
|
670
|
+
pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
675
|
+
const frameworks = detectFrameworks(allDeps);
|
|
676
|
+
const keyDeps = detectKeyDeps(allDeps);
|
|
677
|
+
const language = detectLanguage(root);
|
|
678
|
+
const isMonorepo = pkg.workspaces !== void 0 && (Array.isArray(pkg.workspaces) ? pkg.workspaces.length > 0 : true);
|
|
679
|
+
const projectType = detectProjectType(frameworks, pkg.scripts ?? {}, isMonorepo);
|
|
680
|
+
const projectName = pkg.name ?? path4.basename(root);
|
|
681
|
+
const projectDesc = pkg.description ?? "";
|
|
682
|
+
let readmeSummary = "";
|
|
683
|
+
for (const name of ["README.md", "readme.md", "README"]) {
|
|
684
|
+
const p = path4.join(root, name);
|
|
685
|
+
if (existsSync3(p)) {
|
|
686
|
+
try {
|
|
687
|
+
const content = await readFile2(p, "utf8");
|
|
688
|
+
readmeSummary = readmeExcerpt(content);
|
|
689
|
+
break;
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const dirs = await scanDirs(root, 2);
|
|
695
|
+
const moduleLines = inferModuleDescriptions(dirs, frameworks);
|
|
696
|
+
const scripts = pkg.scripts ?? {};
|
|
697
|
+
const scriptLines = Object.entries(scripts).filter(([k]) => ["build", "dev", "start", "test", "lint", "deploy"].includes(k)).map(([k, v]) => `- \`${k}\`: ${v}`).slice(0, 6);
|
|
698
|
+
const stackParts = [language];
|
|
699
|
+
if (frameworks.length) stackParts.push(...frameworks);
|
|
700
|
+
const techStack = stackParts.join(", ");
|
|
701
|
+
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}\``);
|
|
702
|
+
const lines = [
|
|
703
|
+
`# Project context \u2014 ${projectName}`,
|
|
704
|
+
"",
|
|
705
|
+
`> Auto-generated by \`haive init --bootstrap\`. Review and refine \u2014 especially the Architecture and Gotchas sections.`,
|
|
706
|
+
"",
|
|
707
|
+
`## Overview`,
|
|
708
|
+
`**Type:** ${projectType}`,
|
|
709
|
+
`**Tech stack:** ${techStack}`,
|
|
710
|
+
...projectDesc ? [`**Description:** ${projectDesc}`] : [],
|
|
711
|
+
...readmeSummary ? [`**From README:** ${readmeSummary}`] : [],
|
|
712
|
+
"",
|
|
713
|
+
`## Architecture`,
|
|
714
|
+
`TODO \u2014 fill in the high-level architecture (inferred structure below, verify manually):`,
|
|
715
|
+
"",
|
|
716
|
+
...moduleLines.length ? moduleLines : ["TODO \u2014 no clear structure detected."],
|
|
717
|
+
"",
|
|
718
|
+
`## Key modules`,
|
|
719
|
+
`TODO \u2014 describe the purpose of the main modules. The directory scan found:`,
|
|
720
|
+
...dirs.filter((d) => !d.includes("/")).slice(0, 8).map((d) => `- \`${d}/\``),
|
|
721
|
+
"",
|
|
722
|
+
`## Conventions`,
|
|
723
|
+
`TODO \u2014 fill in coding conventions (naming, patterns, file layout).`,
|
|
724
|
+
"",
|
|
725
|
+
...scriptLines.length ? [
|
|
726
|
+
`**Available scripts:**`,
|
|
727
|
+
...scriptLines,
|
|
728
|
+
""
|
|
729
|
+
] : [],
|
|
730
|
+
...keyDeps.length ? [
|
|
731
|
+
`**Key dependencies in use:** ${keyDeps.map((d) => `\`${d}\``).join(", ")}`,
|
|
732
|
+
""
|
|
733
|
+
] : [],
|
|
734
|
+
...notableDeps.length ? [
|
|
735
|
+
`**Other notable packages:** ${notableDeps.join(", ")}`,
|
|
736
|
+
""
|
|
737
|
+
] : [],
|
|
738
|
+
`## Glossary`,
|
|
739
|
+
`TODO \u2014 domain terms and what they mean here.`,
|
|
740
|
+
"",
|
|
741
|
+
`## Gotchas`,
|
|
742
|
+
`TODO \u2014 known traps, surprising behavior, things newcomers stub their toes on.`,
|
|
743
|
+
`(Run \`haive memory import-changelog\` or \`haive memory import README.md\` to seed these automatically.)`,
|
|
744
|
+
""
|
|
745
|
+
];
|
|
746
|
+
return lines.join("\n");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/commands/init-mcp-setup.ts
|
|
750
|
+
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
751
|
+
import { existsSync as existsSync4 } from "fs";
|
|
752
|
+
import path5 from "path";
|
|
753
|
+
import os from "os";
|
|
754
|
+
var HOME = os.homedir();
|
|
755
|
+
var HAIVE_MCP_ENTRY = {
|
|
756
|
+
command: "haive-mcp",
|
|
757
|
+
args: []
|
|
758
|
+
};
|
|
759
|
+
function cursorMcpPath() {
|
|
760
|
+
return path5.join(HOME, ".cursor", "mcp.json");
|
|
761
|
+
}
|
|
762
|
+
async function configureCursor() {
|
|
763
|
+
const mcpPath = cursorMcpPath();
|
|
764
|
+
const cursorDir = path5.join(HOME, ".cursor");
|
|
765
|
+
if (!existsSync4(cursorDir)) return { client: "Cursor", status: "not_installed" };
|
|
766
|
+
let config = {};
|
|
767
|
+
if (existsSync4(mcpPath)) {
|
|
768
|
+
try {
|
|
769
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
config.mcpServers ??= {};
|
|
774
|
+
if (config.mcpServers["haive"]) return { client: "Cursor", status: "already_configured" };
|
|
775
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
776
|
+
await mkdir(cursorDir, { recursive: true });
|
|
777
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
778
|
+
return { client: "Cursor", status: "configured", path: mcpPath };
|
|
779
|
+
}
|
|
780
|
+
function vscodeMcpPath() {
|
|
781
|
+
const candidates = [
|
|
782
|
+
path5.join(HOME, ".config", "Code", "User", "mcp.json"),
|
|
783
|
+
// Linux
|
|
784
|
+
path5.join(HOME, "Library", "Application Support", "Code", "User", "mcp.json"),
|
|
785
|
+
// macOS
|
|
786
|
+
path5.join(HOME, "AppData", "Roaming", "Code", "User", "mcp.json"),
|
|
787
|
+
// Windows
|
|
788
|
+
path5.join(HOME, ".config", "Code - Insiders", "User", "mcp.json")
|
|
789
|
+
];
|
|
790
|
+
for (const c of candidates) {
|
|
791
|
+
if (existsSync4(path5.dirname(c))) return c;
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
async function configureVSCode() {
|
|
796
|
+
const mcpPath = vscodeMcpPath();
|
|
797
|
+
if (!mcpPath) return { client: "VS Code", status: "not_installed" };
|
|
798
|
+
let config = {};
|
|
799
|
+
if (existsSync4(mcpPath)) {
|
|
800
|
+
try {
|
|
801
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
config.servers ??= {};
|
|
806
|
+
if (config.servers["haive"]) return { client: "VS Code", status: "already_configured" };
|
|
807
|
+
config.servers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
808
|
+
await mkdir(path5.dirname(mcpPath), { recursive: true });
|
|
809
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
810
|
+
return { client: "VS Code", status: "configured", path: mcpPath };
|
|
811
|
+
}
|
|
812
|
+
function claudeConfigPath() {
|
|
813
|
+
const p = path5.join(HOME, ".claude.json");
|
|
814
|
+
if (existsSync4(p)) return p;
|
|
815
|
+
const p2 = path5.join(HOME, ".config", "claude", "claude.json");
|
|
816
|
+
if (existsSync4(path5.dirname(p2))) return p2;
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
async function configureClaude() {
|
|
820
|
+
const cfgPath = claudeConfigPath() ?? path5.join(HOME, ".claude.json");
|
|
821
|
+
if (!existsSync4(cfgPath) && !existsSync4(path5.join(HOME, ".claude"))) {
|
|
822
|
+
return { client: "Claude Code", status: "not_installed" };
|
|
823
|
+
}
|
|
824
|
+
let config = {};
|
|
825
|
+
if (existsSync4(cfgPath)) {
|
|
826
|
+
try {
|
|
827
|
+
config = JSON.parse(await readFile3(cfgPath, "utf8"));
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
config.mcpServers ??= {};
|
|
832
|
+
if (config.mcpServers["haive"]) return { client: "Claude Code", status: "already_configured" };
|
|
833
|
+
config.mcpServers["haive"] = { ...HAIVE_MCP_ENTRY, type: "stdio" };
|
|
834
|
+
await writeFile(cfgPath, JSON.stringify(config, null, 2), "utf8");
|
|
835
|
+
return { client: "Claude Code", status: "configured", path: cfgPath };
|
|
836
|
+
}
|
|
837
|
+
function windsurfMcpPath() {
|
|
838
|
+
const candidates = [
|
|
839
|
+
path5.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
840
|
+
path5.join(HOME, ".windsurf", "mcp.json")
|
|
841
|
+
];
|
|
842
|
+
for (const c of candidates) {
|
|
843
|
+
if (existsSync4(path5.dirname(c))) return c;
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
async function configureWindsurf() {
|
|
848
|
+
const mcpPath = windsurfMcpPath();
|
|
849
|
+
if (!mcpPath) return { client: "Windsurf", status: "not_installed" };
|
|
850
|
+
let config = {};
|
|
851
|
+
if (existsSync4(mcpPath)) {
|
|
852
|
+
try {
|
|
853
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
config.mcpServers ??= {};
|
|
858
|
+
if (config.mcpServers["haive"]) return { client: "Windsurf", status: "already_configured" };
|
|
859
|
+
config.mcpServers["haive"] = HAIVE_MCP_ENTRY;
|
|
860
|
+
await mkdir(path5.dirname(mcpPath), { recursive: true });
|
|
861
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
862
|
+
return { client: "Windsurf", status: "configured", path: mcpPath };
|
|
863
|
+
}
|
|
864
|
+
async function autoConfigureMcpClients() {
|
|
865
|
+
const results = [];
|
|
866
|
+
const configurators = [configureCursor, configureVSCode, configureClaude, configureWindsurf];
|
|
867
|
+
for (const fn of configurators) {
|
|
868
|
+
try {
|
|
869
|
+
results.push(await fn());
|
|
870
|
+
} catch (err) {
|
|
871
|
+
const name = fn.name.replace("configure", "");
|
|
872
|
+
results.push({ client: name, status: "error", error: String(err) });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return results;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/commands/init-stack-packs.ts
|
|
879
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
880
|
+
import { existsSync as existsSync5 } from "fs";
|
|
881
|
+
import path6 from "path";
|
|
882
|
+
import {
|
|
883
|
+
buildFrontmatter,
|
|
884
|
+
memoryFilePath,
|
|
885
|
+
serializeMemory
|
|
886
|
+
} from "@hiveai/core";
|
|
887
|
+
var PACKS = {
|
|
888
|
+
nestjs: [
|
|
889
|
+
{
|
|
890
|
+
slug: "jwtmodule-requires-secret",
|
|
891
|
+
type: "gotcha",
|
|
892
|
+
tags: ["auth", "jwt", "nestjs"],
|
|
893
|
+
body: `JwtModule must be registered with an explicit secret \u2014 there is no default.
|
|
894
|
+
|
|
895
|
+
\`\`\`ts
|
|
896
|
+
JwtModule.register({ secret: process.env.JWT_SECRET, signOptions: { expiresIn: '7d' } })
|
|
897
|
+
\`\`\`
|
|
898
|
+
|
|
899
|
+
Without a secret, tokens are signed with an empty string and any client can forge them.
|
|
900
|
+
Always load the secret from env and validate it is defined at startup.`
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
slug: "global-validation-pipe",
|
|
904
|
+
type: "convention",
|
|
905
|
+
tags: ["validation", "nestjs", "security"],
|
|
906
|
+
body: `Register ValidationPipe globally in main.ts, not per-controller.
|
|
907
|
+
|
|
908
|
+
\`\`\`ts
|
|
909
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
|
|
910
|
+
\`\`\`
|
|
911
|
+
|
|
912
|
+
- \`whitelist: true\` strips unknown properties silently
|
|
913
|
+
- \`forbidNonWhitelisted: true\` throws 400 on unknown fields (safer)
|
|
914
|
+
- Without this, NestJS passes unvalidated payloads to handlers.`
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
slug: "nestjs-no-direct-orm-in-controller",
|
|
918
|
+
type: "convention",
|
|
919
|
+
tags: ["architecture", "nestjs"],
|
|
920
|
+
body: `Controllers must never import Prisma/TypeORM directly \u2014 that belongs in Services.
|
|
921
|
+
|
|
922
|
+
Controller \u2192 Service \u2192 Repository (or direct ORM) is the required layering.
|
|
923
|
+
Direct ORM usage in controllers makes testing impossible and couples transport to persistence.`
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
slug: "nestjs-exception-filter-for-prisma",
|
|
927
|
+
type: "gotcha",
|
|
928
|
+
tags: ["error-handling", "nestjs", "prisma"],
|
|
929
|
+
body: `Prisma errors bubble up as unhandled 500s without a custom exception filter.
|
|
930
|
+
|
|
931
|
+
Create an \`AllExceptionsFilter\` or a specific \`PrismaClientExceptionFilter\` that maps:
|
|
932
|
+
- P2002 (unique constraint) \u2192 409 Conflict
|
|
933
|
+
- P2025 (record not found) \u2192 404 Not Found
|
|
934
|
+
- P2003 (foreign key) \u2192 422 Unprocessable
|
|
935
|
+
|
|
936
|
+
Without this, clients receive raw Prisma error messages which may leak schema info.`
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
nextjs: [
|
|
940
|
+
{
|
|
941
|
+
slug: "server-components-no-client-hooks",
|
|
942
|
+
type: "gotcha",
|
|
943
|
+
tags: ["nextjs", "react", "server-components"],
|
|
944
|
+
body: `Server Components cannot use useState, useEffect, or any browser APIs.
|
|
945
|
+
|
|
946
|
+
Add \`"use client"\` at the top of any component that needs hooks or event handlers.
|
|
947
|
+
The boundary propagates down \u2014 children of a client component don't need the directive.
|
|
948
|
+
|
|
949
|
+
Common mistake: importing a client-only library (e.g. framer-motion) in a server component
|
|
950
|
+
causes a cryptic runtime error. Check for browser globals (window, document, localStorage).`
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
slug: "nextjs-env-client-exposure",
|
|
954
|
+
type: "gotcha",
|
|
955
|
+
tags: ["security", "nextjs", "env"],
|
|
956
|
+
body: `Only environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser.
|
|
957
|
+
|
|
958
|
+
Never put secrets in NEXT_PUBLIC_* variables \u2014 they are bundled into the client JS.
|
|
959
|
+
Variables without the prefix are server-only and safe for API keys, database URLs, etc.`
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
slug: "nextjs-fetch-cache-defaults",
|
|
963
|
+
type: "gotcha",
|
|
964
|
+
tags: ["nextjs", "caching", "fetch"],
|
|
965
|
+
body: `In Next.js App Router, \`fetch()\` is cached indefinitely by default in Server Components.
|
|
966
|
+
|
|
967
|
+
Add \`{ cache: 'no-store' }\` for dynamic data, or \`{ next: { revalidate: 60 } }\` for ISR.
|
|
968
|
+
Forgetting this means stale data is returned after a deploy until the cache expires.`
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
slug: "nextjs-metadata-api",
|
|
972
|
+
type: "convention",
|
|
973
|
+
tags: ["nextjs", "seo"],
|
|
974
|
+
body: `Use the Metadata API (export const metadata / generateMetadata) instead of <Head>.
|
|
975
|
+
|
|
976
|
+
\`<Head>\` from next/head still works in pages/ but is not supported in the App Router.
|
|
977
|
+
Use \`generateMetadata\` for dynamic titles/descriptions based on route params.`
|
|
978
|
+
}
|
|
979
|
+
],
|
|
980
|
+
remix: [
|
|
981
|
+
{
|
|
982
|
+
slug: "remix-loader-vs-action",
|
|
983
|
+
type: "convention",
|
|
984
|
+
tags: ["remix", "architecture"],
|
|
985
|
+
body: `loader = GET data for rendering. action = handle form submissions / mutations.
|
|
986
|
+
|
|
987
|
+
- \`loader\` runs on every GET request (server-side, returns data for the component)
|
|
988
|
+
- \`action\` runs on POST/PUT/DELETE (mutations \u2014 redirect after success)
|
|
989
|
+
- Never fetch inside the component itself for route data \u2014 use the loader instead.`
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
slug: "remix-error-boundaries",
|
|
993
|
+
type: "gotcha",
|
|
994
|
+
tags: ["remix", "error-handling"],
|
|
995
|
+
body: `Each route should export an ErrorBoundary to catch loader/action errors gracefully.
|
|
996
|
+
|
|
997
|
+
Without it, errors bubble to the root boundary and replace the entire page.
|
|
998
|
+
Export \`export function ErrorBoundary() { ... }\` to scope errors to the route.`
|
|
999
|
+
}
|
|
1000
|
+
],
|
|
1001
|
+
react: [
|
|
1002
|
+
{
|
|
1003
|
+
slug: "useeffect-cleanup",
|
|
1004
|
+
type: "gotcha",
|
|
1005
|
+
tags: ["react", "memory-leak"],
|
|
1006
|
+
body: `useEffect subscriptions, timers, and async operations need cleanup to avoid memory leaks.
|
|
1007
|
+
|
|
1008
|
+
\`\`\`ts
|
|
1009
|
+
useEffect(() => {
|
|
1010
|
+
const controller = new AbortController();
|
|
1011
|
+
fetchData({ signal: controller.signal });
|
|
1012
|
+
return () => controller.abort(); // cleanup
|
|
1013
|
+
}, [dep]);
|
|
1014
|
+
\`\`\`
|
|
1015
|
+
|
|
1016
|
+
Missing cleanup causes: state updates on unmounted components, duplicate subscriptions,
|
|
1017
|
+
and event listeners that accumulate across re-renders.`
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
slug: "react-key-prop-in-lists",
|
|
1021
|
+
type: "gotcha",
|
|
1022
|
+
tags: ["react", "performance"],
|
|
1023
|
+
body: `Keys must be stable, unique IDs \u2014 never use array index as key.
|
|
1024
|
+
|
|
1025
|
+
Using index as key causes React to re-render wrong items on reorder/filter,
|
|
1026
|
+
corrupts form state, and triggers avoidable DOM mutations.
|
|
1027
|
+
Use item.id or a stable hash \u2014 never Math.random().`
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
slug: "react-avoid-use-effect-for-derived-state",
|
|
1031
|
+
type: "convention",
|
|
1032
|
+
tags: ["react", "state"],
|
|
1033
|
+
body: `Don't use useEffect to sync state from props \u2014 compute it during render instead.
|
|
1034
|
+
|
|
1035
|
+
\`\`\`ts
|
|
1036
|
+
// \u274C Bad
|
|
1037
|
+
const [fullName, setFullName] = useState('');
|
|
1038
|
+
useEffect(() => { setFullName(first + ' ' + last); }, [first, last]);
|
|
1039
|
+
|
|
1040
|
+
// \u2705 Good
|
|
1041
|
+
const fullName = first + ' ' + last; // derived during render
|
|
1042
|
+
\`\`\``
|
|
1043
|
+
}
|
|
1044
|
+
],
|
|
1045
|
+
express: [
|
|
1046
|
+
{
|
|
1047
|
+
slug: "express-missing-validation",
|
|
1048
|
+
type: "gotcha",
|
|
1049
|
+
tags: ["security", "express", "validation"],
|
|
1050
|
+
body: `Express does not validate request bodies by default \u2014 always validate with zod, joi, or express-validator.
|
|
1051
|
+
|
|
1052
|
+
Without validation:
|
|
1053
|
+
- req.body fields are \`any\` and may be missing, wrong type, or injected
|
|
1054
|
+
- Downstream code crashes or processes malicious data
|
|
1055
|
+
Add a validation middleware for every route that accepts user input.`
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
slug: "express-async-error-propagation",
|
|
1059
|
+
type: "gotcha",
|
|
1060
|
+
tags: ["express", "error-handling"],
|
|
1061
|
+
body: `Async route handlers don't propagate errors to error middleware without explicit next(err).
|
|
1062
|
+
|
|
1063
|
+
\`\`\`ts
|
|
1064
|
+
// \u274C Unhandled \u2014 Express never sees the rejection
|
|
1065
|
+
app.get('/', async (req, res) => { throw new Error('oops'); });
|
|
1066
|
+
|
|
1067
|
+
// \u2705 Correct
|
|
1068
|
+
app.get('/', async (req, res, next) => {
|
|
1069
|
+
try { await doWork(); }
|
|
1070
|
+
catch (err) { next(err); }
|
|
1071
|
+
});
|
|
1072
|
+
\`\`\`
|
|
1073
|
+
Or use express-async-errors / wrap helper.`
|
|
1074
|
+
}
|
|
1075
|
+
],
|
|
1076
|
+
fastify: [
|
|
1077
|
+
{
|
|
1078
|
+
slug: "fastify-schema-validation-required",
|
|
1079
|
+
type: "convention",
|
|
1080
|
+
tags: ["fastify", "validation", "security"],
|
|
1081
|
+
body: `Always define a JSON schema on routes \u2014 Fastify validates and coerces automatically.
|
|
1082
|
+
|
|
1083
|
+
\`\`\`ts
|
|
1084
|
+
fastify.post('/users', {
|
|
1085
|
+
schema: { body: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' } } } }
|
|
1086
|
+
}, handler)
|
|
1087
|
+
\`\`\`
|
|
1088
|
+
Routes without schema accept any body and bypass Fastify's fast-json-stringify serialization.`
|
|
1089
|
+
}
|
|
1090
|
+
],
|
|
1091
|
+
prisma: [
|
|
1092
|
+
{
|
|
1093
|
+
slug: "prisma-no-disconnect-in-lambda",
|
|
1094
|
+
type: "gotcha",
|
|
1095
|
+
tags: ["prisma", "serverless"],
|
|
1096
|
+
body: `Do NOT call prisma.$disconnect() inside Lambda/Edge function handlers.
|
|
1097
|
+
|
|
1098
|
+
Calling $disconnect() after each request wastes the warm connection pool.
|
|
1099
|
+
Create one PrismaClient per process (module-level singleton), not per request.
|
|
1100
|
+
Disconnecting is only needed when the process is shutting down.`
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
slug: "prisma-migrations-never-modify",
|
|
1104
|
+
type: "convention",
|
|
1105
|
+
tags: ["prisma", "database", "migrations"],
|
|
1106
|
+
body: `Never modify an existing migration file \u2014 create a new one instead.
|
|
1107
|
+
|
|
1108
|
+
Prisma tracks migration history by file hash. Editing a deployed migration
|
|
1109
|
+
causes \`migrate deploy\` to fail with a checksum mismatch in production.
|
|
1110
|
+
Always use \`npx prisma migrate dev --name <description>\` to create incremental migrations.`
|
|
1111
|
+
}
|
|
1112
|
+
],
|
|
1113
|
+
drizzle: [
|
|
1114
|
+
{
|
|
1115
|
+
slug: "drizzle-always-await-queries",
|
|
1116
|
+
type: "gotcha",
|
|
1117
|
+
tags: ["drizzle", "async"],
|
|
1118
|
+
body: `Drizzle queries are thenable but not auto-executed \u2014 always await them.
|
|
1119
|
+
|
|
1120
|
+
\`\`\`ts
|
|
1121
|
+
// \u274C Silently returns a query builder, never executes
|
|
1122
|
+
const rows = db.select().from(users).where(eq(users.id, id));
|
|
1123
|
+
|
|
1124
|
+
// \u2705 Correct
|
|
1125
|
+
const rows = await db.select().from(users).where(eq(users.id, id));
|
|
1126
|
+
\`\`\``
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
slug: "drizzle-schema-must-match-db",
|
|
1130
|
+
type: "gotcha",
|
|
1131
|
+
tags: ["drizzle", "migrations"],
|
|
1132
|
+
body: `Drizzle does NOT auto-sync the schema to the database \u2014 you must run migrations explicitly.
|
|
1133
|
+
|
|
1134
|
+
After changing schema.ts:
|
|
1135
|
+
1. \`npx drizzle-kit generate\` \u2014 creates migration SQL
|
|
1136
|
+
2. \`npx drizzle-kit migrate\` (or push in dev) \u2014 applies it
|
|
1137
|
+
|
|
1138
|
+
Without this, queries silently operate on stale column definitions and may return wrong data.`
|
|
1139
|
+
}
|
|
1140
|
+
],
|
|
1141
|
+
zustand: [
|
|
1142
|
+
{
|
|
1143
|
+
slug: "zustand-select-slices-not-whole-store",
|
|
1144
|
+
type: "convention",
|
|
1145
|
+
tags: ["zustand", "performance", "react"],
|
|
1146
|
+
body: `Always select specific slices \u2014 never subscribe to the whole store.
|
|
1147
|
+
|
|
1148
|
+
\`\`\`ts
|
|
1149
|
+
// \u274C Re-renders on any store change (even unrelated fields)
|
|
1150
|
+
const store = useStore();
|
|
1151
|
+
|
|
1152
|
+
// \u2705 Re-renders only when count changes
|
|
1153
|
+
const count = useStore((s) => s.count);
|
|
1154
|
+
\`\`\`
|
|
1155
|
+
|
|
1156
|
+
Subscribing to the whole store is the single most common Zustand performance mistake.`
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
slug: "zustand-devtools-wrap-dev-only",
|
|
1160
|
+
type: "convention",
|
|
1161
|
+
tags: ["zustand", "devtools", "performance"],
|
|
1162
|
+
body: `Wrap Zustand devtools middleware in a dev-only condition.
|
|
1163
|
+
|
|
1164
|
+
\`\`\`ts
|
|
1165
|
+
import { devtools } from 'zustand/middleware';
|
|
1166
|
+
|
|
1167
|
+
const useStore = create(
|
|
1168
|
+
process.env.NODE_ENV === 'development'
|
|
1169
|
+
? devtools(storeImpl, { name: 'AppStore' })
|
|
1170
|
+
: storeImpl,
|
|
1171
|
+
);
|
|
1172
|
+
\`\`\`
|
|
1173
|
+
|
|
1174
|
+
Shipping devtools to production adds overhead and exposes store internals in bundle.`
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
slug: "zustand-persist-hydration-ssr",
|
|
1178
|
+
type: "gotcha",
|
|
1179
|
+
tags: ["zustand", "ssr", "nextjs", "hydration"],
|
|
1180
|
+
body: `Zustand persist middleware causes hydration mismatch in SSR (Next.js / Remix).
|
|
1181
|
+
|
|
1182
|
+
The server renders with empty state; the client rehydrates from localStorage.
|
|
1183
|
+
Fix: use \`skipHydration: true\` and manually call \`rehydrate()\` after mount.
|
|
1184
|
+
|
|
1185
|
+
\`\`\`ts
|
|
1186
|
+
// In a useEffect or useLayoutEffect on the client:
|
|
1187
|
+
useEffect(() => { useStore.persist.rehydrate(); }, []);
|
|
1188
|
+
\`\`\``
|
|
1189
|
+
}
|
|
1190
|
+
],
|
|
1191
|
+
redux: [
|
|
1192
|
+
{
|
|
1193
|
+
slug: "redux-toolkit-immer-mutate-or-return",
|
|
1194
|
+
type: "gotcha",
|
|
1195
|
+
tags: ["redux", "redux-toolkit", "immer"],
|
|
1196
|
+
body: `In RTK createSlice reducers (Immer), you must EITHER mutate the draft OR return a new value \u2014 never both.
|
|
1197
|
+
|
|
1198
|
+
\`\`\`ts
|
|
1199
|
+
// \u2705 Mutate draft (Immer converts to immutable update)
|
|
1200
|
+
state.count += 1;
|
|
1201
|
+
|
|
1202
|
+
// \u2705 Return new value
|
|
1203
|
+
return { ...state, count: state.count + 1 };
|
|
1204
|
+
|
|
1205
|
+
// \u274C Both \u2014 causes undefined state
|
|
1206
|
+
state.count += 1;
|
|
1207
|
+
return state; // DON'T \u2014 Immer sees both a mutation and a return
|
|
1208
|
+
\`\`\``
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
slug: "redux-toolkit-rtk-query-over-thunk",
|
|
1212
|
+
type: "decision",
|
|
1213
|
+
tags: ["redux", "redux-toolkit", "data-fetching"],
|
|
1214
|
+
body: `Use RTK Query for server data, not createAsyncThunk.
|
|
1215
|
+
|
|
1216
|
+
RTK Query automatically handles: caching, loading/error states, cache invalidation, polling, optimistic updates.
|
|
1217
|
+
createAsyncThunk is for one-off side effects that don't fit the query/mutation model (e.g. file upload with progress).`
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
slug: "redux-toolkit-normalize-nested-data",
|
|
1221
|
+
type: "convention",
|
|
1222
|
+
tags: ["redux", "redux-toolkit", "normalization"],
|
|
1223
|
+
body: `Normalize nested API responses before storing in Redux \u2014 use createEntityAdapter.
|
|
1224
|
+
|
|
1225
|
+
Storing deeply nested objects causes:
|
|
1226
|
+
- Redundant re-renders when any deeply nested field changes
|
|
1227
|
+
- Difficult update logic (deep merge)
|
|
1228
|
+
|
|
1229
|
+
\`\`\`ts
|
|
1230
|
+
const usersAdapter = createEntityAdapter<User>();
|
|
1231
|
+
const usersSlice = createSlice({
|
|
1232
|
+
name: 'users',
|
|
1233
|
+
initialState: usersAdapter.getInitialState(),
|
|
1234
|
+
reducers: { usersReceived: usersAdapter.setAll },
|
|
1235
|
+
});
|
|
1236
|
+
\`\`\``
|
|
1237
|
+
}
|
|
1238
|
+
],
|
|
1239
|
+
reactquery: [
|
|
1240
|
+
{
|
|
1241
|
+
slug: "tanstack-query-stale-time-default",
|
|
1242
|
+
type: "gotcha",
|
|
1243
|
+
tags: ["react-query", "tanstack-query", "caching"],
|
|
1244
|
+
body: `By default, TanStack Query marks data as stale immediately (staleTime: 0) and refetches on every window focus.
|
|
1245
|
+
|
|
1246
|
+
Set a reasonable staleTime to avoid unnecessary network requests:
|
|
1247
|
+
|
|
1248
|
+
\`\`\`ts
|
|
1249
|
+
useQuery({
|
|
1250
|
+
queryKey: ['user', id],
|
|
1251
|
+
queryFn: () => getUser(id),
|
|
1252
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
1253
|
+
})
|
|
1254
|
+
\`\`\`
|
|
1255
|
+
|
|
1256
|
+
Set globally via QueryClient defaultOptions for consistency.`
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
slug: "tanstack-query-invalidate-after-mutation",
|
|
1260
|
+
type: "convention",
|
|
1261
|
+
tags: ["react-query", "tanstack-query", "mutations"],
|
|
1262
|
+
body: `Always invalidate related queries after a mutation to keep the cache fresh.
|
|
1263
|
+
|
|
1264
|
+
\`\`\`ts
|
|
1265
|
+
useMutation({
|
|
1266
|
+
mutationFn: createUser,
|
|
1267
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
|
|
1268
|
+
})
|
|
1269
|
+
\`\`\`
|
|
1270
|
+
|
|
1271
|
+
Skipping invalidation causes the UI to show stale data after a write until the next background refetch.`
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
slug: "tanstack-query-querykey-as-dependency",
|
|
1275
|
+
type: "convention",
|
|
1276
|
+
tags: ["react-query", "tanstack-query"],
|
|
1277
|
+
body: `Treat the queryKey array as a dependency array \u2014 include all variables the queryFn depends on.
|
|
1278
|
+
|
|
1279
|
+
\`\`\`ts
|
|
1280
|
+
// \u274C Won't refetch when userId changes
|
|
1281
|
+
useQuery({ queryKey: ['user'], queryFn: () => getUser(userId) });
|
|
1282
|
+
|
|
1283
|
+
// \u2705 Refetches automatically when userId changes
|
|
1284
|
+
useQuery({ queryKey: ['user', userId], queryFn: () => getUser(userId) });
|
|
1285
|
+
\`\`\``
|
|
1286
|
+
}
|
|
1287
|
+
],
|
|
1288
|
+
trpc: [
|
|
1289
|
+
{
|
|
1290
|
+
slug: "trpc-always-validate-input-with-zod",
|
|
1291
|
+
type: "convention",
|
|
1292
|
+
tags: ["trpc", "validation", "security"],
|
|
1293
|
+
body: `Always validate procedure inputs with Zod \u2014 tRPC infers types but doesn't enforce them at runtime without a schema.
|
|
1294
|
+
|
|
1295
|
+
\`\`\`ts
|
|
1296
|
+
// \u274C No runtime validation \u2014 input is 'unknown'
|
|
1297
|
+
t.procedure.query(({ input }) => getUser(input as string));
|
|
1298
|
+
|
|
1299
|
+
// \u2705 Validated and typed end-to-end
|
|
1300
|
+
t.procedure
|
|
1301
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
1302
|
+
.query(({ input }) => getUser(input.id));
|
|
1303
|
+
\`\`\``
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
slug: "trpc-server-side-caller-for-ssr",
|
|
1307
|
+
type: "convention",
|
|
1308
|
+
tags: ["trpc", "nextjs", "ssr"],
|
|
1309
|
+
body: `Use the server-side caller in Server Components / SSR \u2014 don't call tRPC over HTTP from the server.
|
|
1310
|
+
|
|
1311
|
+
\`\`\`ts
|
|
1312
|
+
// In Next.js App Router server component
|
|
1313
|
+
const caller = appRouter.createCaller(await createContext());
|
|
1314
|
+
const data = await caller.users.getAll(); // Direct function call, no HTTP
|
|
1315
|
+
\`\`\`
|
|
1316
|
+
|
|
1317
|
+
HTTP round-trips from server \u2192 server add latency and bypass auth context.`
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
slug: "trpc-context-for-auth",
|
|
1321
|
+
type: "architecture",
|
|
1322
|
+
tags: ["trpc", "auth"],
|
|
1323
|
+
body: `Put auth session on the tRPC context, not in individual procedures.
|
|
1324
|
+
|
|
1325
|
+
\`\`\`ts
|
|
1326
|
+
// createContext(): resolve session once, share across all procedures
|
|
1327
|
+
export async function createContext({ req }: CreateNextContextOptions) {
|
|
1328
|
+
const session = await getServerSession(req);
|
|
1329
|
+
return { session, db };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// In procedure: ctx.session.user is always typed
|
|
1333
|
+
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
1334
|
+
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
1335
|
+
return next({ ctx: { ...ctx, user: ctx.session.user } });
|
|
1336
|
+
});
|
|
1337
|
+
\`\`\``
|
|
1338
|
+
}
|
|
1339
|
+
],
|
|
1340
|
+
mongoose: [
|
|
1341
|
+
{
|
|
1342
|
+
slug: "mongoose-connection-singleton",
|
|
1343
|
+
type: "convention",
|
|
1344
|
+
tags: ["mongoose", "mongodb", "connection", "serverless"],
|
|
1345
|
+
body: `Create one Mongoose connection at startup \u2014 never connect inside route handlers.
|
|
1346
|
+
|
|
1347
|
+
In serverless (Next.js, Vercel), cache the connection to reuse across warm invocations:
|
|
1348
|
+
|
|
1349
|
+
\`\`\`ts
|
|
1350
|
+
let cached = (global as any).__mongoose ?? { conn: null, promise: null };
|
|
1351
|
+
|
|
1352
|
+
export async function dbConnect() {
|
|
1353
|
+
if (cached.conn) return cached.conn;
|
|
1354
|
+
if (!cached.promise) {
|
|
1355
|
+
cached.promise = mongoose.connect(process.env.MONGODB_URI!);
|
|
1356
|
+
}
|
|
1357
|
+
cached.conn = await cached.promise;
|
|
1358
|
+
(global as any).__mongoose = cached;
|
|
1359
|
+
return cached.conn;
|
|
1360
|
+
}
|
|
1361
|
+
\`\`\``
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
slug: "mongoose-lean-for-read-only",
|
|
1365
|
+
type: "convention",
|
|
1366
|
+
tags: ["mongoose", "performance"],
|
|
1367
|
+
body: `Add .lean() to read-only queries to get plain JS objects instead of full Mongoose documents.
|
|
1368
|
+
|
|
1369
|
+
\`\`\`ts
|
|
1370
|
+
// \u274C Full Mongoose document \u2014 slow, heavy, has virtuals/methods
|
|
1371
|
+
const users = await User.find({});
|
|
1372
|
+
|
|
1373
|
+
// \u2705 Plain JS object \u2014 2-5x faster on large result sets
|
|
1374
|
+
const users = await User.find({}).lean();
|
|
1375
|
+
\`\`\`
|
|
1376
|
+
|
|
1377
|
+
Never use .lean() when you need to call .save() or Mongoose instance methods.`
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
slug: "mongoose-index-frequently-queried-fields",
|
|
1381
|
+
type: "gotcha",
|
|
1382
|
+
tags: ["mongoose", "mongodb", "performance"],
|
|
1383
|
+
body: `Mongoose does NOT create indexes automatically unless you call syncIndexes() or ensureIndexes().
|
|
1384
|
+
|
|
1385
|
+
Declare indexes in the schema and sync them at startup:
|
|
1386
|
+
|
|
1387
|
+
\`\`\`ts
|
|
1388
|
+
UserSchema.index({ email: 1 }, { unique: true });
|
|
1389
|
+
UserSchema.index({ createdAt: -1 });
|
|
1390
|
+
|
|
1391
|
+
// At startup (not per-request):
|
|
1392
|
+
await User.syncIndexes();
|
|
1393
|
+
\`\`\`
|
|
1394
|
+
|
|
1395
|
+
Missing indexes cause full collection scans and timeouts at scale.`
|
|
1396
|
+
}
|
|
1397
|
+
],
|
|
1398
|
+
graphql: [
|
|
1399
|
+
{
|
|
1400
|
+
slug: "graphql-n-plus-one-dataloader",
|
|
1401
|
+
type: "gotcha",
|
|
1402
|
+
tags: ["graphql", "performance", "n+1"],
|
|
1403
|
+
body: `GraphQL resolvers cause N+1 database queries without DataLoader batching.
|
|
1404
|
+
|
|
1405
|
+
Every field resolver runs independently \u2014 fetching related data naively causes N queries for N items.
|
|
1406
|
+
|
|
1407
|
+
\`\`\`ts
|
|
1408
|
+
// In context, create one DataLoader per request (NOT per resolver call)
|
|
1409
|
+
const userLoader = new DataLoader(async (ids: readonly string[]) =>
|
|
1410
|
+
User.findByIds(ids as string[])
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
// In resolver:
|
|
1414
|
+
author: (post) => userLoader.load(post.authorId),
|
|
1415
|
+
\`\`\`
|
|
1416
|
+
|
|
1417
|
+
A list of 100 posts with authors = 101 queries without DataLoader, 2 queries with it.`
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
slug: "graphql-mask-internal-errors-in-production",
|
|
1421
|
+
type: "gotcha",
|
|
1422
|
+
tags: ["graphql", "security", "apollo"],
|
|
1423
|
+
body: `Apollo Server exposes full error details (including stack traces) in development.
|
|
1424
|
+
|
|
1425
|
+
In production, mask internal errors to prevent leaking implementation details:
|
|
1426
|
+
|
|
1427
|
+
\`\`\`ts
|
|
1428
|
+
new ApolloServer({
|
|
1429
|
+
formatError: (formattedError) => {
|
|
1430
|
+
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
|
|
1431
|
+
return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } };
|
|
1432
|
+
}
|
|
1433
|
+
return formattedError;
|
|
1434
|
+
},
|
|
1435
|
+
});
|
|
1436
|
+
\`\`\``
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
slug: "graphql-depth-limit-and-complexity",
|
|
1440
|
+
type: "convention",
|
|
1441
|
+
tags: ["graphql", "security", "dos"],
|
|
1442
|
+
body: `Add query depth and complexity limits to prevent DoS via deeply nested queries.
|
|
1443
|
+
|
|
1444
|
+
Without limits, a single query can request exponentially nested data and exhaust the server.
|
|
1445
|
+
|
|
1446
|
+
\`\`\`ts
|
|
1447
|
+
import depthLimit from 'graphql-depth-limit';
|
|
1448
|
+
import { createComplexityLimitRule } from 'graphql-validation-complexity';
|
|
1449
|
+
|
|
1450
|
+
new ApolloServer({
|
|
1451
|
+
validationRules: [
|
|
1452
|
+
depthLimit(7),
|
|
1453
|
+
createComplexityLimitRule(1000),
|
|
1454
|
+
],
|
|
1455
|
+
});
|
|
1456
|
+
\`\`\``
|
|
1457
|
+
}
|
|
1458
|
+
]
|
|
1459
|
+
};
|
|
1460
|
+
var SUPPORTED_STACKS = Object.keys(PACKS);
|
|
1461
|
+
function isValidStack(name) {
|
|
1462
|
+
return name in PACKS;
|
|
1463
|
+
}
|
|
1464
|
+
function autoDetectStacks(deps) {
|
|
1465
|
+
const detected = [];
|
|
1466
|
+
const stackDetectors = [
|
|
1467
|
+
["nestjs", ["@nestjs/core"]],
|
|
1468
|
+
["nextjs", ["next"]],
|
|
1469
|
+
["remix", ["@remix-run/react", "@remix-run/node"]],
|
|
1470
|
+
["react", ["react"]],
|
|
1471
|
+
["express", ["express"]],
|
|
1472
|
+
["fastify", ["fastify"]],
|
|
1473
|
+
["prisma", ["@prisma/client", "prisma"]],
|
|
1474
|
+
["drizzle", ["drizzle-orm"]],
|
|
1475
|
+
["zustand", ["zustand"]],
|
|
1476
|
+
["redux", ["@reduxjs/toolkit", "redux"]],
|
|
1477
|
+
["reactquery", ["@tanstack/react-query", "react-query"]],
|
|
1478
|
+
["trpc", ["@trpc/server", "@trpc/client"]],
|
|
1479
|
+
["mongoose", ["mongoose"]],
|
|
1480
|
+
["graphql", ["@apollo/client", "@apollo/server", "apollo-server", "graphql"]]
|
|
1481
|
+
];
|
|
1482
|
+
for (const [stack, signals] of stackDetectors) {
|
|
1483
|
+
if (signals.some((s) => s in deps)) detected.push(stack);
|
|
1484
|
+
}
|
|
1485
|
+
if (detected.includes("nextjs") || detected.includes("remix")) {
|
|
1486
|
+
return detected.filter((s) => s !== "react");
|
|
1487
|
+
}
|
|
1488
|
+
return detected;
|
|
1489
|
+
}
|
|
1490
|
+
async function seedStackPack(haivePaths, stack) {
|
|
1491
|
+
const memories = PACKS[stack];
|
|
1492
|
+
if (!memories) return 0;
|
|
1493
|
+
await mkdir2(haivePaths.teamDir, { recursive: true });
|
|
1494
|
+
let count = 0;
|
|
1495
|
+
for (const mem of memories) {
|
|
1496
|
+
const fm = buildFrontmatter({
|
|
1497
|
+
type: mem.type,
|
|
1498
|
+
slug: `${stack}-${mem.slug}`,
|
|
1499
|
+
scope: "team",
|
|
1500
|
+
status: "validated",
|
|
1501
|
+
tags: mem.tags
|
|
1502
|
+
});
|
|
1503
|
+
const filePath = memoryFilePath(haivePaths, "team", fm.id);
|
|
1504
|
+
if (existsSync5(filePath)) continue;
|
|
1505
|
+
const content = serializeMemory({ frontmatter: fm, body: mem.body });
|
|
1506
|
+
await mkdir2(path6.dirname(filePath), { recursive: true });
|
|
1507
|
+
await writeFile2(filePath, content, "utf8");
|
|
1508
|
+
count++;
|
|
1509
|
+
}
|
|
1510
|
+
return count;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/commands/init.ts
|
|
423
1514
|
var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
424
1515
|
|
|
425
|
-
> Generated by \`haive init\`.
|
|
1516
|
+
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
1517
|
+
> or invoke the MCP prompt \`bootstrap_project\` in your AI client for a richer AI-generated version.
|
|
426
1518
|
|
|
427
1519
|
## Architecture
|
|
428
1520
|
TODO \u2014 high-level overview of the codebase.
|
|
@@ -580,23 +1672,46 @@ function registerInit(program2) {
|
|
|
580
1672
|
).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(
|
|
581
1673
|
"--manual",
|
|
582
1674
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
1675
|
+
).option(
|
|
1676
|
+
"--bootstrap",
|
|
1677
|
+
"auto-generate .ai/project-context.md from package.json, README, and directory structure (no AI needed)"
|
|
1678
|
+
).option(
|
|
1679
|
+
"--stack <stacks>",
|
|
1680
|
+
`pre-seed validated memory packs for the given stacks (comma-separated).
|
|
1681
|
+
Supported: ${SUPPORTED_STACKS.join(", ")}.
|
|
1682
|
+
Use 'auto' to detect from package.json automatically.`
|
|
1683
|
+
).option(
|
|
1684
|
+
"--no-mcp-setup",
|
|
1685
|
+
"skip auto-configuring haive-mcp in Cursor / VS Code / Claude Code"
|
|
583
1686
|
).action(async (opts) => {
|
|
584
|
-
const root =
|
|
1687
|
+
const root = path7.resolve(opts.dir);
|
|
585
1688
|
const paths = resolveHaivePaths4(root);
|
|
586
1689
|
const autopilot = opts.manual !== true;
|
|
587
|
-
if (
|
|
1690
|
+
if (existsSync6(paths.haiveDir)) {
|
|
588
1691
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
589
1692
|
}
|
|
590
|
-
await
|
|
591
|
-
await
|
|
592
|
-
await
|
|
593
|
-
await
|
|
594
|
-
if (!
|
|
595
|
-
|
|
596
|
-
|
|
1693
|
+
await mkdir3(paths.personalDir, { recursive: true });
|
|
1694
|
+
await mkdir3(paths.teamDir, { recursive: true });
|
|
1695
|
+
await mkdir3(paths.moduleDir, { recursive: true });
|
|
1696
|
+
await mkdir3(paths.modulesContextDir, { recursive: true });
|
|
1697
|
+
if (!existsSync6(paths.projectContext)) {
|
|
1698
|
+
if (opts.bootstrap) {
|
|
1699
|
+
ui.info("Bootstrapping project context from local files\u2026");
|
|
1700
|
+
try {
|
|
1701
|
+
const context = await generateBootstrapContext(root);
|
|
1702
|
+
await writeFile3(paths.projectContext, context, "utf8");
|
|
1703
|
+
ui.success("Created .ai/project-context.md (auto-bootstrapped from local files)");
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
ui.warn(`Bootstrap failed (${String(err)}) \u2014 writing default template instead`);
|
|
1706
|
+
await writeFile3(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
1707
|
+
}
|
|
1708
|
+
} else {
|
|
1709
|
+
await writeFile3(paths.projectContext, PROJECT_CONTEXT_TEMPLATE, "utf8");
|
|
1710
|
+
ui.success(`Created ${path7.relative(root, paths.projectContext)}`);
|
|
1711
|
+
}
|
|
597
1712
|
}
|
|
598
|
-
const configExists =
|
|
599
|
-
|
|
1713
|
+
const configExists = existsSync6(
|
|
1714
|
+
path7.join(paths.haiveDir, "haive.config.json")
|
|
600
1715
|
);
|
|
601
1716
|
if (!configExists) {
|
|
602
1717
|
await saveConfig(paths, autopilot ? AUTOPILOT_DEFAULTS : { autopilot: false });
|
|
@@ -607,17 +1722,35 @@ function registerInit(program2) {
|
|
|
607
1722
|
if (opts.bridges) {
|
|
608
1723
|
await writeBridge(root, "CLAUDE.md");
|
|
609
1724
|
await writeBridge(root, ".cursorrules");
|
|
610
|
-
await writeBridge(root,
|
|
1725
|
+
await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
|
|
1726
|
+
}
|
|
1727
|
+
const stacksToSeed = await resolveStacksToSeed(root, opts.stack);
|
|
1728
|
+
if (stacksToSeed.length > 0) {
|
|
1729
|
+
let totalSeeded = 0;
|
|
1730
|
+
for (const stack of stacksToSeed) {
|
|
1731
|
+
const count = await seedStackPack(paths, stack);
|
|
1732
|
+
if (count > 0) {
|
|
1733
|
+
ui.success(`Seeded ${count} memories for stack: ${stack}`);
|
|
1734
|
+
totalSeeded += count;
|
|
1735
|
+
} else {
|
|
1736
|
+
ui.info(`Stack pack '${stack}': all memories already exist \u2014 skipped`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (totalSeeded > 0) {
|
|
1740
|
+
ui.success(
|
|
1741
|
+
`${totalSeeded} validated team memories pre-seeded \u2014 haive is useful from J+0`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
611
1744
|
}
|
|
612
1745
|
const wantCi = opts.withCi || autopilot;
|
|
613
1746
|
if (wantCi) {
|
|
614
|
-
const ciPath =
|
|
615
|
-
if (
|
|
1747
|
+
const ciPath = path7.join(root, ".github", "workflows", "haive-sync.yml");
|
|
1748
|
+
if (existsSync6(ciPath)) {
|
|
616
1749
|
ui.info("CI workflow already exists \u2014 skipped");
|
|
617
1750
|
} else {
|
|
618
|
-
await
|
|
619
|
-
await
|
|
620
|
-
ui.success(`Created ${
|
|
1751
|
+
await mkdir3(path7.dirname(ciPath), { recursive: true });
|
|
1752
|
+
await writeFile3(ciPath, CI_WORKFLOW, "utf8");
|
|
1753
|
+
ui.success(`Created ${path7.relative(root, ciPath)}`);
|
|
621
1754
|
}
|
|
622
1755
|
}
|
|
623
1756
|
if (autopilot) {
|
|
@@ -641,6 +1774,27 @@ function registerInit(program2) {
|
|
|
641
1774
|
ui.warn("Code-map build failed \u2014 run `haive index code` manually");
|
|
642
1775
|
}
|
|
643
1776
|
}
|
|
1777
|
+
if (opts.mcpSetup !== false) {
|
|
1778
|
+
const mcpResults = await autoConfigureMcpClients();
|
|
1779
|
+
const configured = mcpResults.filter((r) => r.status === "configured");
|
|
1780
|
+
const alreadyOk = mcpResults.filter((r) => r.status === "already_configured");
|
|
1781
|
+
for (const r of configured) {
|
|
1782
|
+
ui.success(`haive-mcp configured in ${r.client} (${r.path})`);
|
|
1783
|
+
}
|
|
1784
|
+
for (const r of alreadyOk) {
|
|
1785
|
+
ui.info(`haive-mcp already configured in ${r.client} \u2014 skipped`);
|
|
1786
|
+
}
|
|
1787
|
+
if (configured.length === 0 && alreadyOk.length === 0) {
|
|
1788
|
+
ui.warn(
|
|
1789
|
+
"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"
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
if (configured.length > 0) {
|
|
1793
|
+
ui.info(
|
|
1794
|
+
ui.dim(" \u2192 Restart your AI client for MCP changes to take effect.")
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
644
1798
|
ui.success(`hAIve initialized at ${root}${autopilot ? " (autopilot mode)" : ""}`);
|
|
645
1799
|
console.log();
|
|
646
1800
|
if (autopilot) {
|
|
@@ -651,45 +1805,74 @@ function registerInit(program2) {
|
|
|
651
1805
|
console.log(ui.dim(" \u2713 Code-map refreshes automatically after every pull"));
|
|
652
1806
|
console.log(ui.dim(" \u2713 Git hooks installed (auto-sync after pull/merge)"));
|
|
653
1807
|
console.log(ui.dim(" \u2713 CI workflow created (pr-stale-check + sync-on-merge)"));
|
|
1808
|
+
if (stacksToSeed.length > 0) {
|
|
1809
|
+
console.log(ui.dim(` \u2713 Stack memory packs pre-seeded (${stacksToSeed.join(", ")})`));
|
|
1810
|
+
}
|
|
654
1811
|
console.log();
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1812
|
+
if (!opts.bootstrap) {
|
|
1813
|
+
console.log(ui.bold("One remaining step (optional but recommended):"));
|
|
1814
|
+
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 fill project-context.md without AI"));
|
|
1815
|
+
console.log(" " + ui.dim("Or in your AI client: invoke the MCP prompt ") + ui.bold("bootstrap_project"));
|
|
1816
|
+
} else {
|
|
1817
|
+
console.log(ui.bold("Project context bootstrapped from local files."));
|
|
1818
|
+
console.log(ui.dim(" Review .ai/project-context.md and fill in the TODO sections."));
|
|
1819
|
+
console.log(ui.dim(" Or invoke the MCP prompt `bootstrap_project` for a richer AI-generated version."));
|
|
1820
|
+
}
|
|
1821
|
+
console.log();
|
|
1822
|
+
console.log(ui.dim(" Seed more memories instantly:"));
|
|
1823
|
+
console.log(ui.dim(" haive memory import-changelog \u2014 from CHANGELOG.md"));
|
|
1824
|
+
console.log(ui.dim(" haive memory import README.md \u2014 from README / docs"));
|
|
658
1825
|
} else {
|
|
659
1826
|
console.log(ui.bold("Next steps:"));
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1827
|
+
if (!opts.bootstrap) {
|
|
1828
|
+
console.log(ui.dim(" 1. Fill project context (pick one):"));
|
|
1829
|
+
console.log(" " + ui.bold("haive init --bootstrap") + ui.dim(" \u2190 instant, no AI needed"));
|
|
1830
|
+
console.log(" or invoke the MCP prompt " + ui.bold("bootstrap_project") + ui.dim(" in your AI client"));
|
|
1831
|
+
} else {
|
|
1832
|
+
console.log(ui.dim(" 1. Review .ai/project-context.md and fill in the TODO sections."));
|
|
1833
|
+
}
|
|
666
1834
|
console.log();
|
|
667
|
-
console.log(ui.dim("
|
|
1835
|
+
console.log(ui.dim(" 2. Start every AI session with:"));
|
|
668
1836
|
console.log(" " + ui.bold("get_briefing({ task: '\u2026what you are about to do\u2026' })"));
|
|
669
1837
|
console.log();
|
|
670
1838
|
console.log(ui.dim(" Tip: run `haive init` (without --manual) for zero-friction autopilot mode."));
|
|
671
1839
|
}
|
|
672
1840
|
});
|
|
673
1841
|
}
|
|
1842
|
+
async function resolveStacksToSeed(root, stackOpt) {
|
|
1843
|
+
if (!stackOpt) return [];
|
|
1844
|
+
if (stackOpt === "auto") {
|
|
1845
|
+
const pkgPath = path7.join(root, "package.json");
|
|
1846
|
+
if (!existsSync6(pkgPath)) return [];
|
|
1847
|
+
try {
|
|
1848
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
1849
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1850
|
+
return autoDetectStacks(allDeps);
|
|
1851
|
+
} catch {
|
|
1852
|
+
return [];
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return stackOpt.split(",").map((s) => s.trim().toLowerCase()).filter(isValidStack);
|
|
1856
|
+
}
|
|
674
1857
|
async function writeBridge(root, relPath) {
|
|
675
|
-
const target =
|
|
676
|
-
if (
|
|
1858
|
+
const target = path7.join(root, relPath);
|
|
1859
|
+
if (existsSync6(target)) {
|
|
677
1860
|
ui.info(`Bridge ${relPath} already exists \u2014 skipped`);
|
|
678
1861
|
return;
|
|
679
1862
|
}
|
|
680
|
-
await
|
|
681
|
-
await
|
|
1863
|
+
await mkdir3(path7.dirname(target), { recursive: true });
|
|
1864
|
+
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
682
1865
|
ui.success(`Created bridge ${relPath}`);
|
|
683
1866
|
}
|
|
684
1867
|
|
|
685
1868
|
// src/commands/install-hooks.ts
|
|
686
|
-
import { mkdir as
|
|
687
|
-
import { existsSync as
|
|
688
|
-
import
|
|
1869
|
+
import { mkdir as mkdir4, writeFile as writeFile4, chmod, readFile as readFile5 } from "fs/promises";
|
|
1870
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1871
|
+
import path8 from "path";
|
|
689
1872
|
import "commander";
|
|
690
1873
|
import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
691
1874
|
var HOOK_MARKER = "# hAIve auto-generated";
|
|
692
|
-
var
|
|
1875
|
+
var POST_MERGE_BODY = `#!/bin/sh
|
|
693
1876
|
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
694
1877
|
|
|
695
1878
|
# After a merge or pull, refresh memory anchors and auto-promote eligible
|
|
@@ -700,46 +1883,83 @@ elif [ -x ./node_modules/.bin/haive ]; then
|
|
|
700
1883
|
./node_modules/.bin/haive sync --quiet --since ORIG_HEAD || true
|
|
701
1884
|
fi
|
|
702
1885
|
`;
|
|
703
|
-
var
|
|
1886
|
+
var PRE_PUSH_BODY = `#!/bin/sh
|
|
1887
|
+
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
1888
|
+
|
|
1889
|
+
# Before pushing, run haive precommit to surface known anti-patterns and stale memories.
|
|
1890
|
+
# Exit 0 always \u2014 this is advisory only (set HAIVE_BLOCK=1 to make it blocking).
|
|
1891
|
+
HAIVE_BLOCK=\${HAIVE_BLOCK:-0}
|
|
1892
|
+
|
|
1893
|
+
_haive() {
|
|
1894
|
+
if command -v haive >/dev/null 2>&1; then haive "$@"
|
|
1895
|
+
elif [ -x ./node_modules/.bin/haive ]; then ./node_modules/.bin/haive "$@"
|
|
1896
|
+
else return 0
|
|
1897
|
+
fi
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
# Run pre-commit check on diff between local and remote
|
|
1901
|
+
LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
1902
|
+
REMOTE_SHA=$(git rev-parse --verify "@{u}" 2>/dev/null || echo "")
|
|
1903
|
+
if [ -n "$REMOTE_SHA" ]; then
|
|
1904
|
+
DIFF=$(git diff "$REMOTE_SHA"..HEAD 2>/dev/null || "")
|
|
1905
|
+
if [ -n "$DIFF" ]; then
|
|
1906
|
+
_haive precommit --quiet 2>/dev/null || true
|
|
1907
|
+
fi
|
|
1908
|
+
fi
|
|
1909
|
+
|
|
1910
|
+
# Remind agent to save session recap if env var is set
|
|
1911
|
+
if [ "$HAIVE_SESSION_REMINDER" = "1" ]; then
|
|
1912
|
+
echo "haive: session active \u2014 remember to call mem_session_end before closing." >&2
|
|
1913
|
+
fi
|
|
1914
|
+
|
|
1915
|
+
exit 0
|
|
1916
|
+
`;
|
|
1917
|
+
var HOOKS = [
|
|
1918
|
+
{ name: "post-merge", body: POST_MERGE_BODY },
|
|
1919
|
+
{ name: "post-rewrite", body: POST_MERGE_BODY },
|
|
1920
|
+
{ name: "pre-push", body: PRE_PUSH_BODY }
|
|
1921
|
+
];
|
|
704
1922
|
function registerInstallHooks(program2) {
|
|
705
1923
|
program2.command("install-hooks").description(
|
|
706
|
-
"Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs
|
|
1924
|
+
"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"
|
|
707
1925
|
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
708
1926
|
const root = findProjectRoot6(opts.dir);
|
|
709
|
-
const gitDir =
|
|
710
|
-
if (!
|
|
1927
|
+
const gitDir = path8.join(root, ".git");
|
|
1928
|
+
if (!existsSync7(gitDir)) {
|
|
711
1929
|
ui.error(`No .git directory at ${root}.`);
|
|
712
1930
|
process.exitCode = 1;
|
|
713
1931
|
return;
|
|
714
1932
|
}
|
|
715
|
-
const hooksDir =
|
|
716
|
-
await
|
|
1933
|
+
const hooksDir = path8.join(gitDir, "hooks");
|
|
1934
|
+
await mkdir4(hooksDir, { recursive: true });
|
|
717
1935
|
let installed = 0;
|
|
718
1936
|
let skipped = 0;
|
|
719
|
-
for (const name of HOOKS) {
|
|
720
|
-
const file =
|
|
721
|
-
if (
|
|
722
|
-
const existing = await
|
|
1937
|
+
for (const { name, body } of HOOKS) {
|
|
1938
|
+
const file = path8.join(hooksDir, name);
|
|
1939
|
+
if (existsSync7(file) && !opts.force) {
|
|
1940
|
+
const existing = await readFile5(file, "utf8");
|
|
723
1941
|
if (!existing.includes(HOOK_MARKER)) {
|
|
724
1942
|
ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
|
|
725
1943
|
skipped++;
|
|
726
1944
|
continue;
|
|
727
1945
|
}
|
|
728
1946
|
}
|
|
729
|
-
await
|
|
1947
|
+
await writeFile4(file, body, "utf8");
|
|
730
1948
|
await chmod(file, 493);
|
|
731
1949
|
installed++;
|
|
732
1950
|
}
|
|
733
1951
|
ui.success(`Installed ${installed} hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
|
|
734
|
-
ui.info("
|
|
1952
|
+
ui.info("post-merge: haive sync runs after every pull/merge.");
|
|
1953
|
+
ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
|
|
1954
|
+
ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
|
|
735
1955
|
});
|
|
736
1956
|
}
|
|
737
1957
|
|
|
738
1958
|
// src/commands/mcp.ts
|
|
739
1959
|
import { spawn } from "child_process";
|
|
740
|
-
import { existsSync as
|
|
1960
|
+
import { existsSync as existsSync8 } from "fs";
|
|
741
1961
|
import { createRequire } from "module";
|
|
742
|
-
import
|
|
1962
|
+
import path9 from "path";
|
|
743
1963
|
import { fileURLToPath } from "url";
|
|
744
1964
|
import "commander";
|
|
745
1965
|
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
@@ -777,26 +1997,26 @@ function registerMcp(program2) {
|
|
|
777
1997
|
function locateMcpBin() {
|
|
778
1998
|
try {
|
|
779
1999
|
const pkgPath = require2.resolve("@hiveai/mcp/package.json");
|
|
780
|
-
const pkgDir =
|
|
781
|
-
const candidate =
|
|
782
|
-
if (
|
|
2000
|
+
const pkgDir = path9.dirname(pkgPath);
|
|
2001
|
+
const candidate = path9.join(pkgDir, "dist", "index.js");
|
|
2002
|
+
if (existsSync8(candidate)) return candidate;
|
|
783
2003
|
} catch {
|
|
784
2004
|
}
|
|
785
|
-
const here =
|
|
786
|
-
const sibling =
|
|
787
|
-
if (
|
|
2005
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
2006
|
+
const sibling = path9.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
|
|
2007
|
+
if (existsSync8(sibling)) return sibling;
|
|
788
2008
|
return null;
|
|
789
2009
|
}
|
|
790
2010
|
|
|
791
2011
|
// src/commands/sync.ts
|
|
792
2012
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
793
|
-
import { readFile as
|
|
794
|
-
import { existsSync as
|
|
795
|
-
import
|
|
2013
|
+
import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
2014
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2015
|
+
import path10 from "path";
|
|
796
2016
|
import "commander";
|
|
797
2017
|
import {
|
|
798
2018
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
799
|
-
buildFrontmatter,
|
|
2019
|
+
buildFrontmatter as buildFrontmatter2,
|
|
800
2020
|
findProjectRoot as findProjectRoot8,
|
|
801
2021
|
getUsage,
|
|
802
2022
|
isAutoPromoteEligible,
|
|
@@ -808,7 +2028,7 @@ import {
|
|
|
808
2028
|
pullCrossRepoSources,
|
|
809
2029
|
resolveHaivePaths as resolveHaivePaths5,
|
|
810
2030
|
resolveManifestFiles,
|
|
811
|
-
serializeMemory,
|
|
2031
|
+
serializeMemory as serializeMemory2,
|
|
812
2032
|
trackDependencies,
|
|
813
2033
|
verifyAnchor,
|
|
814
2034
|
watchContracts
|
|
@@ -827,7 +2047,7 @@ function registerSync(program2) {
|
|
|
827
2047
|
).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) => {
|
|
828
2048
|
const root = findProjectRoot8(opts.dir);
|
|
829
2049
|
const paths = resolveHaivePaths5(root);
|
|
830
|
-
if (!
|
|
2050
|
+
if (!existsSync9(paths.memoriesDir)) {
|
|
831
2051
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
832
2052
|
process.exitCode = 1;
|
|
833
2053
|
return;
|
|
@@ -847,9 +2067,9 @@ function registerSync(program2) {
|
|
|
847
2067
|
for (const { memory: memory2, filePath } of memories) {
|
|
848
2068
|
if (memory2.frontmatter.type === "session_recap") {
|
|
849
2069
|
if (memory2.frontmatter.status === "stale") {
|
|
850
|
-
await
|
|
2070
|
+
await writeFile5(
|
|
851
2071
|
filePath,
|
|
852
|
-
|
|
2072
|
+
serializeMemory2({
|
|
853
2073
|
frontmatter: {
|
|
854
2074
|
...memory2.frontmatter,
|
|
855
2075
|
status: "validated",
|
|
@@ -870,9 +2090,9 @@ function registerSync(program2) {
|
|
|
870
2090
|
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
871
2091
|
if (result.stale) {
|
|
872
2092
|
if (memory2.frontmatter.status !== "stale") {
|
|
873
|
-
await
|
|
2093
|
+
await writeFile5(
|
|
874
2094
|
filePath,
|
|
875
|
-
|
|
2095
|
+
serializeMemory2({
|
|
876
2096
|
frontmatter: {
|
|
877
2097
|
...memory2.frontmatter,
|
|
878
2098
|
status: "stale",
|
|
@@ -886,9 +2106,9 @@ function registerSync(program2) {
|
|
|
886
2106
|
staleMarked++;
|
|
887
2107
|
}
|
|
888
2108
|
} else if (memory2.frontmatter.status === "stale") {
|
|
889
|
-
await
|
|
2109
|
+
await writeFile5(
|
|
890
2110
|
filePath,
|
|
891
|
-
|
|
2111
|
+
serializeMemory2({
|
|
892
2112
|
frontmatter: {
|
|
893
2113
|
...memory2.frontmatter,
|
|
894
2114
|
status: "validated",
|
|
@@ -914,9 +2134,9 @@ function registerSync(program2) {
|
|
|
914
2134
|
minReads: autoPromoteMinReads,
|
|
915
2135
|
maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
|
|
916
2136
|
})) {
|
|
917
|
-
await
|
|
2137
|
+
await writeFile5(
|
|
918
2138
|
filePath,
|
|
919
|
-
|
|
2139
|
+
serializeMemory2({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
920
2140
|
"utf8"
|
|
921
2141
|
);
|
|
922
2142
|
promoted++;
|
|
@@ -925,9 +2145,9 @@ function registerSync(program2) {
|
|
|
925
2145
|
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
926
2146
|
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
927
2147
|
if (ageHours >= autoApproveDelayHours) {
|
|
928
|
-
await
|
|
2148
|
+
await writeFile5(
|
|
929
2149
|
filePath,
|
|
930
|
-
|
|
2150
|
+
serializeMemory2({
|
|
931
2151
|
frontmatter: {
|
|
932
2152
|
...fm,
|
|
933
2153
|
status: "validated",
|
|
@@ -959,7 +2179,7 @@ function registerSync(program2) {
|
|
|
959
2179
|
);
|
|
960
2180
|
}
|
|
961
2181
|
if (opts.injectBridge) {
|
|
962
|
-
const bridgeFile = opts.bridgeFile ?
|
|
2182
|
+
const bridgeFile = opts.bridgeFile ? path10.resolve(opts.bridgeFile) : path10.join(root, "CLAUDE.md");
|
|
963
2183
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
964
2184
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
965
2185
|
}
|
|
@@ -1056,7 +2276,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1056
2276
|
**Prochaines \xE9tapes (si confirm\xE9) :**
|
|
1057
2277
|
- Consulter le CHANGELOG : \`haive memory import-changelog --from node_modules/<pkg>/CHANGELOG.md\`
|
|
1058
2278
|
- V\xE9rifier les m\xE9moires ancr\xE9es : \`haive memory verify\``;
|
|
1059
|
-
const fm =
|
|
2279
|
+
const fm = buildFrontmatter2({
|
|
1060
2280
|
type: "gotcha",
|
|
1061
2281
|
slug,
|
|
1062
2282
|
scope: "team",
|
|
@@ -1065,11 +2285,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1065
2285
|
paths: [result.file],
|
|
1066
2286
|
topic: `dep-bump-${slugParts}`
|
|
1067
2287
|
});
|
|
1068
|
-
const teamDir =
|
|
1069
|
-
await
|
|
1070
|
-
await
|
|
1071
|
-
|
|
1072
|
-
|
|
2288
|
+
const teamDir = path10.join(paths.memoriesDir, "team");
|
|
2289
|
+
await mkdir5(teamDir, { recursive: true });
|
|
2290
|
+
await writeFile5(
|
|
2291
|
+
path10.join(teamDir, `${fm.id}.md`),
|
|
2292
|
+
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1073
2293
|
"utf8"
|
|
1074
2294
|
);
|
|
1075
2295
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -1123,7 +2343,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1123
2343
|
**Prochaines \xE9tapes (si confirm\xE9) :**
|
|
1124
2344
|
- Rechercher les usages : \`haive memory for-files <fichiers concern\xE9s>\`
|
|
1125
2345
|
- V\xE9rifier les m\xE9moires li\xE9es : \`haive memory query ${diff.contract}\``;
|
|
1126
|
-
const fm =
|
|
2346
|
+
const fm = buildFrontmatter2({
|
|
1127
2347
|
type: "gotcha",
|
|
1128
2348
|
slug,
|
|
1129
2349
|
scope: "team",
|
|
@@ -1132,11 +2352,11 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1132
2352
|
paths: [diff.file],
|
|
1133
2353
|
topic: `contract-breaking-${diff.contract}`
|
|
1134
2354
|
});
|
|
1135
|
-
const teamDir =
|
|
1136
|
-
await
|
|
1137
|
-
await
|
|
1138
|
-
|
|
1139
|
-
|
|
2355
|
+
const teamDir = path10.join(paths.memoriesDir, "team");
|
|
2356
|
+
await mkdir5(teamDir, { recursive: true });
|
|
2357
|
+
await writeFile5(
|
|
2358
|
+
path10.join(teamDir, `${fm.id}.md`),
|
|
2359
|
+
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
1140
2360
|
"utf8"
|
|
1141
2361
|
);
|
|
1142
2362
|
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
@@ -1196,7 +2416,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
1196
2416
|
});
|
|
1197
2417
|
}
|
|
1198
2418
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
1199
|
-
if (!
|
|
2419
|
+
if (!existsSync9(memoriesDir)) return;
|
|
1200
2420
|
const all = await loadMemoriesFromDir2(memoriesDir);
|
|
1201
2421
|
const top = all.filter(({ memory: memory2 }) => {
|
|
1202
2422
|
const s = memory2.frontmatter.status;
|
|
@@ -1221,17 +2441,17 @@ ${m.memory.body.trim()}`;
|
|
|
1221
2441
|
` + block + `
|
|
1222
2442
|
|
|
1223
2443
|
${BRIDGE_END}`;
|
|
1224
|
-
const fileExists =
|
|
1225
|
-
let existing = fileExists ? await
|
|
2444
|
+
const fileExists = existsSync9(bridgeFile);
|
|
2445
|
+
let existing = fileExists ? await readFile6(bridgeFile, "utf8") : "";
|
|
1226
2446
|
existing = existing.replace(/\r\n/g, "\n");
|
|
1227
2447
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
1228
2448
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
1229
2449
|
if (startIdx !== -1 && endIdx === -1) {
|
|
1230
|
-
ui.warn(`${
|
|
2450
|
+
ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
1231
2451
|
return;
|
|
1232
2452
|
}
|
|
1233
2453
|
if (startIdx === -1 && endIdx !== -1) {
|
|
1234
|
-
ui.warn(`${
|
|
2454
|
+
ui.warn(`${path10.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
1235
2455
|
return;
|
|
1236
2456
|
}
|
|
1237
2457
|
let updated;
|
|
@@ -1239,14 +2459,14 @@ ${BRIDGE_END}`;
|
|
|
1239
2459
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
1240
2460
|
} else {
|
|
1241
2461
|
if (!fileExists && !quiet) {
|
|
1242
|
-
ui.info(`Creating ${
|
|
2462
|
+
ui.info(`Creating ${path10.relative(root, bridgeFile)} with haive memory block.`);
|
|
1243
2463
|
}
|
|
1244
2464
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
1245
2465
|
}
|
|
1246
|
-
await
|
|
2466
|
+
await writeFile5(bridgeFile, updated, "utf8");
|
|
1247
2467
|
if (!quiet) {
|
|
1248
2468
|
console.log(
|
|
1249
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
2469
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path10.relative(root, bridgeFile)}`)
|
|
1250
2470
|
);
|
|
1251
2471
|
}
|
|
1252
2472
|
}
|
|
@@ -1271,18 +2491,18 @@ function collectSinceChanges(root, ref) {
|
|
|
1271
2491
|
|
|
1272
2492
|
// src/commands/memory-add.ts
|
|
1273
2493
|
import { createHash } from "crypto";
|
|
1274
|
-
import { mkdir as
|
|
1275
|
-
import { existsSync as
|
|
1276
|
-
import
|
|
2494
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
|
|
2495
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2496
|
+
import path11 from "path";
|
|
1277
2497
|
import "commander";
|
|
1278
2498
|
import {
|
|
1279
|
-
buildFrontmatter as
|
|
2499
|
+
buildFrontmatter as buildFrontmatter3,
|
|
1280
2500
|
findProjectRoot as findProjectRoot9,
|
|
1281
2501
|
inferModulesFromPaths,
|
|
1282
2502
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
1283
|
-
memoryFilePath,
|
|
2503
|
+
memoryFilePath as memoryFilePath2,
|
|
1284
2504
|
resolveHaivePaths as resolveHaivePaths6,
|
|
1285
|
-
serializeMemory as
|
|
2505
|
+
serializeMemory as serializeMemory3
|
|
1286
2506
|
} from "@hiveai/core";
|
|
1287
2507
|
function registerMemoryAdd(memory2) {
|
|
1288
2508
|
memory2.command("add").description(
|
|
@@ -1311,7 +2531,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1311
2531
|
).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) => {
|
|
1312
2532
|
const root = findProjectRoot9(opts.dir);
|
|
1313
2533
|
const paths = resolveHaivePaths6(root);
|
|
1314
|
-
if (!
|
|
2534
|
+
if (!existsSync10(paths.haiveDir)) {
|
|
1315
2535
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
1316
2536
|
process.exitCode = 1;
|
|
1317
2537
|
return;
|
|
@@ -1322,7 +2542,7 @@ function registerMemoryAdd(memory2) {
|
|
|
1322
2542
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
|
|
1323
2543
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
1324
2544
|
if (anchorPaths.length > 0) {
|
|
1325
|
-
const missing = anchorPaths.filter((p) => !
|
|
2545
|
+
const missing = anchorPaths.filter((p) => !existsSync10(path11.resolve(root, p)));
|
|
1326
2546
|
if (missing.length > 0) {
|
|
1327
2547
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
1328
2548
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -1334,12 +2554,12 @@ function registerMemoryAdd(memory2) {
|
|
|
1334
2554
|
const title = opts.title ?? opts.slug;
|
|
1335
2555
|
let body;
|
|
1336
2556
|
if (opts.bodyFile !== void 0) {
|
|
1337
|
-
if (!
|
|
2557
|
+
if (!existsSync10(opts.bodyFile)) {
|
|
1338
2558
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
1339
2559
|
process.exitCode = 1;
|
|
1340
2560
|
return;
|
|
1341
2561
|
}
|
|
1342
|
-
const fileContent = await
|
|
2562
|
+
const fileContent = await readFile7(opts.bodyFile, "utf8");
|
|
1343
2563
|
body = opts.title ? `# ${opts.title}
|
|
1344
2564
|
|
|
1345
2565
|
${fileContent.trim()}
|
|
@@ -1355,7 +2575,7 @@ TODO \u2014 write the memory body.
|
|
|
1355
2575
|
`;
|
|
1356
2576
|
}
|
|
1357
2577
|
const scope = opts.scope ?? "personal";
|
|
1358
|
-
if (
|
|
2578
|
+
if (existsSync10(paths.memoriesDir)) {
|
|
1359
2579
|
const incomingHash = createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
1360
2580
|
const allForHash = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1361
2581
|
const hashDup = allForHash.find(
|
|
@@ -1368,7 +2588,7 @@ TODO \u2014 write the memory body.
|
|
|
1368
2588
|
return;
|
|
1369
2589
|
}
|
|
1370
2590
|
}
|
|
1371
|
-
if (opts.topic &&
|
|
2591
|
+
if (opts.topic && existsSync10(paths.memoriesDir)) {
|
|
1372
2592
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1373
2593
|
const topicMatch = existing.find(
|
|
1374
2594
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
@@ -1386,13 +2606,13 @@ TODO \u2014 write the memory body.
|
|
|
1386
2606
|
symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
|
|
1387
2607
|
}
|
|
1388
2608
|
};
|
|
1389
|
-
await
|
|
1390
|
-
ui.success(`Updated (topic upsert) ${
|
|
2609
|
+
await writeFile6(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2610
|
+
ui.success(`Updated (topic upsert) ${path11.relative(root, topicMatch.filePath)}`);
|
|
1391
2611
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
1392
2612
|
return;
|
|
1393
2613
|
}
|
|
1394
2614
|
}
|
|
1395
|
-
const frontmatter =
|
|
2615
|
+
const frontmatter = buildFrontmatter3({
|
|
1396
2616
|
type: opts.type,
|
|
1397
2617
|
slug: opts.slug,
|
|
1398
2618
|
scope,
|
|
@@ -1405,14 +2625,14 @@ TODO \u2014 write the memory body.
|
|
|
1405
2625
|
commit: opts.commit,
|
|
1406
2626
|
topic: opts.topic
|
|
1407
2627
|
});
|
|
1408
|
-
const file =
|
|
1409
|
-
await
|
|
1410
|
-
if (
|
|
2628
|
+
const file = memoryFilePath2(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2629
|
+
await mkdir6(path11.dirname(file), { recursive: true });
|
|
2630
|
+
if (existsSync10(file)) {
|
|
1411
2631
|
ui.error(`Memory already exists at ${file}`);
|
|
1412
2632
|
process.exitCode = 1;
|
|
1413
2633
|
return;
|
|
1414
2634
|
}
|
|
1415
|
-
if (
|
|
2635
|
+
if (existsSync10(paths.memoriesDir)) {
|
|
1416
2636
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
1417
2637
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
1418
2638
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
@@ -1424,8 +2644,8 @@ TODO \u2014 write the memory body.
|
|
|
1424
2644
|
ui.warn("Consider updating one of these with `haive memory update` instead.");
|
|
1425
2645
|
}
|
|
1426
2646
|
}
|
|
1427
|
-
await
|
|
1428
|
-
ui.success(`Created ${
|
|
2647
|
+
await writeFile6(file, serializeMemory3({ frontmatter, body }), "utf8");
|
|
2648
|
+
ui.success(`Created ${path11.relative(root, file)}`);
|
|
1429
2649
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
1430
2650
|
if (inferredTags.length > 0) {
|
|
1431
2651
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -1455,8 +2675,8 @@ function parseCsv2(value) {
|
|
|
1455
2675
|
}
|
|
1456
2676
|
|
|
1457
2677
|
// src/commands/memory-list.ts
|
|
1458
|
-
import { existsSync as
|
|
1459
|
-
import
|
|
2678
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2679
|
+
import path12 from "path";
|
|
1460
2680
|
import "commander";
|
|
1461
2681
|
import { findProjectRoot as findProjectRoot10, resolveHaivePaths as resolveHaivePaths7 } from "@hiveai/core";
|
|
1462
2682
|
|
|
@@ -1472,7 +2692,7 @@ function registerMemoryList(memory2) {
|
|
|
1472
2692
|
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) => {
|
|
1473
2693
|
const root = findProjectRoot10(opts.dir);
|
|
1474
2694
|
const paths = resolveHaivePaths7(root);
|
|
1475
|
-
if (!
|
|
2695
|
+
if (!existsSync11(paths.memoriesDir)) {
|
|
1476
2696
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
1477
2697
|
process.exitCode = 1;
|
|
1478
2698
|
return;
|
|
@@ -1504,7 +2724,7 @@ function registerMemoryList(memory2) {
|
|
|
1504
2724
|
console.log(
|
|
1505
2725
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
1506
2726
|
);
|
|
1507
|
-
console.log(` ${ui.dim(
|
|
2727
|
+
console.log(` ${ui.dim(path12.relative(root, filePath))}`);
|
|
1508
2728
|
}
|
|
1509
2729
|
console.log(ui.dim(`
|
|
1510
2730
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -1539,21 +2759,21 @@ function matchesFilters(loaded, opts) {
|
|
|
1539
2759
|
}
|
|
1540
2760
|
|
|
1541
2761
|
// src/commands/memory-promote.ts
|
|
1542
|
-
import { mkdir as
|
|
1543
|
-
import { existsSync as
|
|
1544
|
-
import
|
|
2762
|
+
import { mkdir as mkdir7, unlink, writeFile as writeFile7 } from "fs/promises";
|
|
2763
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2764
|
+
import path13 from "path";
|
|
1545
2765
|
import "commander";
|
|
1546
2766
|
import {
|
|
1547
2767
|
findProjectRoot as findProjectRoot11,
|
|
1548
|
-
memoryFilePath as
|
|
2768
|
+
memoryFilePath as memoryFilePath3,
|
|
1549
2769
|
resolveHaivePaths as resolveHaivePaths8,
|
|
1550
|
-
serializeMemory as
|
|
2770
|
+
serializeMemory as serializeMemory4
|
|
1551
2771
|
} from "@hiveai/core";
|
|
1552
2772
|
function registerMemoryPromote(memory2) {
|
|
1553
2773
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
1554
2774
|
const root = findProjectRoot11(opts.dir);
|
|
1555
2775
|
const paths = resolveHaivePaths8(root);
|
|
1556
|
-
if (!
|
|
2776
|
+
if (!existsSync12(paths.memoriesDir)) {
|
|
1557
2777
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
1558
2778
|
process.exitCode = 1;
|
|
1559
2779
|
return;
|
|
@@ -1587,31 +2807,31 @@ function registerMemoryPromote(memory2) {
|
|
|
1587
2807
|
},
|
|
1588
2808
|
body: found.memory.body
|
|
1589
2809
|
};
|
|
1590
|
-
const newPath =
|
|
1591
|
-
await
|
|
1592
|
-
await
|
|
2810
|
+
const newPath = memoryFilePath3(paths, "team", updated.frontmatter.id);
|
|
2811
|
+
await mkdir7(path13.dirname(newPath), { recursive: true });
|
|
2812
|
+
await writeFile7(newPath, serializeMemory4(updated), "utf8");
|
|
1593
2813
|
await unlink(found.filePath);
|
|
1594
2814
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
1595
|
-
ui.info(`Now at ${
|
|
2815
|
+
ui.info(`Now at ${path13.relative(root, newPath)}`);
|
|
1596
2816
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
1597
2817
|
});
|
|
1598
2818
|
}
|
|
1599
2819
|
|
|
1600
2820
|
// src/commands/memory-approve.ts
|
|
1601
|
-
import { existsSync as
|
|
1602
|
-
import { writeFile as
|
|
1603
|
-
import
|
|
2821
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2822
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2823
|
+
import path14 from "path";
|
|
1604
2824
|
import "commander";
|
|
1605
2825
|
import {
|
|
1606
2826
|
findProjectRoot as findProjectRoot12,
|
|
1607
2827
|
resolveHaivePaths as resolveHaivePaths9,
|
|
1608
|
-
serializeMemory as
|
|
2828
|
+
serializeMemory as serializeMemory5
|
|
1609
2829
|
} from "@hiveai/core";
|
|
1610
2830
|
function registerMemoryApprove(memory2) {
|
|
1611
2831
|
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) => {
|
|
1612
2832
|
const root = findProjectRoot12(opts.dir);
|
|
1613
2833
|
const paths = resolveHaivePaths9(root);
|
|
1614
|
-
if (!
|
|
2834
|
+
if (!existsSync13(paths.memoriesDir)) {
|
|
1615
2835
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1616
2836
|
process.exitCode = 1;
|
|
1617
2837
|
return;
|
|
@@ -1633,7 +2853,7 @@ function registerMemoryApprove(memory2) {
|
|
|
1633
2853
|
frontmatter: { ...found2.memory.frontmatter, status: "validated" },
|
|
1634
2854
|
body: found2.memory.body
|
|
1635
2855
|
};
|
|
1636
|
-
await
|
|
2856
|
+
await writeFile8(found2.filePath, serializeMemory5(next2), "utf8");
|
|
1637
2857
|
count++;
|
|
1638
2858
|
}
|
|
1639
2859
|
ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
|
|
@@ -1662,27 +2882,27 @@ function registerMemoryApprove(memory2) {
|
|
|
1662
2882
|
frontmatter: { ...found.memory.frontmatter, status: "validated" },
|
|
1663
2883
|
body: found.memory.body
|
|
1664
2884
|
};
|
|
1665
|
-
await
|
|
2885
|
+
await writeFile8(found.filePath, serializeMemory5(next), "utf8");
|
|
1666
2886
|
ui.success(`Approved ${id} (status=validated)`);
|
|
1667
|
-
ui.info(
|
|
2887
|
+
ui.info(path14.relative(root, found.filePath));
|
|
1668
2888
|
});
|
|
1669
2889
|
}
|
|
1670
2890
|
|
|
1671
2891
|
// src/commands/memory-update.ts
|
|
1672
|
-
import { writeFile as
|
|
1673
|
-
import { existsSync as
|
|
1674
|
-
import
|
|
2892
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2893
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2894
|
+
import path15 from "path";
|
|
1675
2895
|
import "commander";
|
|
1676
2896
|
import {
|
|
1677
2897
|
findProjectRoot as findProjectRoot13,
|
|
1678
2898
|
resolveHaivePaths as resolveHaivePaths10,
|
|
1679
|
-
serializeMemory as
|
|
2899
|
+
serializeMemory as serializeMemory6
|
|
1680
2900
|
} from "@hiveai/core";
|
|
1681
2901
|
function registerMemoryUpdate(memory2) {
|
|
1682
2902
|
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) => {
|
|
1683
2903
|
const root = findProjectRoot13(opts.dir);
|
|
1684
2904
|
const paths = resolveHaivePaths10(root);
|
|
1685
|
-
if (!
|
|
2905
|
+
if (!existsSync14(paths.memoriesDir)) {
|
|
1686
2906
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
1687
2907
|
process.exitCode = 1;
|
|
1688
2908
|
return;
|
|
@@ -1729,12 +2949,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
1729
2949
|
ui.warn("Nothing to update \u2014 provide at least one option.");
|
|
1730
2950
|
return;
|
|
1731
2951
|
}
|
|
1732
|
-
await
|
|
2952
|
+
await writeFile9(
|
|
1733
2953
|
loaded.filePath,
|
|
1734
|
-
|
|
2954
|
+
serializeMemory6({ frontmatter: newFrontmatter, body: newBody }),
|
|
1735
2955
|
"utf8"
|
|
1736
2956
|
);
|
|
1737
|
-
ui.success(`Updated ${
|
|
2957
|
+
ui.success(`Updated ${path15.relative(root, loaded.filePath)}`);
|
|
1738
2958
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
1739
2959
|
});
|
|
1740
2960
|
}
|
|
@@ -1753,9 +2973,9 @@ function parseCsv3(value) {
|
|
|
1753
2973
|
}
|
|
1754
2974
|
|
|
1755
2975
|
// src/commands/memory-auto-promote.ts
|
|
1756
|
-
import { writeFile as
|
|
1757
|
-
import { existsSync as
|
|
1758
|
-
import
|
|
2976
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
2977
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2978
|
+
import path16 from "path";
|
|
1759
2979
|
import "commander";
|
|
1760
2980
|
import {
|
|
1761
2981
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
@@ -1764,7 +2984,7 @@ import {
|
|
|
1764
2984
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
1765
2985
|
loadUsageIndex as loadUsageIndex2,
|
|
1766
2986
|
resolveHaivePaths as resolveHaivePaths11,
|
|
1767
|
-
serializeMemory as
|
|
2987
|
+
serializeMemory as serializeMemory7
|
|
1768
2988
|
} from "@hiveai/core";
|
|
1769
2989
|
function registerMemoryAutoPromote(memory2) {
|
|
1770
2990
|
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(
|
|
@@ -1774,7 +2994,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1774
2994
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
1775
2995
|
const root = findProjectRoot14(opts.dir);
|
|
1776
2996
|
const paths = resolveHaivePaths11(root);
|
|
1777
|
-
if (!
|
|
2997
|
+
if (!existsSync15(paths.memoriesDir)) {
|
|
1778
2998
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1779
2999
|
process.exitCode = 1;
|
|
1780
3000
|
return;
|
|
@@ -1800,13 +3020,13 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1800
3020
|
console.log(
|
|
1801
3021
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
1802
3022
|
);
|
|
1803
|
-
console.log(` ${ui.dim(
|
|
3023
|
+
console.log(` ${ui.dim(path16.relative(root, filePath))}`);
|
|
1804
3024
|
if (opts.apply) {
|
|
1805
3025
|
const next = {
|
|
1806
3026
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
1807
3027
|
body: mem.body
|
|
1808
3028
|
};
|
|
1809
|
-
await
|
|
3029
|
+
await writeFile10(filePath, serializeMemory7(next), "utf8");
|
|
1810
3030
|
written++;
|
|
1811
3031
|
}
|
|
1812
3032
|
}
|
|
@@ -1817,9 +3037,9 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
1817
3037
|
|
|
1818
3038
|
// src/commands/memory-edit.ts
|
|
1819
3039
|
import { spawn as spawn2 } from "child_process";
|
|
1820
|
-
import { existsSync as
|
|
1821
|
-
import { readFile as
|
|
1822
|
-
import
|
|
3040
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3041
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3042
|
+
import path17 from "path";
|
|
1823
3043
|
import "commander";
|
|
1824
3044
|
import {
|
|
1825
3045
|
findProjectRoot as findProjectRoot15,
|
|
@@ -1830,7 +3050,7 @@ function registerMemoryEdit(memory2) {
|
|
|
1830
3050
|
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) => {
|
|
1831
3051
|
const root = findProjectRoot15(opts.dir);
|
|
1832
3052
|
const paths = resolveHaivePaths12(root);
|
|
1833
|
-
if (!
|
|
3053
|
+
if (!existsSync16(paths.memoriesDir)) {
|
|
1834
3054
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1835
3055
|
process.exitCode = 1;
|
|
1836
3056
|
return;
|
|
@@ -1843,13 +3063,13 @@ function registerMemoryEdit(memory2) {
|
|
|
1843
3063
|
return;
|
|
1844
3064
|
}
|
|
1845
3065
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
1846
|
-
ui.info(`Opening ${
|
|
3066
|
+
ui.info(`Opening ${path17.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
1847
3067
|
const code = await runEditor(editor, found.filePath);
|
|
1848
3068
|
if (code !== 0) {
|
|
1849
3069
|
ui.warn(`Editor exited with status ${code}.`);
|
|
1850
3070
|
}
|
|
1851
3071
|
try {
|
|
1852
|
-
const fresh = await
|
|
3072
|
+
const fresh = await readFile8(found.filePath, "utf8");
|
|
1853
3073
|
parseMemory(fresh);
|
|
1854
3074
|
ui.success("Memory still parses cleanly.");
|
|
1855
3075
|
} catch (err) {
|
|
@@ -1870,8 +3090,8 @@ function runEditor(editor, file) {
|
|
|
1870
3090
|
}
|
|
1871
3091
|
|
|
1872
3092
|
// src/commands/memory-for-files.ts
|
|
1873
|
-
import { existsSync as
|
|
1874
|
-
import
|
|
3093
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3094
|
+
import path18 from "path";
|
|
1875
3095
|
import "commander";
|
|
1876
3096
|
import {
|
|
1877
3097
|
deriveConfidence,
|
|
@@ -1886,7 +3106,7 @@ function registerMemoryForFiles(memory2) {
|
|
|
1886
3106
|
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) => {
|
|
1887
3107
|
const root = findProjectRoot16(opts.dir);
|
|
1888
3108
|
const paths = resolveHaivePaths13(root);
|
|
1889
|
-
if (!
|
|
3109
|
+
if (!existsSync17(paths.memoriesDir)) {
|
|
1890
3110
|
ui.error(`No .ai/memories at ${root}.`);
|
|
1891
3111
|
process.exitCode = 1;
|
|
1892
3112
|
return;
|
|
@@ -1993,13 +3213,13 @@ function printGroup(root, label, loaded, usage) {
|
|
|
1993
3213
|
const u = getUsage3(usage, fm.id);
|
|
1994
3214
|
const conf = deriveConfidence(fm, u);
|
|
1995
3215
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
1996
|
-
console.log(` ${ui.dim(
|
|
3216
|
+
console.log(` ${ui.dim(path18.relative(root, filePath))}`);
|
|
1997
3217
|
}
|
|
1998
3218
|
}
|
|
1999
3219
|
|
|
2000
3220
|
// src/commands/memory-hot.ts
|
|
2001
|
-
import { existsSync as
|
|
2002
|
-
import
|
|
3221
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3222
|
+
import path19 from "path";
|
|
2003
3223
|
import "commander";
|
|
2004
3224
|
import {
|
|
2005
3225
|
findProjectRoot as findProjectRoot17,
|
|
@@ -2011,7 +3231,7 @@ function registerMemoryHot(memory2) {
|
|
|
2011
3231
|
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) => {
|
|
2012
3232
|
const root = findProjectRoot17(opts.dir);
|
|
2013
3233
|
const paths = resolveHaivePaths14(root);
|
|
2014
|
-
if (!
|
|
3234
|
+
if (!existsSync18(paths.memoriesDir)) {
|
|
2015
3235
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2016
3236
|
process.exitCode = 1;
|
|
2017
3237
|
return;
|
|
@@ -2039,7 +3259,7 @@ function registerMemoryHot(memory2) {
|
|
|
2039
3259
|
console.log(
|
|
2040
3260
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2041
3261
|
);
|
|
2042
|
-
console.log(` ${ui.dim(
|
|
3262
|
+
console.log(` ${ui.dim(path19.relative(root, filePath))}`);
|
|
2043
3263
|
}
|
|
2044
3264
|
ui.info(
|
|
2045
3265
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -2048,16 +3268,16 @@ function registerMemoryHot(memory2) {
|
|
|
2048
3268
|
}
|
|
2049
3269
|
|
|
2050
3270
|
// src/commands/memory-tried.ts
|
|
2051
|
-
import { mkdir as
|
|
2052
|
-
import { existsSync as
|
|
2053
|
-
import
|
|
3271
|
+
import { mkdir as mkdir8, writeFile as writeFile11 } from "fs/promises";
|
|
3272
|
+
import { existsSync as existsSync19 } from "fs";
|
|
3273
|
+
import path20 from "path";
|
|
2054
3274
|
import "commander";
|
|
2055
3275
|
import {
|
|
2056
|
-
buildFrontmatter as
|
|
3276
|
+
buildFrontmatter as buildFrontmatter4,
|
|
2057
3277
|
findProjectRoot as findProjectRoot18,
|
|
2058
|
-
memoryFilePath as
|
|
3278
|
+
memoryFilePath as memoryFilePath4,
|
|
2059
3279
|
resolveHaivePaths as resolveHaivePaths15,
|
|
2060
|
-
serializeMemory as
|
|
3280
|
+
serializeMemory as serializeMemory8
|
|
2061
3281
|
} from "@hiveai/core";
|
|
2062
3282
|
function registerMemoryTried(memory2) {
|
|
2063
3283
|
memory2.command("tried").description(
|
|
@@ -2078,13 +3298,13 @@ function registerMemoryTried(memory2) {
|
|
|
2078
3298
|
).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) => {
|
|
2079
3299
|
const root = findProjectRoot18(opts.dir);
|
|
2080
3300
|
const paths = resolveHaivePaths15(root);
|
|
2081
|
-
if (!
|
|
3301
|
+
if (!existsSync19(paths.haiveDir)) {
|
|
2082
3302
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2083
3303
|
process.exitCode = 1;
|
|
2084
3304
|
return;
|
|
2085
3305
|
}
|
|
2086
3306
|
const slug = opts.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
|
|
2087
|
-
const baseFm =
|
|
3307
|
+
const baseFm = buildFrontmatter4({
|
|
2088
3308
|
type: "attempt",
|
|
2089
3309
|
slug,
|
|
2090
3310
|
scope: opts.scope,
|
|
@@ -2100,15 +3320,15 @@ function registerMemoryTried(memory2) {
|
|
|
2100
3320
|
lines.push("", `**Instead, use:** ${opts.instead}`);
|
|
2101
3321
|
}
|
|
2102
3322
|
const body = lines.join("\n") + "\n";
|
|
2103
|
-
const file =
|
|
2104
|
-
await
|
|
2105
|
-
if (
|
|
3323
|
+
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
3324
|
+
await mkdir8(path20.dirname(file), { recursive: true });
|
|
3325
|
+
if (existsSync19(file)) {
|
|
2106
3326
|
ui.error(`Memory already exists at ${file}`);
|
|
2107
3327
|
process.exitCode = 1;
|
|
2108
3328
|
return;
|
|
2109
3329
|
}
|
|
2110
|
-
await
|
|
2111
|
-
ui.success(`Recorded: ${
|
|
3330
|
+
await writeFile11(file, serializeMemory8({ frontmatter, body }), "utf8");
|
|
3331
|
+
ui.success(`Recorded: ${path20.relative(root, file)}`);
|
|
2112
3332
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
2113
3333
|
});
|
|
2114
3334
|
}
|
|
@@ -2118,8 +3338,8 @@ function parseCsv4(value) {
|
|
|
2118
3338
|
}
|
|
2119
3339
|
|
|
2120
3340
|
// src/commands/memory-pending.ts
|
|
2121
|
-
import { existsSync as
|
|
2122
|
-
import
|
|
3341
|
+
import { existsSync as existsSync20 } from "fs";
|
|
3342
|
+
import path21 from "path";
|
|
2123
3343
|
import "commander";
|
|
2124
3344
|
import {
|
|
2125
3345
|
findProjectRoot as findProjectRoot19,
|
|
@@ -2131,7 +3351,7 @@ function registerMemoryPending(memory2) {
|
|
|
2131
3351
|
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) => {
|
|
2132
3352
|
const root = findProjectRoot19(opts.dir);
|
|
2133
3353
|
const paths = resolveHaivePaths16(root);
|
|
2134
|
-
if (!
|
|
3354
|
+
if (!existsSync20(paths.memoriesDir)) {
|
|
2135
3355
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2136
3356
|
process.exitCode = 1;
|
|
2137
3357
|
return;
|
|
@@ -2159,15 +3379,15 @@ function registerMemoryPending(memory2) {
|
|
|
2159
3379
|
console.log(
|
|
2160
3380
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
2161
3381
|
);
|
|
2162
|
-
console.log(` ${ui.dim(
|
|
3382
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
2163
3383
|
}
|
|
2164
3384
|
ui.info(`${proposed.length} pending`);
|
|
2165
3385
|
});
|
|
2166
3386
|
}
|
|
2167
3387
|
|
|
2168
3388
|
// src/commands/memory-query.ts
|
|
2169
|
-
import { existsSync as
|
|
2170
|
-
import
|
|
3389
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3390
|
+
import path22 from "path";
|
|
2171
3391
|
import "commander";
|
|
2172
3392
|
import {
|
|
2173
3393
|
extractSnippet,
|
|
@@ -2183,7 +3403,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2183
3403
|
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) => {
|
|
2184
3404
|
const root = findProjectRoot20(opts.dir);
|
|
2185
3405
|
const paths = resolveHaivePaths17(root);
|
|
2186
|
-
if (!
|
|
3406
|
+
if (!existsSync21(paths.memoriesDir)) {
|
|
2187
3407
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
2188
3408
|
process.exitCode = 1;
|
|
2189
3409
|
return;
|
|
@@ -2224,7 +3444,7 @@ function registerMemoryQuery(memory2) {
|
|
|
2224
3444
|
const fm = mem.frontmatter;
|
|
2225
3445
|
const statusBadge = ui.statusBadge(fm.status);
|
|
2226
3446
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
2227
|
-
console.log(` ${ui.dim(
|
|
3447
|
+
console.log(` ${ui.dim(path22.relative(root, filePath))}`);
|
|
2228
3448
|
const snippet = extractSnippet(mem.body, snippetNeedle);
|
|
2229
3449
|
if (snippet) console.log(` ${snippet}`);
|
|
2230
3450
|
}
|
|
@@ -2241,8 +3461,8 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
2241
3461
|
}
|
|
2242
3462
|
|
|
2243
3463
|
// src/commands/memory-reject.ts
|
|
2244
|
-
import { writeFile as
|
|
2245
|
-
import { existsSync as
|
|
3464
|
+
import { writeFile as writeFile12 } from "fs/promises";
|
|
3465
|
+
import { existsSync as existsSync22 } from "fs";
|
|
2246
3466
|
import "commander";
|
|
2247
3467
|
import {
|
|
2248
3468
|
findProjectRoot as findProjectRoot21,
|
|
@@ -2250,13 +3470,13 @@ import {
|
|
|
2250
3470
|
recordRejection,
|
|
2251
3471
|
resolveHaivePaths as resolveHaivePaths18,
|
|
2252
3472
|
saveUsageIndex,
|
|
2253
|
-
serializeMemory as
|
|
3473
|
+
serializeMemory as serializeMemory9
|
|
2254
3474
|
} from "@hiveai/core";
|
|
2255
3475
|
function registerMemoryReject(memory2) {
|
|
2256
3476
|
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) => {
|
|
2257
3477
|
const root = findProjectRoot21(opts.dir);
|
|
2258
3478
|
const paths = resolveHaivePaths18(root);
|
|
2259
|
-
if (!
|
|
3479
|
+
if (!existsSync22(paths.memoriesDir)) {
|
|
2260
3480
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2261
3481
|
process.exitCode = 1;
|
|
2262
3482
|
return;
|
|
@@ -2268,9 +3488,9 @@ function registerMemoryReject(memory2) {
|
|
|
2268
3488
|
process.exitCode = 1;
|
|
2269
3489
|
return;
|
|
2270
3490
|
}
|
|
2271
|
-
await
|
|
3491
|
+
await writeFile12(
|
|
2272
3492
|
loaded.filePath,
|
|
2273
|
-
|
|
3493
|
+
serializeMemory9({
|
|
2274
3494
|
frontmatter: {
|
|
2275
3495
|
...loaded.memory.frontmatter,
|
|
2276
3496
|
status: "rejected",
|
|
@@ -2292,9 +3512,9 @@ function registerMemoryReject(memory2) {
|
|
|
2292
3512
|
}
|
|
2293
3513
|
|
|
2294
3514
|
// src/commands/memory-rm.ts
|
|
2295
|
-
import { existsSync as
|
|
3515
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2296
3516
|
import { unlink as unlink2 } from "fs/promises";
|
|
2297
|
-
import
|
|
3517
|
+
import path23 from "path";
|
|
2298
3518
|
import { createInterface } from "readline/promises";
|
|
2299
3519
|
import "commander";
|
|
2300
3520
|
import {
|
|
@@ -2307,7 +3527,7 @@ function registerMemoryRm(memory2) {
|
|
|
2307
3527
|
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) => {
|
|
2308
3528
|
const root = findProjectRoot22(opts.dir);
|
|
2309
3529
|
const paths = resolveHaivePaths19(root);
|
|
2310
|
-
if (!
|
|
3530
|
+
if (!existsSync23(paths.memoriesDir)) {
|
|
2311
3531
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2312
3532
|
process.exitCode = 1;
|
|
2313
3533
|
return;
|
|
@@ -2319,7 +3539,7 @@ function registerMemoryRm(memory2) {
|
|
|
2319
3539
|
process.exitCode = 1;
|
|
2320
3540
|
return;
|
|
2321
3541
|
}
|
|
2322
|
-
const rel =
|
|
3542
|
+
const rel = path23.relative(root, found.filePath);
|
|
2323
3543
|
if (!opts.yes) {
|
|
2324
3544
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2325
3545
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -2343,9 +3563,9 @@ function registerMemoryRm(memory2) {
|
|
|
2343
3563
|
}
|
|
2344
3564
|
|
|
2345
3565
|
// src/commands/memory-show.ts
|
|
2346
|
-
import { existsSync as
|
|
2347
|
-
import { readFile as
|
|
2348
|
-
import
|
|
3566
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3567
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3568
|
+
import path24 from "path";
|
|
2349
3569
|
import "commander";
|
|
2350
3570
|
import {
|
|
2351
3571
|
deriveConfidence as deriveConfidence2,
|
|
@@ -2358,7 +3578,7 @@ function registerMemoryShow(memory2) {
|
|
|
2358
3578
|
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) => {
|
|
2359
3579
|
const root = findProjectRoot23(opts.dir);
|
|
2360
3580
|
const paths = resolveHaivePaths20(root);
|
|
2361
|
-
if (!
|
|
3581
|
+
if (!existsSync24(paths.memoriesDir)) {
|
|
2362
3582
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2363
3583
|
process.exitCode = 1;
|
|
2364
3584
|
return;
|
|
@@ -2371,7 +3591,7 @@ function registerMemoryShow(memory2) {
|
|
|
2371
3591
|
return;
|
|
2372
3592
|
}
|
|
2373
3593
|
if (opts.raw) {
|
|
2374
|
-
console.log(await
|
|
3594
|
+
console.log(await readFile9(found.filePath, "utf8"));
|
|
2375
3595
|
return;
|
|
2376
3596
|
}
|
|
2377
3597
|
const fm = found.memory.frontmatter;
|
|
@@ -2387,7 +3607,7 @@ function registerMemoryShow(memory2) {
|
|
|
2387
3607
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
2388
3608
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
2389
3609
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
2390
|
-
console.log(`${ui.dim("file:")} ${
|
|
3610
|
+
console.log(`${ui.dim("file:")} ${path24.relative(root, found.filePath)}`);
|
|
2391
3611
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
2392
3612
|
console.log(ui.dim("anchor:"));
|
|
2393
3613
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -2402,8 +3622,8 @@ function registerMemoryShow(memory2) {
|
|
|
2402
3622
|
}
|
|
2403
3623
|
|
|
2404
3624
|
// src/commands/memory-stats.ts
|
|
2405
|
-
import { existsSync as
|
|
2406
|
-
import
|
|
3625
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3626
|
+
import path25 from "path";
|
|
2407
3627
|
import "commander";
|
|
2408
3628
|
import {
|
|
2409
3629
|
deriveConfidence as deriveConfidence3,
|
|
@@ -2416,7 +3636,7 @@ function registerMemoryStats(memory2) {
|
|
|
2416
3636
|
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) => {
|
|
2417
3637
|
const root = findProjectRoot24(opts.dir);
|
|
2418
3638
|
const paths = resolveHaivePaths21(root);
|
|
2419
|
-
if (!
|
|
3639
|
+
if (!existsSync25(paths.memoriesDir)) {
|
|
2420
3640
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2421
3641
|
process.exitCode = 1;
|
|
2422
3642
|
return;
|
|
@@ -2441,20 +3661,20 @@ function registerMemoryStats(memory2) {
|
|
|
2441
3661
|
console.log(
|
|
2442
3662
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
2443
3663
|
);
|
|
2444
|
-
console.log(` ${ui.dim(
|
|
3664
|
+
console.log(` ${ui.dim(path25.relative(root, filePath))}`);
|
|
2445
3665
|
}
|
|
2446
3666
|
});
|
|
2447
3667
|
}
|
|
2448
3668
|
|
|
2449
3669
|
// src/commands/memory-verify.ts
|
|
2450
|
-
import { writeFile as
|
|
2451
|
-
import { existsSync as
|
|
2452
|
-
import
|
|
3670
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
3671
|
+
import { existsSync as existsSync26 } from "fs";
|
|
3672
|
+
import path26 from "path";
|
|
2453
3673
|
import "commander";
|
|
2454
3674
|
import {
|
|
2455
3675
|
findProjectRoot as findProjectRoot25,
|
|
2456
3676
|
resolveHaivePaths as resolveHaivePaths22,
|
|
2457
|
-
serializeMemory as
|
|
3677
|
+
serializeMemory as serializeMemory10,
|
|
2458
3678
|
verifyAnchor as verifyAnchor2
|
|
2459
3679
|
} from "@hiveai/core";
|
|
2460
3680
|
function registerMemoryVerify(memory2) {
|
|
@@ -2463,7 +3683,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2463
3683
|
).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) => {
|
|
2464
3684
|
const root = findProjectRoot25(opts.dir);
|
|
2465
3685
|
const paths = resolveHaivePaths22(root);
|
|
2466
|
-
if (!
|
|
3686
|
+
if (!existsSync26(paths.memoriesDir)) {
|
|
2467
3687
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2468
3688
|
process.exitCode = 1;
|
|
2469
3689
|
return;
|
|
@@ -2486,7 +3706,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2486
3706
|
anchorlessIds.push(mem.frontmatter.id);
|
|
2487
3707
|
continue;
|
|
2488
3708
|
}
|
|
2489
|
-
const rel =
|
|
3709
|
+
const rel = path26.relative(root, filePath);
|
|
2490
3710
|
if (result.stale) {
|
|
2491
3711
|
staleCount++;
|
|
2492
3712
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -2501,7 +3721,7 @@ function registerMemoryVerify(memory2) {
|
|
|
2501
3721
|
}
|
|
2502
3722
|
if (opts.update) {
|
|
2503
3723
|
const next = applyVerification(mem, result);
|
|
2504
|
-
await
|
|
3724
|
+
await writeFile13(filePath, serializeMemory10(next), "utf8");
|
|
2505
3725
|
updated++;
|
|
2506
3726
|
}
|
|
2507
3727
|
}
|
|
@@ -2549,8 +3769,8 @@ function applyVerification(mem, result) {
|
|
|
2549
3769
|
}
|
|
2550
3770
|
|
|
2551
3771
|
// src/commands/memory-import.ts
|
|
2552
|
-
import { readFile as
|
|
2553
|
-
import { existsSync as
|
|
3772
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3773
|
+
import { existsSync as existsSync27 } from "fs";
|
|
2554
3774
|
import "commander";
|
|
2555
3775
|
import {
|
|
2556
3776
|
findProjectRoot as findProjectRoot26,
|
|
@@ -2562,17 +3782,17 @@ function registerMemoryImport(memory2) {
|
|
|
2562
3782
|
).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) => {
|
|
2563
3783
|
const root = findProjectRoot26(opts.dir);
|
|
2564
3784
|
const paths = resolveHaivePaths23(root);
|
|
2565
|
-
if (!
|
|
3785
|
+
if (!existsSync27(paths.haiveDir)) {
|
|
2566
3786
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2567
3787
|
process.exitCode = 1;
|
|
2568
3788
|
return;
|
|
2569
3789
|
}
|
|
2570
|
-
if (!
|
|
3790
|
+
if (!existsSync27(opts.from)) {
|
|
2571
3791
|
ui.error(`File not found: ${opts.from}`);
|
|
2572
3792
|
process.exitCode = 1;
|
|
2573
3793
|
return;
|
|
2574
3794
|
}
|
|
2575
|
-
const content = await
|
|
3795
|
+
const content = await readFile10(opts.from, "utf8");
|
|
2576
3796
|
const scope = opts.scope ?? "team";
|
|
2577
3797
|
ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
|
|
2578
3798
|
ui.info(`Content length: ${content.length} chars`);
|
|
@@ -2600,15 +3820,15 @@ function registerMemoryImport(memory2) {
|
|
|
2600
3820
|
}
|
|
2601
3821
|
|
|
2602
3822
|
// src/commands/memory-import-changelog.ts
|
|
2603
|
-
import { existsSync as
|
|
2604
|
-
import { readFile as
|
|
2605
|
-
import
|
|
3823
|
+
import { existsSync as existsSync28 } from "fs";
|
|
3824
|
+
import { readFile as readFile11, mkdir as mkdir9, writeFile as writeFile14 } from "fs/promises";
|
|
3825
|
+
import path27 from "path";
|
|
2606
3826
|
import "commander";
|
|
2607
3827
|
import {
|
|
2608
|
-
buildFrontmatter as
|
|
3828
|
+
buildFrontmatter as buildFrontmatter5,
|
|
2609
3829
|
findProjectRoot as findProjectRoot27,
|
|
2610
3830
|
resolveHaivePaths as resolveHaivePaths24,
|
|
2611
|
-
serializeMemory as
|
|
3831
|
+
serializeMemory as serializeMemory11
|
|
2612
3832
|
} from "@hiveai/core";
|
|
2613
3833
|
function parseChangelog(content) {
|
|
2614
3834
|
const entries = [];
|
|
@@ -2673,13 +3893,13 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2673
3893
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2674
3894
|
const root = findProjectRoot27(opts.dir);
|
|
2675
3895
|
const paths = resolveHaivePaths24(root);
|
|
2676
|
-
const changelogPath =
|
|
2677
|
-
if (!
|
|
3896
|
+
const changelogPath = path27.resolve(root, opts.fromChangelog);
|
|
3897
|
+
if (!existsSync28(changelogPath)) {
|
|
2678
3898
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
2679
3899
|
process.exitCode = 1;
|
|
2680
3900
|
return;
|
|
2681
3901
|
}
|
|
2682
|
-
const content = await
|
|
3902
|
+
const content = await readFile11(changelogPath, "utf8");
|
|
2683
3903
|
let entries = parseChangelog(content);
|
|
2684
3904
|
if (entries.length === 0) {
|
|
2685
3905
|
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
@@ -2693,10 +3913,10 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2693
3913
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
2694
3914
|
}
|
|
2695
3915
|
}
|
|
2696
|
-
const pkgName = opts.package ??
|
|
3916
|
+
const pkgName = opts.package ?? path27.basename(path27.dirname(changelogPath));
|
|
2697
3917
|
const scope = opts.scope ?? "team";
|
|
2698
|
-
const teamDir =
|
|
2699
|
-
await
|
|
3918
|
+
const teamDir = path27.join(paths.memoriesDir, scope);
|
|
3919
|
+
await mkdir9(teamDir, { recursive: true });
|
|
2700
3920
|
let saved = 0;
|
|
2701
3921
|
for (const entry of entries) {
|
|
2702
3922
|
const lines = [];
|
|
@@ -2718,11 +3938,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2718
3938
|
lines.push("");
|
|
2719
3939
|
}
|
|
2720
3940
|
lines.push(
|
|
2721
|
-
`**Source:** \`${
|
|
3941
|
+
`**Source:** \`${path27.relative(root, changelogPath)}\`
|
|
2722
3942
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
2723
3943
|
);
|
|
2724
3944
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
2725
|
-
const fm =
|
|
3945
|
+
const fm = buildFrontmatter5({
|
|
2726
3946
|
type: "gotcha",
|
|
2727
3947
|
slug,
|
|
2728
3948
|
scope,
|
|
@@ -2733,12 +3953,12 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
2733
3953
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
2734
3954
|
`v${entry.version}`
|
|
2735
3955
|
],
|
|
2736
|
-
paths: [
|
|
3956
|
+
paths: [path27.relative(root, changelogPath)],
|
|
2737
3957
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
2738
3958
|
});
|
|
2739
|
-
await
|
|
2740
|
-
|
|
2741
|
-
|
|
3959
|
+
await writeFile14(
|
|
3960
|
+
path27.join(teamDir, `${fm.id}.md`),
|
|
3961
|
+
serializeMemory11({ frontmatter: fm, body: lines.join("\n") }),
|
|
2742
3962
|
"utf8"
|
|
2743
3963
|
);
|
|
2744
3964
|
console.log(ui.green(` \u2713 ${fm.id}`));
|
|
@@ -2760,9 +3980,9 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
2760
3980
|
}
|
|
2761
3981
|
|
|
2762
3982
|
// src/commands/memory-digest.ts
|
|
2763
|
-
import { existsSync as
|
|
2764
|
-
import { writeFile as
|
|
2765
|
-
import
|
|
3983
|
+
import { existsSync as existsSync29 } from "fs";
|
|
3984
|
+
import { writeFile as writeFile15 } from "fs/promises";
|
|
3985
|
+
import path28 from "path";
|
|
2766
3986
|
import "commander";
|
|
2767
3987
|
import {
|
|
2768
3988
|
deriveConfidence as deriveConfidence4,
|
|
@@ -2785,7 +4005,7 @@ function registerMemoryDigest(program2) {
|
|
|
2785
4005
|
).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) => {
|
|
2786
4006
|
const root = findProjectRoot28(opts.dir);
|
|
2787
4007
|
const paths = resolveHaivePaths25(root);
|
|
2788
|
-
if (!
|
|
4008
|
+
if (!existsSync29(paths.memoriesDir)) {
|
|
2789
4009
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
2790
4010
|
process.exitCode = 1;
|
|
2791
4011
|
return;
|
|
@@ -2857,8 +4077,8 @@ function registerMemoryDigest(program2) {
|
|
|
2857
4077
|
);
|
|
2858
4078
|
const digest = lines.join("\n");
|
|
2859
4079
|
if (opts.out) {
|
|
2860
|
-
const outPath =
|
|
2861
|
-
await
|
|
4080
|
+
const outPath = path28.resolve(process.cwd(), opts.out);
|
|
4081
|
+
await writeFile15(outPath, digest, "utf8");
|
|
2862
4082
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
2863
4083
|
} else {
|
|
2864
4084
|
console.log(digest);
|
|
@@ -2867,17 +4087,17 @@ function registerMemoryDigest(program2) {
|
|
|
2867
4087
|
}
|
|
2868
4088
|
|
|
2869
4089
|
// src/commands/session-end.ts
|
|
2870
|
-
import { writeFile as
|
|
2871
|
-
import { existsSync as
|
|
2872
|
-
import
|
|
4090
|
+
import { writeFile as writeFile16, mkdir as mkdir10 } from "fs/promises";
|
|
4091
|
+
import { existsSync as existsSync30 } from "fs";
|
|
4092
|
+
import path29 from "path";
|
|
2873
4093
|
import "commander";
|
|
2874
4094
|
import {
|
|
2875
|
-
buildFrontmatter as
|
|
4095
|
+
buildFrontmatter as buildFrontmatter6,
|
|
2876
4096
|
findProjectRoot as findProjectRoot29,
|
|
2877
4097
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
2878
|
-
memoryFilePath as
|
|
4098
|
+
memoryFilePath as memoryFilePath5,
|
|
2879
4099
|
resolveHaivePaths as resolveHaivePaths26,
|
|
2880
|
-
serializeMemory as
|
|
4100
|
+
serializeMemory as serializeMemory12
|
|
2881
4101
|
} from "@hiveai/core";
|
|
2882
4102
|
function buildRecapBody(opts) {
|
|
2883
4103
|
const lines = [];
|
|
@@ -2928,7 +4148,7 @@ function registerSessionEnd(session2) {
|
|
|
2928
4148
|
).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) => {
|
|
2929
4149
|
const root = findProjectRoot29(opts.dir);
|
|
2930
4150
|
const paths = resolveHaivePaths26(root);
|
|
2931
|
-
if (!
|
|
4151
|
+
if (!existsSync30(paths.haiveDir)) {
|
|
2932
4152
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2933
4153
|
process.exitCode = 1;
|
|
2934
4154
|
return;
|
|
@@ -2937,12 +4157,12 @@ function registerSessionEnd(session2) {
|
|
|
2937
4157
|
const body = buildRecapBody(opts);
|
|
2938
4158
|
const topic = recapTopic(scope, opts.module);
|
|
2939
4159
|
const filesTouched = parseCsv5(opts.files);
|
|
2940
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
4160
|
+
const missingPaths = filesTouched.filter((p) => !existsSync30(path29.resolve(root, p)));
|
|
2941
4161
|
if (missingPaths.length > 0) {
|
|
2942
4162
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
2943
4163
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
2944
4164
|
}
|
|
2945
|
-
if (
|
|
4165
|
+
if (existsSync30(paths.memoriesDir)) {
|
|
2946
4166
|
const existing = await loadMemoriesFromDir6(paths.memoriesDir);
|
|
2947
4167
|
const topicMatch = existing.find(
|
|
2948
4168
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
@@ -2958,13 +4178,13 @@ function registerSessionEnd(session2) {
|
|
|
2958
4178
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
2959
4179
|
}
|
|
2960
4180
|
};
|
|
2961
|
-
await
|
|
4181
|
+
await writeFile16(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2962
4182
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
2963
|
-
ui.info(`id=${fm.id} file=${
|
|
4183
|
+
ui.info(`id=${fm.id} file=${path29.relative(root, topicMatch.filePath)}`);
|
|
2964
4184
|
return;
|
|
2965
4185
|
}
|
|
2966
4186
|
}
|
|
2967
|
-
const frontmatter =
|
|
4187
|
+
const frontmatter = buildFrontmatter6({
|
|
2968
4188
|
type: "session_recap",
|
|
2969
4189
|
slug: "recap",
|
|
2970
4190
|
scope,
|
|
@@ -2974,11 +4194,11 @@ function registerSessionEnd(session2) {
|
|
|
2974
4194
|
topic,
|
|
2975
4195
|
status: "validated"
|
|
2976
4196
|
});
|
|
2977
|
-
const file =
|
|
2978
|
-
await
|
|
2979
|
-
await
|
|
4197
|
+
const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
4198
|
+
await mkdir10(path29.dirname(file), { recursive: true });
|
|
4199
|
+
await writeFile16(file, serializeMemory12({ frontmatter, body }), "utf8");
|
|
2980
4200
|
ui.success(`Session recap created`);
|
|
2981
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
4201
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path29.relative(root, file)}`);
|
|
2982
4202
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
2983
4203
|
});
|
|
2984
4204
|
}
|
|
@@ -2988,9 +4208,9 @@ function parseCsv5(value) {
|
|
|
2988
4208
|
}
|
|
2989
4209
|
|
|
2990
4210
|
// src/commands/snapshot.ts
|
|
2991
|
-
import { existsSync as
|
|
2992
|
-
import { readdir } from "fs/promises";
|
|
2993
|
-
import
|
|
4211
|
+
import { existsSync as existsSync31 } from "fs";
|
|
4212
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
4213
|
+
import path30 from "path";
|
|
2994
4214
|
import "commander";
|
|
2995
4215
|
import {
|
|
2996
4216
|
diffContract,
|
|
@@ -3023,18 +4243,18 @@ function registerSnapshot(program2) {
|
|
|
3023
4243
|
).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) => {
|
|
3024
4244
|
const root = findProjectRoot30(opts.dir);
|
|
3025
4245
|
const paths = resolveHaivePaths27(root);
|
|
3026
|
-
if (!
|
|
4246
|
+
if (!existsSync31(paths.haiveDir)) {
|
|
3027
4247
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
3028
4248
|
process.exitCode = 1;
|
|
3029
4249
|
return;
|
|
3030
4250
|
}
|
|
3031
4251
|
if (opts.list) {
|
|
3032
|
-
const contractsDir =
|
|
3033
|
-
if (!
|
|
4252
|
+
const contractsDir = path30.join(paths.haiveDir, "contracts");
|
|
4253
|
+
if (!existsSync31(contractsDir)) {
|
|
3034
4254
|
console.log(ui.dim("No contract snapshots found."));
|
|
3035
4255
|
return;
|
|
3036
4256
|
}
|
|
3037
|
-
const files = (await
|
|
4257
|
+
const files = (await readdir2(contractsDir)).filter(
|
|
3038
4258
|
(f) => f.endsWith(".lock") && !f.startsWith("deps-")
|
|
3039
4259
|
);
|
|
3040
4260
|
if (files.length === 0) {
|
|
@@ -3085,7 +4305,7 @@ function registerSnapshot(program2) {
|
|
|
3085
4305
|
return;
|
|
3086
4306
|
}
|
|
3087
4307
|
const contractPath = opts.contract;
|
|
3088
|
-
const name = opts.name ??
|
|
4308
|
+
const name = opts.name ?? path30.basename(contractPath, path30.extname(contractPath));
|
|
3089
4309
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
3090
4310
|
const contract = { name, path: contractPath, format };
|
|
3091
4311
|
try {
|
|
@@ -3140,8 +4360,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
3140
4360
|
}
|
|
3141
4361
|
}
|
|
3142
4362
|
function detectFormat(filePath) {
|
|
3143
|
-
const ext =
|
|
3144
|
-
const base =
|
|
4363
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
4364
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
3145
4365
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
3146
4366
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
3147
4367
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -3154,9 +4374,9 @@ function detectFormat(filePath) {
|
|
|
3154
4374
|
}
|
|
3155
4375
|
|
|
3156
4376
|
// src/commands/hub.ts
|
|
3157
|
-
import { existsSync as
|
|
3158
|
-
import { mkdir as
|
|
3159
|
-
import
|
|
4377
|
+
import { existsSync as existsSync32 } from "fs";
|
|
4378
|
+
import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile17, copyFile } from "fs/promises";
|
|
4379
|
+
import path31 from "path";
|
|
3160
4380
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3161
4381
|
import "commander";
|
|
3162
4382
|
import {
|
|
@@ -3165,7 +4385,7 @@ import {
|
|
|
3165
4385
|
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
3166
4386
|
resolveHaivePaths as resolveHaivePaths28,
|
|
3167
4387
|
saveConfig as saveConfig2,
|
|
3168
|
-
serializeMemory as
|
|
4388
|
+
serializeMemory as serializeMemory13
|
|
3169
4389
|
} from "@hiveai/core";
|
|
3170
4390
|
function registerHub(program2) {
|
|
3171
4391
|
const hub = program2.command("hub").description(
|
|
@@ -3175,8 +4395,8 @@ function registerHub(program2) {
|
|
|
3175
4395
|
hub.command("init <hubPath>").description(
|
|
3176
4396
|
"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"
|
|
3177
4397
|
).action(async (hubPath) => {
|
|
3178
|
-
const absPath =
|
|
3179
|
-
await
|
|
4398
|
+
const absPath = path31.resolve(hubPath);
|
|
4399
|
+
await mkdir11(absPath, { recursive: true });
|
|
3180
4400
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
3181
4401
|
if (gitCheck.status !== 0) {
|
|
3182
4402
|
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
@@ -3186,10 +4406,10 @@ function registerHub(program2) {
|
|
|
3186
4406
|
return;
|
|
3187
4407
|
}
|
|
3188
4408
|
}
|
|
3189
|
-
const sharedDir =
|
|
3190
|
-
await
|
|
3191
|
-
await
|
|
3192
|
-
|
|
4409
|
+
const sharedDir = path31.join(absPath, ".ai", "memories", "shared");
|
|
4410
|
+
await mkdir11(sharedDir, { recursive: true });
|
|
4411
|
+
await writeFile17(
|
|
4412
|
+
path31.join(absPath, ".ai", "README.md"),
|
|
3193
4413
|
`# hAIve Team Knowledge Hub
|
|
3194
4414
|
|
|
3195
4415
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -3210,8 +4430,8 @@ haive hub pull # import into a project
|
|
|
3210
4430
|
`,
|
|
3211
4431
|
"utf8"
|
|
3212
4432
|
);
|
|
3213
|
-
await
|
|
3214
|
-
|
|
4433
|
+
await writeFile17(
|
|
4434
|
+
path31.join(absPath, ".gitignore"),
|
|
3215
4435
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
3216
4436
|
"utf8"
|
|
3217
4437
|
);
|
|
@@ -3226,7 +4446,7 @@ haive hub pull # import into a project
|
|
|
3226
4446
|
`
|
|
3227
4447
|
Next steps:
|
|
3228
4448
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
3229
|
-
{ "hubPath": "${
|
|
4449
|
+
{ "hubPath": "${path31.relative(process.cwd(), absPath)}" }
|
|
3230
4450
|
2. Run \`haive hub push\` to publish your shared memories
|
|
3231
4451
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
3232
4452
|
`
|
|
@@ -3255,15 +4475,15 @@ Next steps:
|
|
|
3255
4475
|
process.exitCode = 1;
|
|
3256
4476
|
return;
|
|
3257
4477
|
}
|
|
3258
|
-
const hubRoot =
|
|
3259
|
-
if (!
|
|
4478
|
+
const hubRoot = path31.resolve(root, config.hubPath);
|
|
4479
|
+
if (!existsSync32(hubRoot)) {
|
|
3260
4480
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
3261
4481
|
process.exitCode = 1;
|
|
3262
4482
|
return;
|
|
3263
4483
|
}
|
|
3264
|
-
const projectName =
|
|
3265
|
-
const destDir =
|
|
3266
|
-
await
|
|
4484
|
+
const projectName = path31.basename(root);
|
|
4485
|
+
const destDir = path31.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
4486
|
+
await mkdir11(destDir, { recursive: true });
|
|
3267
4487
|
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3268
4488
|
const shared = all.filter(
|
|
3269
4489
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -3281,15 +4501,15 @@ Next steps:
|
|
|
3281
4501
|
for (const { memory: memory2 } of shared) {
|
|
3282
4502
|
const fm = memory2.frontmatter;
|
|
3283
4503
|
const fileName = `${fm.id}.md`;
|
|
3284
|
-
const destPath =
|
|
3285
|
-
await
|
|
4504
|
+
const destPath = path31.join(destDir, fileName);
|
|
4505
|
+
await writeFile17(destPath, serializeMemory13(memory2), "utf8");
|
|
3286
4506
|
pushed++;
|
|
3287
4507
|
}
|
|
3288
4508
|
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
3289
4509
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
3290
4510
|
if (opts.commit) {
|
|
3291
4511
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
3292
|
-
spawnSync3("git", ["add",
|
|
4512
|
+
spawnSync3("git", ["add", path31.join(".ai", "memories", "shared", projectName)], {
|
|
3293
4513
|
cwd: hubRoot
|
|
3294
4514
|
});
|
|
3295
4515
|
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
@@ -3324,15 +4544,15 @@ Next steps:
|
|
|
3324
4544
|
process.exitCode = 1;
|
|
3325
4545
|
return;
|
|
3326
4546
|
}
|
|
3327
|
-
const hubRoot =
|
|
3328
|
-
const hubSharedDir =
|
|
3329
|
-
if (!
|
|
4547
|
+
const hubRoot = path31.resolve(root, config.hubPath);
|
|
4548
|
+
const hubSharedDir = path31.join(hubRoot, ".ai", "memories", "shared");
|
|
4549
|
+
if (!existsSync32(hubSharedDir)) {
|
|
3330
4550
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
3331
4551
|
return;
|
|
3332
4552
|
}
|
|
3333
|
-
const projectName =
|
|
3334
|
-
const { readdir:
|
|
3335
|
-
const projectDirs = (await
|
|
4553
|
+
const projectName = path31.basename(root);
|
|
4554
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
4555
|
+
const projectDirs = (await readdir3(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
3336
4556
|
if (projectDirs.length === 0) {
|
|
3337
4557
|
console.log(ui.dim("No other projects have pushed to the hub yet."));
|
|
3338
4558
|
return;
|
|
@@ -3340,17 +4560,17 @@ Next steps:
|
|
|
3340
4560
|
let totalImported = 0;
|
|
3341
4561
|
let totalUpdated = 0;
|
|
3342
4562
|
for (const sourceName of projectDirs) {
|
|
3343
|
-
const sourceDir =
|
|
3344
|
-
const destDir =
|
|
3345
|
-
await
|
|
3346
|
-
const sourceFiles = (await
|
|
4563
|
+
const sourceDir = path31.join(hubSharedDir, sourceName);
|
|
4564
|
+
const destDir = path31.join(paths.memoriesDir, "shared", sourceName);
|
|
4565
|
+
await mkdir11(destDir, { recursive: true });
|
|
4566
|
+
const sourceFiles = (await readdir3(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
3347
4567
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
3348
4568
|
const existingInDest = await loadDir(destDir);
|
|
3349
4569
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
3350
4570
|
for (const file of sourceFiles) {
|
|
3351
|
-
const srcPath =
|
|
3352
|
-
const destPath =
|
|
3353
|
-
const fileContent = await
|
|
4571
|
+
const srcPath = path31.join(sourceDir, file);
|
|
4572
|
+
const destPath = path31.join(destDir, file);
|
|
4573
|
+
const fileContent = await readFile12(srcPath, "utf8");
|
|
3354
4574
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
3355
4575
|
if (!alreadyTagged) {
|
|
3356
4576
|
await copyFile(srcPath, destPath);
|
|
@@ -3380,14 +4600,14 @@ Next steps:
|
|
|
3380
4600
|
console.log(
|
|
3381
4601
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
3382
4602
|
);
|
|
3383
|
-
const sharedDir =
|
|
3384
|
-
if (
|
|
3385
|
-
const { readdir:
|
|
3386
|
-
const sources = (await
|
|
4603
|
+
const sharedDir = path31.join(paths.memoriesDir, "shared");
|
|
4604
|
+
if (existsSync32(sharedDir)) {
|
|
4605
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
4606
|
+
const sources = (await readdir3(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3387
4607
|
console.log(`
|
|
3388
4608
|
Imported from ${sources.length} source(s):`);
|
|
3389
4609
|
for (const src of sources) {
|
|
3390
|
-
const files = (await
|
|
4610
|
+
const files = (await readdir3(path31.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
3391
4611
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
3392
4612
|
}
|
|
3393
4613
|
} else {
|
|
@@ -3402,8 +4622,8 @@ Next steps:
|
|
|
3402
4622
|
if (outgoing.length > 0) {
|
|
3403
4623
|
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
3404
4624
|
}
|
|
3405
|
-
void
|
|
3406
|
-
void
|
|
4625
|
+
void readFile12;
|
|
4626
|
+
void writeFile17;
|
|
3407
4627
|
void saveConfig2;
|
|
3408
4628
|
});
|
|
3409
4629
|
}
|
|
@@ -3605,20 +4825,20 @@ function summarize(name, t0, payload, notes) {
|
|
|
3605
4825
|
}
|
|
3606
4826
|
|
|
3607
4827
|
// src/commands/memory-suggest.ts
|
|
3608
|
-
import { mkdir as
|
|
3609
|
-
import { existsSync as
|
|
3610
|
-
import
|
|
4828
|
+
import { mkdir as mkdir12, writeFile as writeFile18 } from "fs/promises";
|
|
4829
|
+
import { existsSync as existsSync33 } from "fs";
|
|
4830
|
+
import path32 from "path";
|
|
3611
4831
|
import "commander";
|
|
3612
4832
|
import {
|
|
3613
4833
|
aggregateUsage as aggregateUsage2,
|
|
3614
|
-
buildFrontmatter as
|
|
4834
|
+
buildFrontmatter as buildFrontmatter7,
|
|
3615
4835
|
findProjectRoot as findProjectRoot34,
|
|
3616
4836
|
loadMemoriesFromDir as loadMemoriesFromDir8,
|
|
3617
|
-
memoryFilePath as
|
|
4837
|
+
memoryFilePath as memoryFilePath6,
|
|
3618
4838
|
parseSince as parseSince2,
|
|
3619
4839
|
readUsageEvents as readUsageEvents2,
|
|
3620
4840
|
resolveHaivePaths as resolveHaivePaths31,
|
|
3621
|
-
serializeMemory as
|
|
4841
|
+
serializeMemory as serializeMemory14
|
|
3622
4842
|
} from "@hiveai/core";
|
|
3623
4843
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
3624
4844
|
"mem_search",
|
|
@@ -3677,7 +4897,7 @@ function registerMemorySuggest(memory2) {
|
|
|
3677
4897
|
}
|
|
3678
4898
|
const created = [];
|
|
3679
4899
|
const skipped = [];
|
|
3680
|
-
const existing =
|
|
4900
|
+
const existing = existsSync33(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
|
|
3681
4901
|
for (const s of top) {
|
|
3682
4902
|
const slug = slugify(s.query);
|
|
3683
4903
|
if (!slug) {
|
|
@@ -3689,7 +4909,7 @@ function registerMemorySuggest(memory2) {
|
|
|
3689
4909
|
skipped.push({ query: s.query, reason: `similar memory already exists (${dup.memory.frontmatter.id})` });
|
|
3690
4910
|
continue;
|
|
3691
4911
|
}
|
|
3692
|
-
const fm =
|
|
4912
|
+
const fm = buildFrontmatter7({
|
|
3693
4913
|
type: s.inferred_type,
|
|
3694
4914
|
slug,
|
|
3695
4915
|
scope,
|
|
@@ -3699,14 +4919,14 @@ function registerMemorySuggest(memory2) {
|
|
|
3699
4919
|
});
|
|
3700
4920
|
fm.status = "draft";
|
|
3701
4921
|
const body = renderTemplate(s);
|
|
3702
|
-
const file =
|
|
3703
|
-
await
|
|
3704
|
-
if (
|
|
3705
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
4922
|
+
const file = memoryFilePath6(paths, fm.scope, fm.id, fm.module);
|
|
4923
|
+
await mkdir12(path32.dirname(file), { recursive: true });
|
|
4924
|
+
if (existsSync33(file)) {
|
|
4925
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path32.relative(root, file)}` });
|
|
3706
4926
|
continue;
|
|
3707
4927
|
}
|
|
3708
|
-
await
|
|
3709
|
-
created.push({ id: fm.id, file:
|
|
4928
|
+
await writeFile18(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
|
|
4929
|
+
created.push({ id: fm.id, file: path32.relative(root, file), query: s.query });
|
|
3710
4930
|
}
|
|
3711
4931
|
if (opts.json) {
|
|
3712
4932
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -3799,9 +5019,9 @@ function truncate(text, max) {
|
|
|
3799
5019
|
}
|
|
3800
5020
|
|
|
3801
5021
|
// src/commands/memory-archive.ts
|
|
3802
|
-
import { existsSync as
|
|
3803
|
-
import { writeFile as
|
|
3804
|
-
import
|
|
5022
|
+
import { existsSync as existsSync34 } from "fs";
|
|
5023
|
+
import { writeFile as writeFile19 } from "fs/promises";
|
|
5024
|
+
import path33 from "path";
|
|
3805
5025
|
import "commander";
|
|
3806
5026
|
import {
|
|
3807
5027
|
findProjectRoot as findProjectRoot35,
|
|
@@ -3809,7 +5029,7 @@ import {
|
|
|
3809
5029
|
loadMemoriesFromDir as loadMemoriesFromDir9,
|
|
3810
5030
|
loadUsageIndex as loadUsageIndex11,
|
|
3811
5031
|
resolveHaivePaths as resolveHaivePaths32,
|
|
3812
|
-
serializeMemory as
|
|
5032
|
+
serializeMemory as serializeMemory15
|
|
3813
5033
|
} from "@hiveai/core";
|
|
3814
5034
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3815
5035
|
function registerMemoryArchive(memory2) {
|
|
@@ -3818,7 +5038,7 @@ function registerMemoryArchive(memory2) {
|
|
|
3818
5038
|
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3819
5039
|
const root = findProjectRoot35(opts.dir);
|
|
3820
5040
|
const paths = resolveHaivePaths32(root);
|
|
3821
|
-
if (!
|
|
5041
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
3822
5042
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
3823
5043
|
process.exitCode = 1;
|
|
3824
5044
|
return;
|
|
@@ -3839,7 +5059,7 @@ function registerMemoryArchive(memory2) {
|
|
|
3839
5059
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
3840
5060
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
3841
5061
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
3842
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
5062
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync34(path33.join(paths.root, p)));
|
|
3843
5063
|
const isAnchorless = !hasAnyAnchor;
|
|
3844
5064
|
if (!isAnchorless && !allPathsGone) continue;
|
|
3845
5065
|
const u = getUsage9(usage, fm.id);
|
|
@@ -3887,7 +5107,7 @@ function registerMemoryArchive(memory2) {
|
|
|
3887
5107
|
if (!found) continue;
|
|
3888
5108
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
3889
5109
|
try {
|
|
3890
|
-
await
|
|
5110
|
+
await writeFile19(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
3891
5111
|
archived++;
|
|
3892
5112
|
} catch (err) {
|
|
3893
5113
|
if (!opts.json) {
|
|
@@ -3913,7 +5133,7 @@ function parseDays(input) {
|
|
|
3913
5133
|
}
|
|
3914
5134
|
|
|
3915
5135
|
// src/commands/doctor.ts
|
|
3916
|
-
import { existsSync as
|
|
5136
|
+
import { existsSync as existsSync35 } from "fs";
|
|
3917
5137
|
import { stat } from "fs/promises";
|
|
3918
5138
|
import "path";
|
|
3919
5139
|
import "commander";
|
|
@@ -3936,7 +5156,7 @@ function registerDoctor(program2) {
|
|
|
3936
5156
|
const root = findProjectRoot36(opts.dir);
|
|
3937
5157
|
const paths = resolveHaivePaths33(root);
|
|
3938
5158
|
const findings = [];
|
|
3939
|
-
if (!
|
|
5159
|
+
if (!existsSync35(paths.haiveDir)) {
|
|
3940
5160
|
findings.push({
|
|
3941
5161
|
severity: "error",
|
|
3942
5162
|
code: "not-initialized",
|
|
@@ -3945,7 +5165,7 @@ function registerDoctor(program2) {
|
|
|
3945
5165
|
});
|
|
3946
5166
|
return emit(findings, opts);
|
|
3947
5167
|
}
|
|
3948
|
-
if (!
|
|
5168
|
+
if (!existsSync35(paths.projectContext)) {
|
|
3949
5169
|
findings.push({
|
|
3950
5170
|
severity: "warn",
|
|
3951
5171
|
code: "no-project-context",
|
|
@@ -3953,8 +5173,8 @@ function registerDoctor(program2) {
|
|
|
3953
5173
|
fix: "haive init"
|
|
3954
5174
|
});
|
|
3955
5175
|
} else {
|
|
3956
|
-
const { readFile:
|
|
3957
|
-
const content = await
|
|
5176
|
+
const { readFile: readFile13 } = await import("fs/promises");
|
|
5177
|
+
const content = await readFile13(paths.projectContext, "utf8");
|
|
3958
5178
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
3959
5179
|
if (isTemplate) {
|
|
3960
5180
|
findings.push({
|
|
@@ -3965,7 +5185,7 @@ function registerDoctor(program2) {
|
|
|
3965
5185
|
});
|
|
3966
5186
|
}
|
|
3967
5187
|
}
|
|
3968
|
-
const memories =
|
|
5188
|
+
const memories = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
|
|
3969
5189
|
const now = Date.now();
|
|
3970
5190
|
if (memories.length === 0) {
|
|
3971
5191
|
findings.push({
|
|
@@ -4119,7 +5339,7 @@ function isSearchTool(name) {
|
|
|
4119
5339
|
}
|
|
4120
5340
|
|
|
4121
5341
|
// src/commands/playback.ts
|
|
4122
|
-
import { existsSync as
|
|
5342
|
+
import { existsSync as existsSync36 } from "fs";
|
|
4123
5343
|
import "commander";
|
|
4124
5344
|
import {
|
|
4125
5345
|
findProjectRoot as findProjectRoot37,
|
|
@@ -4149,7 +5369,7 @@ function registerPlayback(program2) {
|
|
|
4149
5369
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
4150
5370
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
4151
5371
|
const sessions = bucketSessions(filtered, gapMs);
|
|
4152
|
-
const all =
|
|
5372
|
+
const all = existsSync36(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
|
|
4153
5373
|
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
4154
5374
|
const enriched = sessions.map((s, i) => {
|
|
4155
5375
|
const startMs = Date.parse(s.start);
|
|
@@ -4345,7 +5565,7 @@ function runCommand(cmd, args, cwd) {
|
|
|
4345
5565
|
|
|
4346
5566
|
// src/index.ts
|
|
4347
5567
|
var program = new Command39();
|
|
4348
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
5568
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.7.1");
|
|
4349
5569
|
registerInit(program);
|
|
4350
5570
|
registerMcp(program);
|
|
4351
5571
|
registerBriefing(program);
|