@hiveai/cli 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -18
- package/dist/index.js +872 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command32 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -53,8 +53,8 @@ var ui = {
|
|
|
53
53
|
// src/commands/briefing.ts
|
|
54
54
|
function registerBriefing(program2) {
|
|
55
55
|
program2.command("briefing").description(
|
|
56
|
-
|
|
57
|
-
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (
|
|
56
|
+
'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --symbols PaymentService,TenantFilter # look up where symbols live\n'
|
|
57
|
+
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option(
|
|
58
58
|
"--scope <scope>",
|
|
59
59
|
"personal | team | module | all (default: team)",
|
|
60
60
|
"team"
|
|
@@ -204,7 +204,9 @@ function parseCsv(value) {
|
|
|
204
204
|
import "commander";
|
|
205
205
|
import { findProjectRoot as findProjectRoot2 } from "@hiveai/core";
|
|
206
206
|
function registerTui(program2) {
|
|
207
|
-
program2.command("tui").description(
|
|
207
|
+
program2.command("tui").description(
|
|
208
|
+
"Interactive terminal dashboard for browsing and managing memories.\n\n Screens (switch with 1 / 2 / 3):\n 1 \u2014 Memories: list + preview, filter by status (Tab), actions (a/r/p/d)\n 2 \u2014 Health: stale, pending review, anchorless memories\n 3 \u2014 Stats: most-read, decaying, total counts\n\n Key bindings:\n \u2191 \u2193 navigate list\n Tab cycle status filter (all \u2192 proposed \u2192 validated \u2192 stale)\n a approve selected memory\n r reject selected memory\n p promote personal \u2192 team (proposed)\n d delete selected memory\n q / Esc exit\n"
|
|
209
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
208
210
|
if (!process.stdout.isTTY) {
|
|
209
211
|
console.error("haive tui requires an interactive terminal (TTY).");
|
|
210
212
|
process.exitCode = 1;
|
|
@@ -303,9 +305,13 @@ import {
|
|
|
303
305
|
saveCodeMap
|
|
304
306
|
} from "@hiveai/core";
|
|
305
307
|
function registerIndexCode(program2) {
|
|
306
|
-
const idx = program2.command("index").description(
|
|
308
|
+
const idx = program2.command("index").description(
|
|
309
|
+
"Build local indexes that let AIs look up symbols instead of grepping.\n\n Run once after init, then haive sync refreshes it automatically when source changes."
|
|
310
|
+
);
|
|
307
311
|
idx.action(() => idx.help());
|
|
308
|
-
idx.command("code").description(
|
|
312
|
+
idx.command("code").description(
|
|
313
|
+
"Scan source files and write .ai/code-map.json (file \u2192 exports + 1-line description).\n\n Supported languages: TypeScript, JavaScript, Java, Python, Go, Rust, C#, PHP.\n The map is used by:\n \u2022 get_briefing (symbol_locations) \u2014 look up where a class/function lives\n \u2022 code_map MCP tool \u2014 browse exports without grepping\n \u2022 haive briefing --symbols \u2014 look up symbols from the CLI\n\n Run automatically by haive init (autopilot mode) and haive sync (if source changed).\n\n Example:\n haive index code\n haive index code --exclude generated,proto\n"
|
|
314
|
+
).option("-d, --dir <dir>", "project root").option(
|
|
309
315
|
"--exclude <csv>",
|
|
310
316
|
"extra directory names to skip (comma-separated)",
|
|
311
317
|
""
|
|
@@ -456,15 +462,39 @@ jobs:
|
|
|
456
462
|
issue_number: context.issue.number,
|
|
457
463
|
body: \`### haive \u2014 Stale memories detected\\n\\nSome memories anchored to code modified in this PR may be outdated:\\n\\n\\\`\\\`\\\`\\n\${report}\\n\\\`\\\`\\\`\\n\\nRun \\\`haive memory verify --update\\\` locally to refresh them before merging.\`
|
|
458
464
|
});
|
|
465
|
+
|
|
466
|
+
# On push to main: push shared memories to the hub (if hubPath is configured)
|
|
467
|
+
# Uncomment and configure hubPath in .ai/haive.config.json to enable.
|
|
468
|
+
# hub-push:
|
|
469
|
+
# if: github.event_name == 'push'
|
|
470
|
+
# needs: sync-on-merge
|
|
471
|
+
# runs-on: ubuntu-latest
|
|
472
|
+
# permissions:
|
|
473
|
+
# contents: write
|
|
474
|
+
# steps:
|
|
475
|
+
# - uses: actions/checkout@v4
|
|
476
|
+
# with:
|
|
477
|
+
# fetch-depth: 0
|
|
478
|
+
# - uses: actions/setup-node@v4
|
|
479
|
+
# with:
|
|
480
|
+
# node-version: '20'
|
|
481
|
+
# - name: install haive
|
|
482
|
+
# run: npm install -g @hiveai/cli
|
|
483
|
+
# - name: push shared memories to hub
|
|
484
|
+
# run: haive hub push --commit
|
|
485
|
+
# # Requires hubPath in .ai/haive.config.json pointing to a cloned hub repo.
|
|
486
|
+
# # The hub repo must be available at that path in the CI workspace.
|
|
459
487
|
`;
|
|
460
488
|
function registerInit(program2) {
|
|
461
|
-
program2.command("init").description(
|
|
462
|
-
"--
|
|
463
|
-
|
|
489
|
+
program2.command("init").description(
|
|
490
|
+
"Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Add --manual if you want to control memory approval and session recaps yourself."
|
|
491
|
+
).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(
|
|
492
|
+
"--manual",
|
|
493
|
+
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
464
494
|
).action(async (opts) => {
|
|
465
495
|
const root = path3.resolve(opts.dir);
|
|
466
496
|
const paths = resolveHaivePaths4(root);
|
|
467
|
-
const autopilot = opts.
|
|
497
|
+
const autopilot = opts.manual !== true;
|
|
468
498
|
if (existsSync3(paths.haiveDir)) {
|
|
469
499
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
470
500
|
}
|
|
@@ -548,7 +578,7 @@ function registerInit(program2) {
|
|
|
548
578
|
console.log(ui.dim(" 3. Start every AI session with:"));
|
|
549
579
|
console.log(" " + ui.bold("get_briefing({ task: '\u2026what you are about to do\u2026' })"));
|
|
550
580
|
console.log();
|
|
551
|
-
console.log(ui.dim(" Tip: run `haive init --
|
|
581
|
+
console.log(ui.dim(" Tip: run `haive init` (without --manual) for zero-friction autopilot mode."));
|
|
552
582
|
}
|
|
553
583
|
});
|
|
554
584
|
}
|
|
@@ -583,7 +613,9 @@ fi
|
|
|
583
613
|
`;
|
|
584
614
|
var HOOKS = ["post-merge", "post-rewrite"];
|
|
585
615
|
function registerInstallHooks(program2) {
|
|
586
|
-
program2.command("install-hooks").description(
|
|
616
|
+
program2.command("install-hooks").description(
|
|
617
|
+
"Install git hooks so haive sync runs automatically after every pull or merge.\n\n Installs a post-merge hook at .git/hooks/post-merge that runs:\n haive sync --quiet --since ORIG_HEAD --embed\n\n This ensures memory anchors are always verified and the embeddings index\n is kept fresh without requiring any manual steps.\n\n Installed automatically by haive init (autopilot mode).\n Use --force to overwrite an existing post-merge hook.\n"
|
|
618
|
+
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
587
619
|
const root = findProjectRoot6(opts.dir);
|
|
588
620
|
const gitDir = path4.join(root, ".git");
|
|
589
621
|
if (!existsSync4(gitDir)) {
|
|
@@ -624,7 +656,20 @@ import "commander";
|
|
|
624
656
|
import { findProjectRoot as findProjectRoot7 } from "@hiveai/core";
|
|
625
657
|
var require2 = createRequire(import.meta.url);
|
|
626
658
|
function registerMcp(program2) {
|
|
627
|
-
program2.command("mcp").description(
|
|
659
|
+
program2.command("mcp").description(
|
|
660
|
+
`Start the hAIve MCP server (stdio transport) for direct testing.
|
|
661
|
+
|
|
662
|
+
In normal use, your AI client (Claude Code, Cursor, VS Code) starts the server
|
|
663
|
+
automatically via the haive-mcp binary listed in your MCP config file.
|
|
664
|
+
|
|
665
|
+
This command is useful for debugging or for clients that require manual startup.
|
|
666
|
+
|
|
667
|
+
Client config examples (point at haive-mcp binary, not this command):
|
|
668
|
+
Claude Code: ~/.claude.json \u2192 mcpServers.haive.command = "haive-mcp"
|
|
669
|
+
Cursor: ~/.cursor/mcp.json \u2192 mcpServers.haive.command = "haive-mcp"
|
|
670
|
+
VS Code: code --add-mcp '{"name":"haive","command":"haive-mcp",...}'
|
|
671
|
+
`
|
|
672
|
+
).option("-d, --dir <dir>", "project root (defaults to nearest .ai/ or .git/)").action((opts) => {
|
|
628
673
|
const root = findProjectRoot7(opts.dir);
|
|
629
674
|
const bin = locateMcpBin();
|
|
630
675
|
if (!bin) {
|
|
@@ -656,12 +701,13 @@ function locateMcpBin() {
|
|
|
656
701
|
|
|
657
702
|
// src/commands/sync.ts
|
|
658
703
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
659
|
-
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
704
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
660
705
|
import { existsSync as existsSync6 } from "fs";
|
|
661
706
|
import path6 from "path";
|
|
662
707
|
import "commander";
|
|
663
708
|
import {
|
|
664
709
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
710
|
+
buildFrontmatter,
|
|
665
711
|
findProjectRoot as findProjectRoot8,
|
|
666
712
|
getUsage,
|
|
667
713
|
isAutoPromoteEligible,
|
|
@@ -670,20 +716,26 @@ import {
|
|
|
670
716
|
loadConfig,
|
|
671
717
|
loadMemoriesFromDir as loadMemoriesFromDir2,
|
|
672
718
|
loadUsageIndex,
|
|
719
|
+
pullCrossRepoSources,
|
|
673
720
|
resolveHaivePaths as resolveHaivePaths5,
|
|
721
|
+
resolveManifestFiles,
|
|
674
722
|
serializeMemory,
|
|
675
|
-
|
|
723
|
+
trackDependencies,
|
|
724
|
+
verifyAnchor,
|
|
725
|
+
watchContracts
|
|
676
726
|
} from "@hiveai/core";
|
|
677
727
|
var BRIDGE_START = "<!-- haive:memories-start -->";
|
|
678
728
|
var BRIDGE_END = "<!-- haive:memories-end -->";
|
|
679
729
|
function registerSync(program2) {
|
|
680
|
-
program2.command("sync").description(
|
|
730
|
+
program2.command("sync").description(
|
|
731
|
+
"Refresh memory state after a git pull or merge.\n What it does:\n 1. Verifies anchor paths \u2014 marks stale if files/symbols moved or deleted\n 2. Re-validates previously stale memories that are now fresh\n 3. Auto-promotes proposed memories (by usage count or time delay in autopilot)\n 4. Auto-refreshes code-map if source files changed\n 5. Reports decay warnings for memories unused >90 days\n\n Install git hooks to run sync automatically: haive install-hooks\n\n Examples:\n haive sync\n haive sync --since main # also report memories changed since main\n haive sync --embed # also rebuild embeddings index\n"
|
|
732
|
+
).option("-d, --dir <dir>", "project root").option("--quiet", "minimal output (suitable for git hooks)").option(
|
|
681
733
|
"--since <ref>",
|
|
682
734
|
"git ref/commit to compare against; report memories added/modified/removed since"
|
|
683
735
|
).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
|
|
684
736
|
"--inject-bridge",
|
|
685
737
|
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
686
|
-
).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)").action(async (opts) => {
|
|
738
|
+
).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) => {
|
|
687
739
|
const root = findProjectRoot8(opts.dir);
|
|
688
740
|
const paths = resolveHaivePaths5(root);
|
|
689
741
|
if (!existsSync6(paths.memoriesDir)) {
|
|
@@ -853,6 +905,124 @@ function registerSync(program2) {
|
|
|
853
905
|
}
|
|
854
906
|
}
|
|
855
907
|
}
|
|
908
|
+
if (opts.noCrossRepo !== true && (config.crossRepoSources ?? []).length > 0) {
|
|
909
|
+
try {
|
|
910
|
+
const crossReports = await pullCrossRepoSources(paths, config, root);
|
|
911
|
+
for (const r of crossReports) {
|
|
912
|
+
const total = r.imported.length + r.updated.length;
|
|
913
|
+
if (total > 0 || r.errors.length > 0) {
|
|
914
|
+
log(
|
|
915
|
+
ui.dim(
|
|
916
|
+
`cross-repo [${r.source}]: ${r.imported.length} imported \xB7 ${r.updated.length} updated \xB7 ${r.skipped.length} unchanged` + (r.errors.length > 0 ? ` \xB7 \u26A0 ${r.errors.length} error(s)` : "")
|
|
917
|
+
)
|
|
918
|
+
);
|
|
919
|
+
for (const e of r.errors) ui.warn(` cross-repo error: ${e}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} catch (err) {
|
|
923
|
+
ui.warn(`cross-repo pull failed: ${String(err)}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (opts.noDeps !== true) {
|
|
927
|
+
try {
|
|
928
|
+
const manifestFiles = resolveManifestFiles(root, config.dependencyFiles);
|
|
929
|
+
if (manifestFiles.length > 0) {
|
|
930
|
+
const depResults = await trackDependencies(root, paths.haiveDir, manifestFiles);
|
|
931
|
+
for (const result of depResults) {
|
|
932
|
+
const majorBumps = result.changes.filter((c) => c.isMajorBump);
|
|
933
|
+
const minorChanges = result.changes.filter((c) => !c.isMajorBump);
|
|
934
|
+
if (result.changes.length > 0) {
|
|
935
|
+
log(
|
|
936
|
+
ui.yellow(
|
|
937
|
+
`\u26A0 dependency changes in ${result.file}: ${majorBumps.length} major bump(s) \xB7 ${minorChanges.length} minor change(s)`
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
for (const c of majorBumps) {
|
|
941
|
+
log(ui.yellow(` MAJOR: ${c.name} ${c.from} \u2192 ${c.to}`));
|
|
942
|
+
}
|
|
943
|
+
for (const c of minorChanges) {
|
|
944
|
+
log(ui.dim(` minor: ${c.name} ${c.from} \u2192 ${c.to}`));
|
|
945
|
+
}
|
|
946
|
+
if (majorBumps.length > 0) {
|
|
947
|
+
const slugParts = result.file.replace(/[^a-z0-9]/gi, "-").toLowerCase();
|
|
948
|
+
const slug = `dep-major-bump-${slugParts}-${Date.now().toString(36)}`;
|
|
949
|
+
const body = `## Major dependency version bumps detected in \`${result.file}\`
|
|
950
|
+
|
|
951
|
+
` + majorBumps.map((c) => `- **${c.name}**: \`${c.from}\` \u2192 \`${c.to}\` (major bump \u2014 check for breaking changes)`).join("\n") + `
|
|
952
|
+
|
|
953
|
+
**Action:** Review the changelogs for these packages and update any memories anchored to their APIs.
|
|
954
|
+
Run \`haive memory import --from-changelog CHANGELOG.md\` if available.`;
|
|
955
|
+
const fm = buildFrontmatter({
|
|
956
|
+
type: "gotcha",
|
|
957
|
+
slug,
|
|
958
|
+
scope: "team",
|
|
959
|
+
status: "validated",
|
|
960
|
+
tags: ["dependency", "breaking-change", "auto-generated"],
|
|
961
|
+
paths: [result.file],
|
|
962
|
+
topic: `dep-bump-${slugParts}`
|
|
963
|
+
});
|
|
964
|
+
const teamDir = path6.join(paths.memoriesDir, "team");
|
|
965
|
+
await mkdir3(teamDir, { recursive: true });
|
|
966
|
+
await writeFile3(
|
|
967
|
+
path6.join(teamDir, `${fm.id}.md`),
|
|
968
|
+
serializeMemory({ frontmatter: fm, body }),
|
|
969
|
+
"utf8"
|
|
970
|
+
);
|
|
971
|
+
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} catch (err) {
|
|
977
|
+
ui.warn(`dependency tracker failed: ${String(err)}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (opts.noContracts !== true && (config.contractFiles ?? []).length > 0) {
|
|
981
|
+
try {
|
|
982
|
+
const diffs = await watchContracts(root, paths.haiveDir, config.contractFiles);
|
|
983
|
+
for (const diff of diffs) {
|
|
984
|
+
const breaking = diff.changes.filter((c) => c.severity === "breaking");
|
|
985
|
+
const additive = diff.changes.filter((c) => c.severity === "additive");
|
|
986
|
+
log(
|
|
987
|
+
ui.yellow(
|
|
988
|
+
`\u26A0 contract changed [${diff.contract}]: ${breaking.length} breaking \xB7 ${additive.length} additive`
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
for (const c of diff.changes) {
|
|
992
|
+
const icon = c.severity === "breaking" ? "\u{1F534}" : c.severity === "additive" ? "\u{1F7E2}" : "\u{1F7E1}";
|
|
993
|
+
log(` ${icon} ${c.description}`);
|
|
994
|
+
}
|
|
995
|
+
if (breaking.length > 0) {
|
|
996
|
+
const slug = `contract-breaking-${diff.contract.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-${Date.now().toString(36)}`;
|
|
997
|
+
const body = `## Breaking changes detected in contract: \`${diff.contract}\` (\`${diff.file}\`)
|
|
998
|
+
|
|
999
|
+
` + breaking.map((c) => `- \u{1F534} **${c.kind}**: ${c.description}`).join("\n") + (additive.length > 0 ? "\n\n### Additive changes (non-breaking)\n" + additive.map((c) => `- \u{1F7E2} ${c.description}`).join("\n") : "") + `
|
|
1000
|
+
|
|
1001
|
+
**Action:** Review all consumers of this contract and update accordingly.
|
|
1002
|
+
Check memories tagged with \`${diff.contract}\` for potentially stale knowledge.`;
|
|
1003
|
+
const fm = buildFrontmatter({
|
|
1004
|
+
type: "gotcha",
|
|
1005
|
+
slug,
|
|
1006
|
+
scope: "team",
|
|
1007
|
+
status: "validated",
|
|
1008
|
+
tags: ["api-contract", "breaking-change", diff.contract, "auto-generated"],
|
|
1009
|
+
paths: [diff.file],
|
|
1010
|
+
topic: `contract-breaking-${diff.contract}`
|
|
1011
|
+
});
|
|
1012
|
+
const teamDir = path6.join(paths.memoriesDir, "team");
|
|
1013
|
+
await mkdir3(teamDir, { recursive: true });
|
|
1014
|
+
await writeFile3(
|
|
1015
|
+
path6.join(teamDir, `${fm.id}.md`),
|
|
1016
|
+
serializeMemory({ frontmatter: fm, body }),
|
|
1017
|
+
"utf8"
|
|
1018
|
+
);
|
|
1019
|
+
log(ui.yellow(` \u2192 memory created: ${fm.id}`));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
ui.warn(`contract watcher failed: ${String(err)}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
856
1026
|
const existingMap = await loadCodeMap2(paths);
|
|
857
1027
|
if (existingMap) {
|
|
858
1028
|
const mapAge = new Date(existingMap.generated_at).getTime();
|
|
@@ -978,12 +1148,12 @@ function collectSinceChanges(root, ref) {
|
|
|
978
1148
|
|
|
979
1149
|
// src/commands/memory-add.ts
|
|
980
1150
|
import { createHash } from "crypto";
|
|
981
|
-
import { mkdir as
|
|
1151
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
982
1152
|
import { existsSync as existsSync7 } from "fs";
|
|
983
1153
|
import path7 from "path";
|
|
984
1154
|
import "commander";
|
|
985
1155
|
import {
|
|
986
|
-
buildFrontmatter,
|
|
1156
|
+
buildFrontmatter as buildFrontmatter2,
|
|
987
1157
|
findProjectRoot as findProjectRoot9,
|
|
988
1158
|
inferModulesFromPaths,
|
|
989
1159
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
@@ -992,7 +1162,30 @@ import {
|
|
|
992
1162
|
serializeMemory as serializeMemory2
|
|
993
1163
|
} from "@hiveai/core";
|
|
994
1164
|
function registerMemoryAdd(memory2) {
|
|
995
|
-
memory2.command("add").description(
|
|
1165
|
+
memory2.command("add").description(
|
|
1166
|
+
`Save a piece of knowledge as a persistent memory.
|
|
1167
|
+
|
|
1168
|
+
Memory types:
|
|
1169
|
+
convention \u2014 how things are done here (naming, patterns, tooling)
|
|
1170
|
+
decision \u2014 a choice made and WHY (tradeoffs, constraints)
|
|
1171
|
+
gotcha \u2014 non-obvious behavior that surprises newcomers
|
|
1172
|
+
architecture \u2014 structural overview of a system or module
|
|
1173
|
+
glossary \u2014 domain terms and their meaning in this codebase
|
|
1174
|
+
attempt \u2014 failed approach (prefer 'haive memory tried' for better structure)
|
|
1175
|
+
|
|
1176
|
+
Tips:
|
|
1177
|
+
\u2022 --paths anchors the memory to source files for staleness detection
|
|
1178
|
+
\u2022 --topic enables upsert: future adds with the same topic update the existing memory
|
|
1179
|
+
\u2022 In autopilot mode, memories go directly to validated with team scope by default
|
|
1180
|
+
|
|
1181
|
+
Examples:
|
|
1182
|
+
haive memory add --type gotcha --slug jpa-open-in-view --scope team \\\\
|
|
1183
|
+
--paths src/main/resources/application.properties \\\\
|
|
1184
|
+
--body "spring.jpa.open-in-view=false is intentional \u2014 do not re-enable."
|
|
1185
|
+
haive memory add --type convention --slug flyway-no-modify --topic flyway \\\\
|
|
1186
|
+
--scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
|
|
1187
|
+
`
|
|
1188
|
+
).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) => {
|
|
996
1189
|
const root = findProjectRoot9(opts.dir);
|
|
997
1190
|
const paths = resolveHaivePaths6(root);
|
|
998
1191
|
if (!existsSync7(paths.haiveDir)) {
|
|
@@ -1076,7 +1269,7 @@ TODO \u2014 write the memory body.
|
|
|
1076
1269
|
return;
|
|
1077
1270
|
}
|
|
1078
1271
|
}
|
|
1079
|
-
const frontmatter =
|
|
1272
|
+
const frontmatter = buildFrontmatter2({
|
|
1080
1273
|
type: opts.type,
|
|
1081
1274
|
slug: opts.slug,
|
|
1082
1275
|
scope,
|
|
@@ -1090,7 +1283,7 @@ TODO \u2014 write the memory body.
|
|
|
1090
1283
|
topic: opts.topic
|
|
1091
1284
|
});
|
|
1092
1285
|
const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1093
|
-
await
|
|
1286
|
+
await mkdir4(path7.dirname(file), { recursive: true });
|
|
1094
1287
|
if (existsSync7(file)) {
|
|
1095
1288
|
ui.error(`Memory already exists at ${file}`);
|
|
1096
1289
|
process.exitCode = 1;
|
|
@@ -1223,7 +1416,7 @@ function matchesFilters(loaded, opts) {
|
|
|
1223
1416
|
}
|
|
1224
1417
|
|
|
1225
1418
|
// src/commands/memory-promote.ts
|
|
1226
|
-
import { mkdir as
|
|
1419
|
+
import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
|
|
1227
1420
|
import { existsSync as existsSync9 } from "fs";
|
|
1228
1421
|
import path9 from "path";
|
|
1229
1422
|
import "commander";
|
|
@@ -1272,7 +1465,7 @@ function registerMemoryPromote(memory2) {
|
|
|
1272
1465
|
body: found.memory.body
|
|
1273
1466
|
};
|
|
1274
1467
|
const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
|
|
1275
|
-
await
|
|
1468
|
+
await mkdir5(path9.dirname(newPath), { recursive: true });
|
|
1276
1469
|
await writeFile5(newPath, serializeMemory3(updated), "utf8");
|
|
1277
1470
|
await unlink(found.filePath);
|
|
1278
1471
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
@@ -1732,12 +1925,12 @@ function registerMemoryHot(memory2) {
|
|
|
1732
1925
|
}
|
|
1733
1926
|
|
|
1734
1927
|
// src/commands/memory-tried.ts
|
|
1735
|
-
import { mkdir as
|
|
1928
|
+
import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
|
|
1736
1929
|
import { existsSync as existsSync16 } from "fs";
|
|
1737
1930
|
import path16 from "path";
|
|
1738
1931
|
import "commander";
|
|
1739
1932
|
import {
|
|
1740
|
-
buildFrontmatter as
|
|
1933
|
+
buildFrontmatter as buildFrontmatter3,
|
|
1741
1934
|
findProjectRoot as findProjectRoot18,
|
|
1742
1935
|
memoryFilePath as memoryFilePath3,
|
|
1743
1936
|
resolveHaivePaths as resolveHaivePaths15,
|
|
@@ -1745,8 +1938,21 @@ import {
|
|
|
1745
1938
|
} from "@hiveai/core";
|
|
1746
1939
|
function registerMemoryTried(memory2) {
|
|
1747
1940
|
memory2.command("tried").description(
|
|
1748
|
-
|
|
1749
|
-
|
|
1941
|
+
`Record a FAILED approach \u2014 prevents repeated mistakes in future sessions.
|
|
1942
|
+
|
|
1943
|
+
This is the most valuable type of negative knowledge. It surfaces FIRST in
|
|
1944
|
+
get_briefing so agents can't miss it. Auto-validated (no approval cycle).
|
|
1945
|
+
|
|
1946
|
+
Use this immediately when you try something and it fails.
|
|
1947
|
+
|
|
1948
|
+
Example:
|
|
1949
|
+
haive memory tried \\\\
|
|
1950
|
+
--what "importing X with ESM dynamic import" \\\\
|
|
1951
|
+
--why-failed "tsup bundles it as CJS, dynamic import fails at runtime" \\\\
|
|
1952
|
+
--instead "use static import in the entry file" \\\\
|
|
1953
|
+
--paths packages/cli/src/index.ts
|
|
1954
|
+
`
|
|
1955
|
+
).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) => {
|
|
1750
1956
|
const root = findProjectRoot18(opts.dir);
|
|
1751
1957
|
const paths = resolveHaivePaths15(root);
|
|
1752
1958
|
if (!existsSync16(paths.haiveDir)) {
|
|
@@ -1755,7 +1961,7 @@ function registerMemoryTried(memory2) {
|
|
|
1755
1961
|
return;
|
|
1756
1962
|
}
|
|
1757
1963
|
const slug = opts.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
|
|
1758
|
-
const baseFm =
|
|
1964
|
+
const baseFm = buildFrontmatter3({
|
|
1759
1965
|
type: "attempt",
|
|
1760
1966
|
slug,
|
|
1761
1967
|
scope: opts.scope,
|
|
@@ -1772,7 +1978,7 @@ function registerMemoryTried(memory2) {
|
|
|
1772
1978
|
}
|
|
1773
1979
|
const body = lines.join("\n") + "\n";
|
|
1774
1980
|
const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
1775
|
-
await
|
|
1981
|
+
await mkdir6(path16.dirname(file), { recursive: true });
|
|
1776
1982
|
if (existsSync16(file)) {
|
|
1777
1983
|
ui.error(`Memory already exists at ${file}`);
|
|
1778
1984
|
process.exitCode = 1;
|
|
@@ -2129,7 +2335,9 @@ import {
|
|
|
2129
2335
|
verifyAnchor as verifyAnchor2
|
|
2130
2336
|
} from "@hiveai/core";
|
|
2131
2337
|
function registerMemoryVerify(memory2) {
|
|
2132
|
-
memory2.command("verify").description(
|
|
2338
|
+
memory2.command("verify").description(
|
|
2339
|
+
"Check that memory anchor paths still exist in the current codebase.\n\n A memory is 'stale' when its anchored file or symbol was moved, deleted, or renamed.\n Stale memories are shown with a warning in get_briefing and should be updated or deleted.\n\n haive sync runs this automatically. Use this command for on-demand checks or in CI.\n\n CI recommendation: add 'haive memory verify' to your haive-sync.yml PR check job\n to catch stale memories before they reach main.\n\n Examples:\n haive memory verify # check all, report only\n haive memory verify --update # mark stale/fresh on disk\n haive memory verify --id 2026-04-28-gotcha-x # check one memory\n"
|
|
2340
|
+
).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) => {
|
|
2133
2341
|
const root = findProjectRoot25(opts.dir);
|
|
2134
2342
|
const paths = resolveHaivePaths22(root);
|
|
2135
2343
|
if (!existsSync23(paths.memoriesDir)) {
|
|
@@ -2268,18 +2476,178 @@ function registerMemoryImport(memory2) {
|
|
|
2268
2476
|
});
|
|
2269
2477
|
}
|
|
2270
2478
|
|
|
2271
|
-
// src/commands/memory-
|
|
2479
|
+
// src/commands/memory-import-changelog.ts
|
|
2272
2480
|
import { existsSync as existsSync25 } from "fs";
|
|
2273
|
-
import { writeFile as writeFile12 } from "fs/promises";
|
|
2481
|
+
import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
|
|
2274
2482
|
import path23 from "path";
|
|
2275
2483
|
import "commander";
|
|
2276
2484
|
import {
|
|
2277
|
-
|
|
2485
|
+
buildFrontmatter as buildFrontmatter4,
|
|
2278
2486
|
findProjectRoot as findProjectRoot27,
|
|
2487
|
+
resolveHaivePaths as resolveHaivePaths24,
|
|
2488
|
+
serializeMemory as serializeMemory10
|
|
2489
|
+
} from "@hiveai/core";
|
|
2490
|
+
function parseChangelog(content) {
|
|
2491
|
+
const entries = [];
|
|
2492
|
+
const versionRe = /^#{1,3}\s+(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/m;
|
|
2493
|
+
const sections = content.split(/^#{1,3}\s+/m).slice(1);
|
|
2494
|
+
for (const section of sections) {
|
|
2495
|
+
const versionMatch = section.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
|
|
2496
|
+
if (!versionMatch) continue;
|
|
2497
|
+
const version = versionMatch[1];
|
|
2498
|
+
const entry = {
|
|
2499
|
+
version,
|
|
2500
|
+
breaking: [],
|
|
2501
|
+
deprecated: [],
|
|
2502
|
+
removed: [],
|
|
2503
|
+
fixed: [],
|
|
2504
|
+
added: []
|
|
2505
|
+
};
|
|
2506
|
+
const subSections = section.split(/^#{2,4}\s+/m);
|
|
2507
|
+
for (const sub of subSections) {
|
|
2508
|
+
const firstLine = sub.split("\n")[0].toLowerCase().trim();
|
|
2509
|
+
const items = sub.split("\n").slice(1).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).map((l) => l.replace(/^[\s\-*]+/, "").trim()).filter(Boolean);
|
|
2510
|
+
if (/breaking/.test(firstLine)) {
|
|
2511
|
+
entry.breaking.push(...items);
|
|
2512
|
+
} else if (/deprecated/.test(firstLine)) {
|
|
2513
|
+
entry.deprecated.push(...items);
|
|
2514
|
+
} else if (/removed/.test(firstLine)) {
|
|
2515
|
+
entry.removed.push(...items);
|
|
2516
|
+
} else if (/fixed|bug/.test(firstLine)) {
|
|
2517
|
+
entry.fixed.push(...items);
|
|
2518
|
+
} else if (/added|new|feat/.test(firstLine)) {
|
|
2519
|
+
entry.added.push(...items);
|
|
2520
|
+
}
|
|
2521
|
+
for (const sub2 of subSections) {
|
|
2522
|
+
for (const line of sub2.split("\n")) {
|
|
2523
|
+
const breakingMatch = line.match(/BREAKING CHANGE[S]?:\s*(.+)/i);
|
|
2524
|
+
if (breakingMatch && !entry.breaking.includes(breakingMatch[1].trim())) {
|
|
2525
|
+
entry.breaking.push(breakingMatch[1].trim());
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (entry.breaking.length === 0) {
|
|
2531
|
+
for (const line of section.split("\n")) {
|
|
2532
|
+
if (/breaking|⚠|deprecated|removed/.test(line.toLowerCase())) {
|
|
2533
|
+
const item = line.replace(/^[\s\-*#]+/, "").trim();
|
|
2534
|
+
if (item) entry.breaking.push(item);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
const hasContent = entry.breaking.length > 0 || entry.deprecated.length > 0 || entry.removed.length > 0;
|
|
2539
|
+
if (hasContent) entries.push(entry);
|
|
2540
|
+
}
|
|
2541
|
+
void versionRe;
|
|
2542
|
+
return entries;
|
|
2543
|
+
}
|
|
2544
|
+
function registerMemoryImportChangelog(memory2) {
|
|
2545
|
+
memory2.command("import-changelog").description(
|
|
2546
|
+
"Import breaking changes from a CHANGELOG.md as hAIve memories.\n\n Parses Keep-a-Changelog and Angular commit format changelogs,\n extracts breaking changes, deprecations, and removals,\n and saves each version's changes as a gotcha memory.\n\n Examples:\n haive memory import-changelog --from node_modules/@company/sdk/CHANGELOG.md --package @company/sdk\n haive memory import-changelog --from CHANGELOG.md\n haive memory import-changelog --from CHANGELOG.md --versions 2.0.0,2.1.0\n"
|
|
2547
|
+
).requiredOption("--from <file>", "path to the CHANGELOG.md file").option("--package <name>", "name of the package (used in memory title and tags)").option("--scope <scope>", "memory scope: team | personal (default: team)", "team").option(
|
|
2548
|
+
"--versions <csv>",
|
|
2549
|
+
"only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
|
|
2550
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2551
|
+
const root = findProjectRoot27(opts.dir);
|
|
2552
|
+
const paths = resolveHaivePaths24(root);
|
|
2553
|
+
const changelogPath = path23.resolve(root, opts.fromChangelog);
|
|
2554
|
+
if (!existsSync25(changelogPath)) {
|
|
2555
|
+
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
2556
|
+
process.exitCode = 1;
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
const content = await readFile8(changelogPath, "utf8");
|
|
2560
|
+
let entries = parseChangelog(content);
|
|
2561
|
+
if (entries.length === 0) {
|
|
2562
|
+
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
if (opts.versions) {
|
|
2566
|
+
if (opts.versions === "latest") {
|
|
2567
|
+
entries = [entries[0]];
|
|
2568
|
+
} else {
|
|
2569
|
+
const requested = opts.versions.split(",").map((v) => v.trim());
|
|
2570
|
+
entries = entries.filter((e) => requested.includes(e.version));
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
const pkgName = opts.package ?? path23.basename(path23.dirname(changelogPath));
|
|
2574
|
+
const scope = opts.scope ?? "team";
|
|
2575
|
+
const teamDir = path23.join(paths.memoriesDir, scope);
|
|
2576
|
+
await mkdir7(teamDir, { recursive: true });
|
|
2577
|
+
let saved = 0;
|
|
2578
|
+
for (const entry of entries) {
|
|
2579
|
+
const lines = [];
|
|
2580
|
+
lines.push(`## ${pkgName} v${entry.version} \u2014 Breaking Changes & Deprecations
|
|
2581
|
+
`);
|
|
2582
|
+
if (entry.breaking.length > 0) {
|
|
2583
|
+
lines.push("### \u{1F534} Breaking Changes\n");
|
|
2584
|
+
for (const item of entry.breaking) lines.push(`- ${item}`);
|
|
2585
|
+
lines.push("");
|
|
2586
|
+
}
|
|
2587
|
+
if (entry.deprecated.length > 0) {
|
|
2588
|
+
lines.push("### \u{1F7E1} Deprecated\n");
|
|
2589
|
+
for (const item of entry.deprecated) lines.push(`- ${item}`);
|
|
2590
|
+
lines.push("");
|
|
2591
|
+
}
|
|
2592
|
+
if (entry.removed.length > 0) {
|
|
2593
|
+
lines.push("### \u26AB Removed\n");
|
|
2594
|
+
for (const item of entry.removed) lines.push(`- ${item}`);
|
|
2595
|
+
lines.push("");
|
|
2596
|
+
}
|
|
2597
|
+
lines.push(
|
|
2598
|
+
`**Source:** \`${path23.relative(root, changelogPath)}\`
|
|
2599
|
+
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
2600
|
+
);
|
|
2601
|
+
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
2602
|
+
const fm = buildFrontmatter4({
|
|
2603
|
+
type: "gotcha",
|
|
2604
|
+
slug,
|
|
2605
|
+
scope,
|
|
2606
|
+
status: "validated",
|
|
2607
|
+
tags: [
|
|
2608
|
+
"changelog",
|
|
2609
|
+
"breaking-change",
|
|
2610
|
+
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
2611
|
+
`v${entry.version}`
|
|
2612
|
+
],
|
|
2613
|
+
paths: [path23.relative(root, changelogPath)],
|
|
2614
|
+
topic: `changelog-${pkgName}-${entry.version}`
|
|
2615
|
+
});
|
|
2616
|
+
await writeFile12(
|
|
2617
|
+
path23.join(teamDir, `${fm.id}.md`),
|
|
2618
|
+
serializeMemory10({ frontmatter: fm, body: lines.join("\n") }),
|
|
2619
|
+
"utf8"
|
|
2620
|
+
);
|
|
2621
|
+
console.log(ui.green(` \u2713 ${fm.id}`));
|
|
2622
|
+
saved++;
|
|
2623
|
+
}
|
|
2624
|
+
console.log(
|
|
2625
|
+
`
|
|
2626
|
+
${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${pkgName}`)}`
|
|
2627
|
+
);
|
|
2628
|
+
if (saved > 0) {
|
|
2629
|
+
console.log(
|
|
2630
|
+
ui.dim(` Memories saved to .ai/memories/${scope}/`)
|
|
2631
|
+
);
|
|
2632
|
+
console.log(
|
|
2633
|
+
ui.dim(` Run \`haive briefing --task "update ${pkgName}"\` to see them in context.`)
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// src/commands/memory-digest.ts
|
|
2640
|
+
import { existsSync as existsSync26 } from "fs";
|
|
2641
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
2642
|
+
import path24 from "path";
|
|
2643
|
+
import "commander";
|
|
2644
|
+
import {
|
|
2645
|
+
deriveConfidence as deriveConfidence4,
|
|
2646
|
+
findProjectRoot as findProjectRoot28,
|
|
2279
2647
|
getUsage as getUsage8,
|
|
2280
2648
|
loadMemoriesFromDir as loadMemoriesFromDir5,
|
|
2281
2649
|
loadUsageIndex as loadUsageIndex10,
|
|
2282
|
-
resolveHaivePaths as
|
|
2650
|
+
resolveHaivePaths as resolveHaivePaths25
|
|
2283
2651
|
} from "@hiveai/core";
|
|
2284
2652
|
var CONFIDENCE_EMOJI = {
|
|
2285
2653
|
unverified: "\u2B1C",
|
|
@@ -2290,11 +2658,11 @@ var CONFIDENCE_EMOJI = {
|
|
|
2290
2658
|
};
|
|
2291
2659
|
function registerMemoryDigest(program2) {
|
|
2292
2660
|
program2.command("digest").description(
|
|
2293
|
-
"Generate a Markdown review digest of recently added
|
|
2294
|
-
).option("--days <n>", "look-back window in days", "7").option("--scope <scope>", "personal | team | module | all", "team").option("--out <file>", "write digest to a file instead of stdout").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2295
|
-
const root =
|
|
2296
|
-
const paths =
|
|
2297
|
-
if (!
|
|
2661
|
+
"Generate a Markdown review digest of recently added or updated memories.\n\n Groups memories by type, shows confidence, status, read count, and anchor info.\n Each memory has action checkboxes (approve / reject / keep as-is) for peer review.\n\n Use this to do a bulk weekly review of team memories, or share with teammates\n as a pull-request attachment so humans can validate what the AI captured.\n\n Examples:\n haive memory digest # last 7 days, team scope\n haive memory digest --days 30 --scope all # last 30 days, all scopes\n haive memory digest --out review.md # write to file\n"
|
|
2662
|
+
).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) => {
|
|
2663
|
+
const root = findProjectRoot28(opts.dir);
|
|
2664
|
+
const paths = resolveHaivePaths25(root);
|
|
2665
|
+
if (!existsSync26(paths.memoriesDir)) {
|
|
2298
2666
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
2299
2667
|
process.exitCode = 1;
|
|
2300
2668
|
return;
|
|
@@ -2366,8 +2734,8 @@ function registerMemoryDigest(program2) {
|
|
|
2366
2734
|
);
|
|
2367
2735
|
const digest = lines.join("\n");
|
|
2368
2736
|
if (opts.out) {
|
|
2369
|
-
const outPath =
|
|
2370
|
-
await
|
|
2737
|
+
const outPath = path24.resolve(process.cwd(), opts.out);
|
|
2738
|
+
await writeFile13(outPath, digest, "utf8");
|
|
2371
2739
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
2372
2740
|
} else {
|
|
2373
2741
|
console.log(digest);
|
|
@@ -2376,17 +2744,17 @@ function registerMemoryDigest(program2) {
|
|
|
2376
2744
|
}
|
|
2377
2745
|
|
|
2378
2746
|
// src/commands/session-end.ts
|
|
2379
|
-
import { writeFile as
|
|
2380
|
-
import { existsSync as
|
|
2381
|
-
import
|
|
2747
|
+
import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
|
|
2748
|
+
import { existsSync as existsSync27 } from "fs";
|
|
2749
|
+
import path25 from "path";
|
|
2382
2750
|
import "commander";
|
|
2383
2751
|
import {
|
|
2384
|
-
buildFrontmatter as
|
|
2385
|
-
findProjectRoot as
|
|
2752
|
+
buildFrontmatter as buildFrontmatter5,
|
|
2753
|
+
findProjectRoot as findProjectRoot29,
|
|
2386
2754
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
2387
2755
|
memoryFilePath as memoryFilePath4,
|
|
2388
|
-
resolveHaivePaths as
|
|
2389
|
-
serializeMemory as
|
|
2756
|
+
resolveHaivePaths as resolveHaivePaths26,
|
|
2757
|
+
serializeMemory as serializeMemory11
|
|
2390
2758
|
} from "@hiveai/core";
|
|
2391
2759
|
function buildRecapBody(opts) {
|
|
2392
2760
|
const lines = [];
|
|
@@ -2417,10 +2785,27 @@ function recapTopic(scope, module) {
|
|
|
2417
2785
|
return module ? `session-recap-${scope}-${module}` : `session-recap-${scope}`;
|
|
2418
2786
|
}
|
|
2419
2787
|
function registerSessionEnd(session2) {
|
|
2420
|
-
session2.command("end").description(
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2788
|
+
session2.command("end").description(
|
|
2789
|
+
`Save an end-of-session recap so the NEXT session starts with fresh context.
|
|
2790
|
+
|
|
2791
|
+
One recap per scope is kept and updated in-place (topic-upsert). The next
|
|
2792
|
+
session's get_briefing (or haive briefing) shows it at the very top.
|
|
2793
|
+
|
|
2794
|
+
In autopilot mode, a minimal recap saves automatically on MCP server exit.
|
|
2795
|
+
Calling this manually produces a richer, more actionable recap.
|
|
2796
|
+
|
|
2797
|
+
Example:
|
|
2798
|
+
haive session end \\\\
|
|
2799
|
+
--goal "Add Stripe webhook handler" \\\\
|
|
2800
|
+
--accomplished "Implemented webhook endpoint, added idempotency key" \\\\
|
|
2801
|
+
--discoveries "Missing .env.example entry for STRIPE_WEBHOOK_SECRET" \\\\
|
|
2802
|
+
--files src/payments/WebhookController.ts,src/payments/WebhookService.ts \\\\
|
|
2803
|
+
--next "Add integration tests for webhook signature validation"
|
|
2804
|
+
`
|
|
2805
|
+
).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) => {
|
|
2806
|
+
const root = findProjectRoot29(opts.dir);
|
|
2807
|
+
const paths = resolveHaivePaths26(root);
|
|
2808
|
+
if (!existsSync27(paths.haiveDir)) {
|
|
2424
2809
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2425
2810
|
process.exitCode = 1;
|
|
2426
2811
|
return;
|
|
@@ -2429,12 +2814,12 @@ function registerSessionEnd(session2) {
|
|
|
2429
2814
|
const body = buildRecapBody(opts);
|
|
2430
2815
|
const topic = recapTopic(scope, opts.module);
|
|
2431
2816
|
const filesTouched = parseCsv5(opts.files);
|
|
2432
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
2817
|
+
const missingPaths = filesTouched.filter((p) => !existsSync27(path25.resolve(root, p)));
|
|
2433
2818
|
if (missingPaths.length > 0) {
|
|
2434
2819
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
2435
2820
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
2436
2821
|
}
|
|
2437
|
-
if (
|
|
2822
|
+
if (existsSync27(paths.memoriesDir)) {
|
|
2438
2823
|
const existing = await loadMemoriesFromDir6(paths.memoriesDir);
|
|
2439
2824
|
const topicMatch = existing.find(
|
|
2440
2825
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
@@ -2450,13 +2835,13 @@ function registerSessionEnd(session2) {
|
|
|
2450
2835
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
2451
2836
|
}
|
|
2452
2837
|
};
|
|
2453
|
-
await
|
|
2838
|
+
await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
|
|
2454
2839
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
2455
|
-
ui.info(`id=${fm.id} file=${
|
|
2840
|
+
ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
|
|
2456
2841
|
return;
|
|
2457
2842
|
}
|
|
2458
2843
|
}
|
|
2459
|
-
const frontmatter =
|
|
2844
|
+
const frontmatter = buildFrontmatter5({
|
|
2460
2845
|
type: "session_recap",
|
|
2461
2846
|
slug: "recap",
|
|
2462
2847
|
scope,
|
|
@@ -2467,10 +2852,10 @@ function registerSessionEnd(session2) {
|
|
|
2467
2852
|
status: "validated"
|
|
2468
2853
|
});
|
|
2469
2854
|
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2470
|
-
await
|
|
2471
|
-
await
|
|
2855
|
+
await mkdir8(path25.dirname(file), { recursive: true });
|
|
2856
|
+
await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
|
|
2472
2857
|
ui.success(`Session recap created`);
|
|
2473
|
-
ui.info(`id=${frontmatter.id} scope=${scope} file=${
|
|
2858
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path25.relative(root, file)}`);
|
|
2474
2859
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
2475
2860
|
});
|
|
2476
2861
|
}
|
|
@@ -2479,9 +2864,430 @@ function parseCsv5(value) {
|
|
|
2479
2864
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2480
2865
|
}
|
|
2481
2866
|
|
|
2867
|
+
// src/commands/snapshot.ts
|
|
2868
|
+
import { existsSync as existsSync28 } from "fs";
|
|
2869
|
+
import { readdir } from "fs/promises";
|
|
2870
|
+
import path26 from "path";
|
|
2871
|
+
import "commander";
|
|
2872
|
+
import {
|
|
2873
|
+
diffContract,
|
|
2874
|
+
findProjectRoot as findProjectRoot30,
|
|
2875
|
+
loadConfig as loadConfig2,
|
|
2876
|
+
resolveHaivePaths as resolveHaivePaths27,
|
|
2877
|
+
snapshotContract
|
|
2878
|
+
} from "@hiveai/core";
|
|
2879
|
+
function registerSnapshot(program2) {
|
|
2880
|
+
program2.command("snapshot").description(
|
|
2881
|
+
`Take or compare an API contract snapshot to detect breaking changes.
|
|
2882
|
+
|
|
2883
|
+
A snapshot captures the structure of a contract file (endpoints, types, fields).
|
|
2884
|
+
Running 'haive sync' automatically checks all configured contracts.
|
|
2885
|
+
This command lets you snapshot or diff a single contract on demand.
|
|
2886
|
+
|
|
2887
|
+
Supported formats: openapi, graphql, proto, typescript, json-schema
|
|
2888
|
+
|
|
2889
|
+
Examples:
|
|
2890
|
+
haive snapshot --contract docs/openapi.yaml --name payment-api
|
|
2891
|
+
haive snapshot --diff --name payment-api
|
|
2892
|
+
haive snapshot --list
|
|
2893
|
+
|
|
2894
|
+
To monitor contracts automatically on haive sync, add them to haive.config.json:
|
|
2895
|
+
{ "contractFiles": [{ "name": "payment-api", "path": "docs/openapi.yaml", "format": "openapi" }] }
|
|
2896
|
+
`
|
|
2897
|
+
).option("--contract <file>", "path to the contract file to snapshot (relative to project root)").option("--name <name>", "name for this contract (used in the lock file and memories)").option(
|
|
2898
|
+
"--format <format>",
|
|
2899
|
+
"contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
|
|
2900
|
+
).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) => {
|
|
2901
|
+
const root = findProjectRoot30(opts.dir);
|
|
2902
|
+
const paths = resolveHaivePaths27(root);
|
|
2903
|
+
if (!existsSync28(paths.haiveDir)) {
|
|
2904
|
+
ui.error("No .ai/ found. Run `haive init` first.");
|
|
2905
|
+
process.exitCode = 1;
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (opts.list) {
|
|
2909
|
+
const contractsDir = path26.join(paths.haiveDir, "contracts");
|
|
2910
|
+
if (!existsSync28(contractsDir)) {
|
|
2911
|
+
console.log(ui.dim("No contract snapshots found."));
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
const files = (await readdir(contractsDir)).filter(
|
|
2915
|
+
(f) => f.endsWith(".lock") && !f.startsWith("deps-")
|
|
2916
|
+
);
|
|
2917
|
+
if (files.length === 0) {
|
|
2918
|
+
console.log(ui.dim("No contract snapshots found."));
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
console.log(ui.bold(`Contract snapshots (${files.length}):`));
|
|
2922
|
+
for (const f of files) {
|
|
2923
|
+
const name2 = f.replace(".lock", "");
|
|
2924
|
+
console.log(` ${name2}`);
|
|
2925
|
+
}
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
if (opts.diff) {
|
|
2929
|
+
if (!opts.name) {
|
|
2930
|
+
const config2 = await loadConfig2(paths);
|
|
2931
|
+
const contracts = config2.contractFiles ?? [];
|
|
2932
|
+
if (contracts.length === 0) {
|
|
2933
|
+
ui.error("--diff requires --name, or configure contractFiles in haive.config.json");
|
|
2934
|
+
process.exitCode = 1;
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
for (const contract3 of contracts) {
|
|
2938
|
+
await runDiff(root, paths.haiveDir, contract3);
|
|
2939
|
+
}
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const config = await loadConfig2(paths);
|
|
2943
|
+
const configured = (config.contractFiles ?? []).find((c) => c.name === opts.name);
|
|
2944
|
+
if (!configured && !opts.contract) {
|
|
2945
|
+
ui.error(
|
|
2946
|
+
`Contract "${opts.name}" not found in haive.config.json and --contract not provided.`
|
|
2947
|
+
);
|
|
2948
|
+
process.exitCode = 1;
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const contract2 = configured ?? {
|
|
2952
|
+
name: opts.name,
|
|
2953
|
+
path: opts.contract,
|
|
2954
|
+
format: detectFormat(opts.contract ?? "") ?? "openapi"
|
|
2955
|
+
};
|
|
2956
|
+
await runDiff(root, paths.haiveDir, contract2);
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (!opts.contract) {
|
|
2960
|
+
ui.error("Provide --contract <file> or use --diff / --list.");
|
|
2961
|
+
process.exitCode = 1;
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
const contractPath = opts.contract;
|
|
2965
|
+
const name = opts.name ?? path26.basename(contractPath, path26.extname(contractPath));
|
|
2966
|
+
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
2967
|
+
const contract = { name, path: contractPath, format };
|
|
2968
|
+
try {
|
|
2969
|
+
const snapshot = await snapshotContract(root, paths.haiveDir, contract);
|
|
2970
|
+
console.log(ui.green(`\u2713 snapshot saved: ${name}`));
|
|
2971
|
+
if (snapshot.endpoints) {
|
|
2972
|
+
console.log(ui.dim(` ${snapshot.endpoints.length} endpoint(s) captured`));
|
|
2973
|
+
}
|
|
2974
|
+
if (snapshot.types) {
|
|
2975
|
+
console.log(ui.dim(` ${snapshot.types.length} type(s) captured`));
|
|
2976
|
+
}
|
|
2977
|
+
console.log(ui.dim(` lock: .ai/contracts/${name}.lock`));
|
|
2978
|
+
console.log(ui.dim(" Next haive sync will detect changes automatically."));
|
|
2979
|
+
console.log(
|
|
2980
|
+
ui.dim(
|
|
2981
|
+
` Tip: add to haive.config.json \u2192 contractFiles to monitor automatically:
|
|
2982
|
+
{ "name": "${name}", "path": "${contractPath}", "format": "${format}" }`
|
|
2983
|
+
)
|
|
2984
|
+
);
|
|
2985
|
+
} catch (err) {
|
|
2986
|
+
ui.error(String(err));
|
|
2987
|
+
process.exitCode = 1;
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
async function runDiff(root, haiveDir, contract) {
|
|
2992
|
+
try {
|
|
2993
|
+
const result = await diffContract(root, haiveDir, contract);
|
|
2994
|
+
if (result.unchanged) {
|
|
2995
|
+
console.log(ui.green(`\u2713 ${contract.name}: no changes detected`));
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const breaking = result.changes.filter((c) => c.severity === "breaking");
|
|
2999
|
+
const additive = result.changes.filter((c) => c.severity === "additive");
|
|
3000
|
+
const unknown = result.changes.filter((c) => c.severity === "unknown");
|
|
3001
|
+
console.log(
|
|
3002
|
+
ui.bold(`Contract diff: ${contract.name}`) + ` \u2014 ${breaking.length} breaking \xB7 ${additive.length} additive \xB7 ${unknown.length} unknown`
|
|
3003
|
+
);
|
|
3004
|
+
for (const c of result.changes) {
|
|
3005
|
+
const icon = c.severity === "breaking" ? "\u{1F534}" : c.severity === "additive" ? "\u{1F7E2}" : "\u{1F7E1}";
|
|
3006
|
+
console.log(` ${icon} ${c.description}`);
|
|
3007
|
+
}
|
|
3008
|
+
if (breaking.length > 0) {
|
|
3009
|
+
console.log(
|
|
3010
|
+
ui.yellow(
|
|
3011
|
+
"\n \u26A0 Breaking changes detected \u2014 run `haive sync` to create a gotcha memory for your team."
|
|
3012
|
+
)
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
} catch (err) {
|
|
3016
|
+
ui.error(`diff failed for ${contract.name}: ${String(err)}`);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
function detectFormat(filePath) {
|
|
3020
|
+
const ext = path26.extname(filePath).toLowerCase();
|
|
3021
|
+
const base = path26.basename(filePath).toLowerCase();
|
|
3022
|
+
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
3023
|
+
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
3024
|
+
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
3025
|
+
return "openapi";
|
|
3026
|
+
}
|
|
3027
|
+
if (ext === ".graphql" || ext === ".gql") return "graphql";
|
|
3028
|
+
if (ext === ".proto") return "proto";
|
|
3029
|
+
if (ext === ".d.ts" || ext === ".ts") return "typescript";
|
|
3030
|
+
return null;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// src/commands/hub.ts
|
|
3034
|
+
import { existsSync as existsSync29 } from "fs";
|
|
3035
|
+
import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile15, copyFile } from "fs/promises";
|
|
3036
|
+
import path27 from "path";
|
|
3037
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3038
|
+
import "commander";
|
|
3039
|
+
import {
|
|
3040
|
+
findProjectRoot as findProjectRoot31,
|
|
3041
|
+
loadConfig as loadConfig3,
|
|
3042
|
+
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
3043
|
+
resolveHaivePaths as resolveHaivePaths28,
|
|
3044
|
+
saveConfig as saveConfig2,
|
|
3045
|
+
serializeMemory as serializeMemory12
|
|
3046
|
+
} from "@hiveai/core";
|
|
3047
|
+
function registerHub(program2) {
|
|
3048
|
+
const hub = program2.command("hub").description(
|
|
3049
|
+
'Manage a shared team-knowledge hub \u2014 a central repo that multiple projects contribute to and pull from.\n\n The hub is a plain git repo with a .ai/ directory. Each project pushes its\n `shared`-scoped memories to the hub and pulls from all other projects.\n\n Setup:\n 1. haive hub init /path/to/team-hub\n 2. Add hubPath to .ai/haive.config.json: { "hubPath": "../team-hub" }\n 3. haive hub push \u2014 publish your shared memories\n 4. haive hub pull \u2014 import other projects\' shared memories\n\n Or configure in haive.config.json and haive sync handles it automatically.\n'
|
|
3050
|
+
);
|
|
3051
|
+
hub.action(() => hub.help());
|
|
3052
|
+
hub.command("init <hubPath>").description(
|
|
3053
|
+
"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"
|
|
3054
|
+
).action(async (hubPath) => {
|
|
3055
|
+
const absPath = path27.resolve(hubPath);
|
|
3056
|
+
await mkdir9(absPath, { recursive: true });
|
|
3057
|
+
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
3058
|
+
if (gitCheck.status !== 0) {
|
|
3059
|
+
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
3060
|
+
if (init.status !== 0) {
|
|
3061
|
+
ui.error(`git init failed: ${init.stderr}`);
|
|
3062
|
+
process.exitCode = 1;
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const sharedDir = path27.join(absPath, ".ai", "memories", "shared");
|
|
3067
|
+
await mkdir9(sharedDir, { recursive: true });
|
|
3068
|
+
await writeFile15(
|
|
3069
|
+
path27.join(absPath, ".ai", "README.md"),
|
|
3070
|
+
`# hAIve Team Knowledge Hub
|
|
3071
|
+
|
|
3072
|
+
This repo is a shared knowledge hub for hAIve.
|
|
3073
|
+
|
|
3074
|
+
Each project contributes its \`shared\`-scoped memories here.
|
|
3075
|
+
Other projects pull from it via \`haive hub pull\`.
|
|
3076
|
+
|
|
3077
|
+
## Structure
|
|
3078
|
+
|
|
3079
|
+
\`\`.ai/memories/shared/<project-name>/\`
|
|
3080
|
+
|
|
3081
|
+
## Usage
|
|
3082
|
+
|
|
3083
|
+
\`\`\`bash
|
|
3084
|
+
haive hub push # publish from a project
|
|
3085
|
+
haive hub pull # import into a project
|
|
3086
|
+
\`\`\`
|
|
3087
|
+
`,
|
|
3088
|
+
"utf8"
|
|
3089
|
+
);
|
|
3090
|
+
await writeFile15(
|
|
3091
|
+
path27.join(absPath, ".gitignore"),
|
|
3092
|
+
".ai/.cache/\n.ai/memories/personal/\n",
|
|
3093
|
+
"utf8"
|
|
3094
|
+
);
|
|
3095
|
+
spawnSync3("git", ["add", "."], { cwd: absPath });
|
|
3096
|
+
spawnSync3("git", ["commit", "-m", "chore: initialize hAIve team-knowledge hub"], {
|
|
3097
|
+
cwd: absPath,
|
|
3098
|
+
encoding: "utf8"
|
|
3099
|
+
});
|
|
3100
|
+
console.log(ui.green(`\u2713 Hub initialized at ${absPath}`));
|
|
3101
|
+
console.log(
|
|
3102
|
+
ui.dim(
|
|
3103
|
+
`
|
|
3104
|
+
Next steps:
|
|
3105
|
+
1. Add hubPath to your project's .ai/haive.config.json:
|
|
3106
|
+
{ "hubPath": "${path27.relative(process.cwd(), absPath)}" }
|
|
3107
|
+
2. Run \`haive hub push\` to publish your shared memories
|
|
3108
|
+
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
3109
|
+
`
|
|
3110
|
+
)
|
|
3111
|
+
);
|
|
3112
|
+
});
|
|
3113
|
+
hub.command("push").description(
|
|
3114
|
+
`Push this project's shared-scoped memories to the hub.
|
|
3115
|
+
|
|
3116
|
+
Copies all memories with scope=shared to hub/.ai/memories/shared/<project-name>/.
|
|
3117
|
+
Optionally commits to the hub repo.
|
|
3118
|
+
|
|
3119
|
+
Examples:
|
|
3120
|
+
haive hub push
|
|
3121
|
+
haive hub push --commit
|
|
3122
|
+
haive hub push --commit --message "feat: add payment API contract memories"
|
|
3123
|
+
`
|
|
3124
|
+
).option("-d, --dir <dir>", "project root").option("--commit", "auto-commit to the hub repo after pushing").option("--message <msg>", "commit message for the hub (used with --commit)").action(async (opts) => {
|
|
3125
|
+
const root = findProjectRoot31(opts.dir);
|
|
3126
|
+
const paths = resolveHaivePaths28(root);
|
|
3127
|
+
const config = await loadConfig3(paths);
|
|
3128
|
+
if (!config.hubPath) {
|
|
3129
|
+
ui.error(
|
|
3130
|
+
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
3131
|
+
);
|
|
3132
|
+
process.exitCode = 1;
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const hubRoot = path27.resolve(root, config.hubPath);
|
|
3136
|
+
if (!existsSync29(hubRoot)) {
|
|
3137
|
+
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
3138
|
+
process.exitCode = 1;
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const projectName = path27.basename(root);
|
|
3142
|
+
const destDir = path27.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
3143
|
+
await mkdir9(destDir, { recursive: true });
|
|
3144
|
+
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3145
|
+
const shared = all.filter(
|
|
3146
|
+
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
3147
|
+
!memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
3148
|
+
);
|
|
3149
|
+
if (shared.length === 0) {
|
|
3150
|
+
ui.warn(
|
|
3151
|
+
`No shared-scoped memories found. Create memories with scope=shared to push to the hub.
|
|
3152
|
+
Example: haive memory add --type architecture --slug my-api --scope shared --body "..."
|
|
3153
|
+
Or with MCP: mem_save({ scope: 'shared', ... })`
|
|
3154
|
+
);
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
let pushed = 0;
|
|
3158
|
+
for (const { memory: memory2 } of shared) {
|
|
3159
|
+
const fm = memory2.frontmatter;
|
|
3160
|
+
const fileName = `${fm.id}.md`;
|
|
3161
|
+
const destPath = path27.join(destDir, fileName);
|
|
3162
|
+
await writeFile15(destPath, serializeMemory12(memory2), "utf8");
|
|
3163
|
+
pushed++;
|
|
3164
|
+
}
|
|
3165
|
+
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
3166
|
+
console.log(ui.dim(` Location: ${destDir}`));
|
|
3167
|
+
if (opts.commit) {
|
|
3168
|
+
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
3169
|
+
spawnSync3("git", ["add", path27.join(".ai", "memories", "shared", projectName)], {
|
|
3170
|
+
cwd: hubRoot
|
|
3171
|
+
});
|
|
3172
|
+
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
3173
|
+
cwd: hubRoot,
|
|
3174
|
+
encoding: "utf8"
|
|
3175
|
+
});
|
|
3176
|
+
if (commit.status === 0) {
|
|
3177
|
+
console.log(ui.green(`\u2713 Committed to hub: "${message}"`));
|
|
3178
|
+
} else if (commit.stdout?.includes("nothing to commit")) {
|
|
3179
|
+
console.log(ui.dim(" Hub already up to date \u2014 nothing to commit."));
|
|
3180
|
+
} else {
|
|
3181
|
+
ui.warn(`git commit in hub failed: ${commit.stderr}`);
|
|
3182
|
+
}
|
|
3183
|
+
} else {
|
|
3184
|
+
console.log(
|
|
3185
|
+
ui.dim(
|
|
3186
|
+
" Tip: use --commit to auto-commit to the hub repo, or commit manually."
|
|
3187
|
+
)
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
hub.command("pull").description(
|
|
3192
|
+
"Pull shared memories from the hub into this project.\n\n Imports all memories from hub/.ai/memories/shared/ EXCEPT this project's own.\n Imported memories land in .ai/memories/shared/<source-project-name>/.\n\n Examples:\n haive hub pull\n"
|
|
3193
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3194
|
+
const root = findProjectRoot31(opts.dir);
|
|
3195
|
+
const paths = resolveHaivePaths28(root);
|
|
3196
|
+
const config = await loadConfig3(paths);
|
|
3197
|
+
if (!config.hubPath) {
|
|
3198
|
+
ui.error(
|
|
3199
|
+
'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
|
|
3200
|
+
);
|
|
3201
|
+
process.exitCode = 1;
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const hubRoot = path27.resolve(root, config.hubPath);
|
|
3205
|
+
const hubSharedDir = path27.join(hubRoot, ".ai", "memories", "shared");
|
|
3206
|
+
if (!existsSync29(hubSharedDir)) {
|
|
3207
|
+
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
const projectName = path27.basename(root);
|
|
3211
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
3212
|
+
const projectDirs = (await readdir2(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
3213
|
+
if (projectDirs.length === 0) {
|
|
3214
|
+
console.log(ui.dim("No other projects have pushed to the hub yet."));
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
let totalImported = 0;
|
|
3218
|
+
let totalUpdated = 0;
|
|
3219
|
+
for (const sourceName of projectDirs) {
|
|
3220
|
+
const sourceDir = path27.join(hubSharedDir, sourceName);
|
|
3221
|
+
const destDir = path27.join(paths.memoriesDir, "shared", sourceName);
|
|
3222
|
+
await mkdir9(destDir, { recursive: true });
|
|
3223
|
+
const sourceFiles = (await readdir2(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
3224
|
+
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
3225
|
+
const existingInDest = await loadDir(destDir);
|
|
3226
|
+
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
3227
|
+
for (const file of sourceFiles) {
|
|
3228
|
+
const srcPath = path27.join(sourceDir, file);
|
|
3229
|
+
const destPath = path27.join(destDir, file);
|
|
3230
|
+
const fileContent = await readFile9(srcPath, "utf8");
|
|
3231
|
+
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
3232
|
+
if (!alreadyTagged) {
|
|
3233
|
+
await copyFile(srcPath, destPath);
|
|
3234
|
+
} else {
|
|
3235
|
+
await copyFile(srcPath, destPath);
|
|
3236
|
+
}
|
|
3237
|
+
const memId = file.replace(".md", "");
|
|
3238
|
+
if (existingIds.has(memId)) {
|
|
3239
|
+
totalUpdated++;
|
|
3240
|
+
} else {
|
|
3241
|
+
totalImported++;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
console.log(
|
|
3245
|
+
ui.dim(` [${sourceName}]: ${sourceFiles.length} memor${sourceFiles.length === 1 ? "y" : "ies"} synced`)
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
console.log(
|
|
3249
|
+
ui.green(`\u2713 Hub pull complete: ${totalImported} new \xB7 ${totalUpdated} updated`)
|
|
3250
|
+
);
|
|
3251
|
+
});
|
|
3252
|
+
hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3253
|
+
const root = findProjectRoot31(opts.dir);
|
|
3254
|
+
const paths = resolveHaivePaths28(root);
|
|
3255
|
+
const config = await loadConfig3(paths);
|
|
3256
|
+
console.log(ui.bold("Hub status"));
|
|
3257
|
+
console.log(
|
|
3258
|
+
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
3259
|
+
);
|
|
3260
|
+
const sharedDir = path27.join(paths.memoriesDir, "shared");
|
|
3261
|
+
if (existsSync29(sharedDir)) {
|
|
3262
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
3263
|
+
const sources = (await readdir2(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3264
|
+
console.log(`
|
|
3265
|
+
Imported from ${sources.length} source(s):`);
|
|
3266
|
+
for (const src of sources) {
|
|
3267
|
+
const files = (await readdir2(path27.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
3268
|
+
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
3269
|
+
}
|
|
3270
|
+
} else {
|
|
3271
|
+
console.log(ui.dim(" No imported shared memories yet."));
|
|
3272
|
+
}
|
|
3273
|
+
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
3274
|
+
const outgoing = all.filter(
|
|
3275
|
+
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
3276
|
+
);
|
|
3277
|
+
console.log(`
|
|
3278
|
+
This project's shared memories (ready to push): ${outgoing.length}`);
|
|
3279
|
+
if (outgoing.length > 0) {
|
|
3280
|
+
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
3281
|
+
}
|
|
3282
|
+
void readFile9;
|
|
3283
|
+
void writeFile15;
|
|
3284
|
+
void saveConfig2;
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
|
|
2482
3288
|
// src/index.ts
|
|
2483
|
-
var program = new
|
|
2484
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
3289
|
+
var program = new Command32();
|
|
3290
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.4.0");
|
|
2485
3291
|
registerInit(program);
|
|
2486
3292
|
registerMcp(program);
|
|
2487
3293
|
registerBriefing(program);
|
|
@@ -2509,9 +3315,12 @@ registerMemoryUpdate(memory);
|
|
|
2509
3315
|
registerMemoryHot(memory);
|
|
2510
3316
|
registerMemoryTried(memory);
|
|
2511
3317
|
registerMemoryImport(memory);
|
|
3318
|
+
registerMemoryImportChangelog(memory);
|
|
2512
3319
|
registerMemoryDigest(memory);
|
|
2513
3320
|
var session = program.command("session").description("Manage session lifecycle");
|
|
2514
3321
|
registerSessionEnd(session);
|
|
3322
|
+
registerSnapshot(program);
|
|
3323
|
+
registerHub(program);
|
|
2515
3324
|
program.parseAsync(process.argv).catch((err) => {
|
|
2516
3325
|
if (isZodError(err)) {
|
|
2517
3326
|
for (const issue of err.issues) {
|