@hiveai/cli 0.3.3 → 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/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 Command29 } from "commander";
4
+ import { Command as Command32 } from "commander";
5
5
 
6
6
  // src/commands/briefing.ts
7
7
  import { existsSync } from "fs";
@@ -462,6 +462,28 @@ jobs:
462
462
  issue_number: context.issue.number,
463
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.\`
464
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.
465
487
  `;
466
488
  function registerInit(program2) {
467
489
  program2.command("init").description(
@@ -679,12 +701,13 @@ function locateMcpBin() {
679
701
 
680
702
  // src/commands/sync.ts
681
703
  import { spawnSync as spawnSync2 } from "child_process";
682
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
704
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
683
705
  import { existsSync as existsSync6 } from "fs";
684
706
  import path6 from "path";
685
707
  import "commander";
686
708
  import {
687
709
  DEFAULT_AUTO_PROMOTE_RULE,
710
+ buildFrontmatter,
688
711
  findProjectRoot as findProjectRoot8,
689
712
  getUsage,
690
713
  isAutoPromoteEligible,
@@ -693,9 +716,13 @@ import {
693
716
  loadConfig,
694
717
  loadMemoriesFromDir as loadMemoriesFromDir2,
695
718
  loadUsageIndex,
719
+ pullCrossRepoSources,
696
720
  resolveHaivePaths as resolveHaivePaths5,
721
+ resolveManifestFiles,
697
722
  serializeMemory,
698
- verifyAnchor
723
+ trackDependencies,
724
+ verifyAnchor,
725
+ watchContracts
699
726
  } from "@hiveai/core";
700
727
  var BRIDGE_START = "<!-- haive:memories-start -->";
701
728
  var BRIDGE_END = "<!-- haive:memories-end -->";
@@ -708,7 +735,7 @@ function registerSync(program2) {
708
735
  ).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
709
736
  "--inject-bridge",
710
737
  "inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
711
- ).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) => {
712
739
  const root = findProjectRoot8(opts.dir);
713
740
  const paths = resolveHaivePaths5(root);
714
741
  if (!existsSync6(paths.memoriesDir)) {
@@ -878,6 +905,124 @@ function registerSync(program2) {
878
905
  }
879
906
  }
880
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
+ }
881
1026
  const existingMap = await loadCodeMap2(paths);
882
1027
  if (existingMap) {
883
1028
  const mapAge = new Date(existingMap.generated_at).getTime();
@@ -1003,12 +1148,12 @@ function collectSinceChanges(root, ref) {
1003
1148
 
1004
1149
  // src/commands/memory-add.ts
1005
1150
  import { createHash } from "crypto";
1006
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1151
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1007
1152
  import { existsSync as existsSync7 } from "fs";
1008
1153
  import path7 from "path";
1009
1154
  import "commander";
1010
1155
  import {
1011
- buildFrontmatter,
1156
+ buildFrontmatter as buildFrontmatter2,
1012
1157
  findProjectRoot as findProjectRoot9,
1013
1158
  inferModulesFromPaths,
1014
1159
  loadMemoriesFromDir as loadMemoriesFromDir3,
@@ -1124,7 +1269,7 @@ TODO \u2014 write the memory body.
1124
1269
  return;
1125
1270
  }
1126
1271
  }
1127
- const frontmatter = buildFrontmatter({
1272
+ const frontmatter = buildFrontmatter2({
1128
1273
  type: opts.type,
1129
1274
  slug: opts.slug,
1130
1275
  scope,
@@ -1138,7 +1283,7 @@ TODO \u2014 write the memory body.
1138
1283
  topic: opts.topic
1139
1284
  });
1140
1285
  const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1141
- await mkdir3(path7.dirname(file), { recursive: true });
1286
+ await mkdir4(path7.dirname(file), { recursive: true });
1142
1287
  if (existsSync7(file)) {
1143
1288
  ui.error(`Memory already exists at ${file}`);
1144
1289
  process.exitCode = 1;
@@ -1271,7 +1416,7 @@ function matchesFilters(loaded, opts) {
1271
1416
  }
1272
1417
 
1273
1418
  // src/commands/memory-promote.ts
1274
- import { mkdir as mkdir4, unlink, writeFile as writeFile5 } from "fs/promises";
1419
+ import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
1275
1420
  import { existsSync as existsSync9 } from "fs";
1276
1421
  import path9 from "path";
1277
1422
  import "commander";
@@ -1320,7 +1465,7 @@ function registerMemoryPromote(memory2) {
1320
1465
  body: found.memory.body
1321
1466
  };
1322
1467
  const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
1323
- await mkdir4(path9.dirname(newPath), { recursive: true });
1468
+ await mkdir5(path9.dirname(newPath), { recursive: true });
1324
1469
  await writeFile5(newPath, serializeMemory3(updated), "utf8");
1325
1470
  await unlink(found.filePath);
1326
1471
  ui.success(`Promoted ${id} to team scope (status=proposed)`);
@@ -1780,12 +1925,12 @@ function registerMemoryHot(memory2) {
1780
1925
  }
1781
1926
 
1782
1927
  // src/commands/memory-tried.ts
1783
- import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
1928
+ import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
1784
1929
  import { existsSync as existsSync16 } from "fs";
1785
1930
  import path16 from "path";
1786
1931
  import "commander";
1787
1932
  import {
1788
- buildFrontmatter as buildFrontmatter2,
1933
+ buildFrontmatter as buildFrontmatter3,
1789
1934
  findProjectRoot as findProjectRoot18,
1790
1935
  memoryFilePath as memoryFilePath3,
1791
1936
  resolveHaivePaths as resolveHaivePaths15,
@@ -1816,7 +1961,7 @@ function registerMemoryTried(memory2) {
1816
1961
  return;
1817
1962
  }
1818
1963
  const slug = opts.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
1819
- const baseFm = buildFrontmatter2({
1964
+ const baseFm = buildFrontmatter3({
1820
1965
  type: "attempt",
1821
1966
  slug,
1822
1967
  scope: opts.scope,
@@ -1833,7 +1978,7 @@ function registerMemoryTried(memory2) {
1833
1978
  }
1834
1979
  const body = lines.join("\n") + "\n";
1835
1980
  const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1836
- await mkdir5(path16.dirname(file), { recursive: true });
1981
+ await mkdir6(path16.dirname(file), { recursive: true });
1837
1982
  if (existsSync16(file)) {
1838
1983
  ui.error(`Memory already exists at ${file}`);
1839
1984
  process.exitCode = 1;
@@ -2331,18 +2476,178 @@ function registerMemoryImport(memory2) {
2331
2476
  });
2332
2477
  }
2333
2478
 
2334
- // src/commands/memory-digest.ts
2479
+ // src/commands/memory-import-changelog.ts
2335
2480
  import { existsSync as existsSync25 } from "fs";
2336
- import { writeFile as writeFile12 } from "fs/promises";
2481
+ import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2337
2482
  import path23 from "path";
2338
2483
  import "commander";
2339
2484
  import {
2340
- deriveConfidence as deriveConfidence4,
2485
+ buildFrontmatter as buildFrontmatter4,
2341
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,
2342
2647
  getUsage as getUsage8,
2343
2648
  loadMemoriesFromDir as loadMemoriesFromDir5,
2344
2649
  loadUsageIndex as loadUsageIndex10,
2345
- resolveHaivePaths as resolveHaivePaths24
2650
+ resolveHaivePaths as resolveHaivePaths25
2346
2651
  } from "@hiveai/core";
2347
2652
  var CONFIDENCE_EMOJI = {
2348
2653
  unverified: "\u2B1C",
@@ -2355,9 +2660,9 @@ function registerMemoryDigest(program2) {
2355
2660
  program2.command("digest").description(
2356
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"
2357
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) => {
2358
- const root = findProjectRoot27(opts.dir);
2359
- const paths = resolveHaivePaths24(root);
2360
- if (!existsSync25(paths.memoriesDir)) {
2663
+ const root = findProjectRoot28(opts.dir);
2664
+ const paths = resolveHaivePaths25(root);
2665
+ if (!existsSync26(paths.memoriesDir)) {
2361
2666
  ui.error("No .ai/memories found. Run `haive init` first.");
2362
2667
  process.exitCode = 1;
2363
2668
  return;
@@ -2429,8 +2734,8 @@ function registerMemoryDigest(program2) {
2429
2734
  );
2430
2735
  const digest = lines.join("\n");
2431
2736
  if (opts.out) {
2432
- const outPath = path23.resolve(process.cwd(), opts.out);
2433
- await writeFile12(outPath, digest, "utf8");
2737
+ const outPath = path24.resolve(process.cwd(), opts.out);
2738
+ await writeFile13(outPath, digest, "utf8");
2434
2739
  ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
2435
2740
  } else {
2436
2741
  console.log(digest);
@@ -2439,17 +2744,17 @@ function registerMemoryDigest(program2) {
2439
2744
  }
2440
2745
 
2441
2746
  // src/commands/session-end.ts
2442
- import { writeFile as writeFile13, mkdir as mkdir6 } from "fs/promises";
2443
- import { existsSync as existsSync26 } from "fs";
2444
- import path24 from "path";
2747
+ import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
2748
+ import { existsSync as existsSync27 } from "fs";
2749
+ import path25 from "path";
2445
2750
  import "commander";
2446
2751
  import {
2447
- buildFrontmatter as buildFrontmatter3,
2448
- findProjectRoot as findProjectRoot28,
2752
+ buildFrontmatter as buildFrontmatter5,
2753
+ findProjectRoot as findProjectRoot29,
2449
2754
  loadMemoriesFromDir as loadMemoriesFromDir6,
2450
2755
  memoryFilePath as memoryFilePath4,
2451
- resolveHaivePaths as resolveHaivePaths25,
2452
- serializeMemory as serializeMemory10
2756
+ resolveHaivePaths as resolveHaivePaths26,
2757
+ serializeMemory as serializeMemory11
2453
2758
  } from "@hiveai/core";
2454
2759
  function buildRecapBody(opts) {
2455
2760
  const lines = [];
@@ -2498,9 +2803,9 @@ function registerSessionEnd(session2) {
2498
2803
  --next "Add integration tests for webhook signature validation"
2499
2804
  `
2500
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) => {
2501
- const root = findProjectRoot28(opts.dir);
2502
- const paths = resolveHaivePaths25(root);
2503
- if (!existsSync26(paths.haiveDir)) {
2806
+ const root = findProjectRoot29(opts.dir);
2807
+ const paths = resolveHaivePaths26(root);
2808
+ if (!existsSync27(paths.haiveDir)) {
2504
2809
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
2505
2810
  process.exitCode = 1;
2506
2811
  return;
@@ -2509,12 +2814,12 @@ function registerSessionEnd(session2) {
2509
2814
  const body = buildRecapBody(opts);
2510
2815
  const topic = recapTopic(scope, opts.module);
2511
2816
  const filesTouched = parseCsv5(opts.files);
2512
- const missingPaths = filesTouched.filter((p) => !existsSync26(path24.resolve(root, p)));
2817
+ const missingPaths = filesTouched.filter((p) => !existsSync27(path25.resolve(root, p)));
2513
2818
  if (missingPaths.length > 0) {
2514
2819
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
2515
2820
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
2516
2821
  }
2517
- if (existsSync26(paths.memoriesDir)) {
2822
+ if (existsSync27(paths.memoriesDir)) {
2518
2823
  const existing = await loadMemoriesFromDir6(paths.memoriesDir);
2519
2824
  const topicMatch = existing.find(
2520
2825
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
@@ -2530,13 +2835,13 @@ function registerSessionEnd(session2) {
2530
2835
  paths: filesTouched.length ? filesTouched : fm.anchor.paths
2531
2836
  }
2532
2837
  };
2533
- await writeFile13(topicMatch.filePath, serializeMemory10({ frontmatter: newFrontmatter, body }), "utf8");
2838
+ await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
2534
2839
  ui.success(`Session recap updated (revision #${revisionCount})`);
2535
- ui.info(`id=${fm.id} file=${path24.relative(root, topicMatch.filePath)}`);
2840
+ ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
2536
2841
  return;
2537
2842
  }
2538
2843
  }
2539
- const frontmatter = buildFrontmatter3({
2844
+ const frontmatter = buildFrontmatter5({
2540
2845
  type: "session_recap",
2541
2846
  slug: "recap",
2542
2847
  scope,
@@ -2547,10 +2852,10 @@ function registerSessionEnd(session2) {
2547
2852
  status: "validated"
2548
2853
  });
2549
2854
  const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
2550
- await mkdir6(path24.dirname(file), { recursive: true });
2551
- await writeFile13(file, serializeMemory10({ frontmatter, body }), "utf8");
2855
+ await mkdir8(path25.dirname(file), { recursive: true });
2856
+ await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
2552
2857
  ui.success(`Session recap created`);
2553
- ui.info(`id=${frontmatter.id} scope=${scope} file=${path24.relative(root, file)}`);
2858
+ ui.info(`id=${frontmatter.id} scope=${scope} file=${path25.relative(root, file)}`);
2554
2859
  ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
2555
2860
  });
2556
2861
  }
@@ -2559,9 +2864,430 @@ function parseCsv5(value) {
2559
2864
  return value.split(",").map((s) => s.trim()).filter(Boolean);
2560
2865
  }
2561
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
+
2562
3288
  // src/index.ts
2563
- var program = new Command29();
2564
- program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.3.3");
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");
2565
3291
  registerInit(program);
2566
3292
  registerMcp(program);
2567
3293
  registerBriefing(program);
@@ -2589,9 +3315,12 @@ registerMemoryUpdate(memory);
2589
3315
  registerMemoryHot(memory);
2590
3316
  registerMemoryTried(memory);
2591
3317
  registerMemoryImport(memory);
3318
+ registerMemoryImportChangelog(memory);
2592
3319
  registerMemoryDigest(memory);
2593
3320
  var session = program.command("session").description("Manage session lifecycle");
2594
3321
  registerSessionEnd(session);
3322
+ registerSnapshot(program);
3323
+ registerHub(program);
2595
3324
  program.parseAsync(process.argv).catch((err) => {
2596
3325
  if (isZodError(err)) {
2597
3326
  for (const issue of err.issues) {