@hiveai/cli 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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";
@@ -382,9 +382,16 @@ See \`.ai/project-context.md\` for the full project context.
382
382
  Memories live under \`.ai/memories/\` (personal/team/module).
383
383
 
384
384
  ## hAIve \u2014 mandatory rules for AI agents
385
+
386
+ ### Session rules
385
387
  - **Before** marking a task as done ("Done", "Finished", "C'est fait"), call the MCP prompt \`post_task\`.
386
388
  - **Immediately** when an approach fails (wrong API, wrong pattern, wrong assumption), call \`mem_tried\` \u2014 do not wait until the end of the session.
387
389
  - When starting a new task, call \`get_briefing\` with the task description to load relevant memories and avoid repeating past mistakes.
390
+
391
+ ### Safety rules \u2014 NEVER violate these
392
+ - If \`get_briefing\` returns an \`action_required\` list, **stop and show each item to the developer** before doing anything. Use the exact \`developer_message\` provided. Wait for explicit confirmation.
393
+ - **Never modify code autonomously** because of a breaking change detected in another project (dependency version bump, API contract change, removed field). Always ask first.
394
+ - When in doubt about a cross-repo change: ask, don't act.
388
395
  `;
389
396
  var CI_WORKFLOW = `name: haive-sync
390
397
 
@@ -462,6 +469,28 @@ jobs:
462
469
  issue_number: context.issue.number,
463
470
  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
471
  });
472
+
473
+ # On push to main: push shared memories to the hub (if hubPath is configured)
474
+ # Uncomment and configure hubPath in .ai/haive.config.json to enable.
475
+ # hub-push:
476
+ # if: github.event_name == 'push'
477
+ # needs: sync-on-merge
478
+ # runs-on: ubuntu-latest
479
+ # permissions:
480
+ # contents: write
481
+ # steps:
482
+ # - uses: actions/checkout@v4
483
+ # with:
484
+ # fetch-depth: 0
485
+ # - uses: actions/setup-node@v4
486
+ # with:
487
+ # node-version: '20'
488
+ # - name: install haive
489
+ # run: npm install -g @hiveai/cli
490
+ # - name: push shared memories to hub
491
+ # run: haive hub push --commit
492
+ # # Requires hubPath in .ai/haive.config.json pointing to a cloned hub repo.
493
+ # # The hub repo must be available at that path in the CI workspace.
465
494
  `;
466
495
  function registerInit(program2) {
467
496
  program2.command("init").description(
@@ -679,12 +708,13 @@ function locateMcpBin() {
679
708
 
680
709
  // src/commands/sync.ts
681
710
  import { spawnSync as spawnSync2 } from "child_process";
682
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
711
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
683
712
  import { existsSync as existsSync6 } from "fs";
684
713
  import path6 from "path";
685
714
  import "commander";
686
715
  import {
687
716
  DEFAULT_AUTO_PROMOTE_RULE,
717
+ buildFrontmatter,
688
718
  findProjectRoot as findProjectRoot8,
689
719
  getUsage,
690
720
  isAutoPromoteEligible,
@@ -693,9 +723,13 @@ import {
693
723
  loadConfig,
694
724
  loadMemoriesFromDir as loadMemoriesFromDir2,
695
725
  loadUsageIndex,
726
+ pullCrossRepoSources,
696
727
  resolveHaivePaths as resolveHaivePaths5,
728
+ resolveManifestFiles,
697
729
  serializeMemory,
698
- verifyAnchor
730
+ trackDependencies,
731
+ verifyAnchor,
732
+ watchContracts
699
733
  } from "@hiveai/core";
700
734
  var BRIDGE_START = "<!-- haive:memories-start -->";
701
735
  var BRIDGE_END = "<!-- haive:memories-end -->";
@@ -708,7 +742,7 @@ function registerSync(program2) {
708
742
  ).option("--no-verify", "skip the anchor verification step").option("--no-promote", "skip the auto-promotion step").option(
709
743
  "--inject-bridge",
710
744
  "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) => {
745
+ ).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
746
  const root = findProjectRoot8(opts.dir);
713
747
  const paths = resolveHaivePaths5(root);
714
748
  if (!existsSync6(paths.memoriesDir)) {
@@ -878,6 +912,158 @@ function registerSync(program2) {
878
912
  }
879
913
  }
880
914
  }
915
+ if (opts.noCrossRepo !== true && (config.crossRepoSources ?? []).length > 0) {
916
+ try {
917
+ const crossReports = await pullCrossRepoSources(paths, config, root);
918
+ for (const r of crossReports) {
919
+ const total = r.imported.length + r.updated.length;
920
+ if (total > 0 || r.errors.length > 0) {
921
+ log(
922
+ ui.dim(
923
+ `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)` : "")
924
+ )
925
+ );
926
+ for (const e of r.errors) ui.warn(` cross-repo error: ${e}`);
927
+ }
928
+ }
929
+ } catch (err) {
930
+ ui.warn(`cross-repo pull failed: ${String(err)}`);
931
+ }
932
+ }
933
+ if (opts.noDeps !== true) {
934
+ try {
935
+ const manifestFiles = resolveManifestFiles(root, config.dependencyFiles);
936
+ if (manifestFiles.length > 0) {
937
+ const depResults = await trackDependencies(root, paths.haiveDir, manifestFiles);
938
+ for (const result of depResults) {
939
+ const majorBumps = result.changes.filter((c) => c.isMajorBump);
940
+ const minorChanges = result.changes.filter((c) => !c.isMajorBump);
941
+ if (result.changes.length > 0) {
942
+ log(
943
+ ui.yellow(
944
+ `\u26A0 dependency changes in ${result.file}: ${majorBumps.length} major bump(s) \xB7 ${minorChanges.length} minor change(s)`
945
+ )
946
+ );
947
+ for (const c of majorBumps) {
948
+ log(ui.yellow(` MAJOR: ${c.name} ${c.from} \u2192 ${c.to}`));
949
+ }
950
+ for (const c of minorChanges) {
951
+ log(ui.dim(` minor: ${c.name} ${c.from} \u2192 ${c.to}`));
952
+ }
953
+ if (majorBumps.length > 0) {
954
+ const slugParts = result.file.replace(/[^a-z0-9]/gi, "-").toLowerCase();
955
+ const slug = `dep-major-bump-${slugParts}-${Date.now().toString(36)}`;
956
+ const depList = majorBumps.map((c) => `- \u{1F534} **${c.name}** : \`${c.from}\` \u2192 \`${c.to}\``).join("\n");
957
+ const body = `## \u26A0\uFE0F Action requise \u2014 confirmation humaine obligatoire
958
+
959
+ Des d\xE9pendances dans \`${result.file}\` ont chang\xE9 de version majeure.
960
+ Une version majeure peut contenir des **breaking changes** qui impactent ce projet.
961
+
962
+ ${depList}
963
+
964
+ ---
965
+
966
+ **\u{1F6AB} Ne modifie aucun code de fa\xE7on autonome.**
967
+
968
+ Informe le d\xE9veloppeur avec ce message :
969
+
970
+ > *\xAB J'ai d\xE9tect\xE9 que ${majorBumps.map((c) => `\`${c.name}\``).join(", ")} a chang\xE9 de version majeure (${majorBumps.map((c) => `${c.from} \u2192 ${c.to}`).join(", ")}). Cela peut introduire des incompatibilit\xE9s dans ce projet. Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ? \xBB*
971
+
972
+ Attends une **confirmation explicite** avant d'agir.
973
+
974
+ **Prochaines \xE9tapes (si confirm\xE9) :**
975
+ - Consulter le CHANGELOG : \`haive memory import-changelog --from node_modules/<pkg>/CHANGELOG.md\`
976
+ - V\xE9rifier les m\xE9moires ancr\xE9es : \`haive memory verify\``;
977
+ const fm = buildFrontmatter({
978
+ type: "gotcha",
979
+ slug,
980
+ scope: "team",
981
+ status: "validated",
982
+ tags: ["dependency", "breaking-change", "auto-generated", "requires-human-approval"],
983
+ paths: [result.file],
984
+ topic: `dep-bump-${slugParts}`
985
+ });
986
+ const teamDir = path6.join(paths.memoriesDir, "team");
987
+ await mkdir3(teamDir, { recursive: true });
988
+ await writeFile3(
989
+ path6.join(teamDir, `${fm.id}.md`),
990
+ serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
991
+ "utf8"
992
+ );
993
+ log(ui.yellow(` \u2192 memory created: ${fm.id}`));
994
+ }
995
+ }
996
+ }
997
+ }
998
+ } catch (err) {
999
+ ui.warn(`dependency tracker failed: ${String(err)}`);
1000
+ }
1001
+ }
1002
+ if (opts.noContracts !== true && (config.contractFiles ?? []).length > 0) {
1003
+ try {
1004
+ const diffs = await watchContracts(root, paths.haiveDir, config.contractFiles);
1005
+ for (const diff of diffs) {
1006
+ const breaking = diff.changes.filter((c) => c.severity === "breaking");
1007
+ const additive = diff.changes.filter((c) => c.severity === "additive");
1008
+ log(
1009
+ ui.yellow(
1010
+ `\u26A0 contract changed [${diff.contract}]: ${breaking.length} breaking \xB7 ${additive.length} additive`
1011
+ )
1012
+ );
1013
+ for (const c of diff.changes) {
1014
+ const icon = c.severity === "breaking" ? "\u{1F534}" : c.severity === "additive" ? "\u{1F7E2}" : "\u{1F7E1}";
1015
+ log(` ${icon} ${c.description}`);
1016
+ }
1017
+ if (breaking.length > 0) {
1018
+ const slug = `contract-breaking-${diff.contract.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-${Date.now().toString(36)}`;
1019
+ const breakingList = breaking.map((c) => `- \u{1F534} **${c.kind}** : ${c.description}`).join("\n");
1020
+ const addList = additive.length > 0 ? `
1021
+
1022
+ ### Changements non-breaking (additifs)
1023
+ ` + additive.map((c) => `- \u{1F7E2} ${c.description}`).join("\n") : "";
1024
+ const body = `## \u26A0\uFE0F Action requise \u2014 confirmation humaine obligatoire
1025
+
1026
+ Le contrat **\`${diff.contract}\`** (\`${diff.file}\`) a \xE9t\xE9 modifi\xE9.
1027
+ Des **breaking changes** ont \xE9t\xE9 d\xE9tect\xE9s \u2014 ce projet consomme peut-\xEAtre ce contrat.
1028
+
1029
+ ${breakingList}${addList}
1030
+
1031
+ ---
1032
+
1033
+ **\u{1F6AB} Ne modifie aucun code de fa\xE7on autonome.**
1034
+
1035
+ Informe le d\xE9veloppeur avec ce message :
1036
+
1037
+ > *\xAB J'ai d\xE9tect\xE9 que le contrat \`${diff.contract}\` a chang\xE9 : ${breaking.length} breaking change(s) d\xE9tect\xE9(s). Ce projet d\xE9pend peut-\xEAtre de ce contrat. Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ? \xBB*
1038
+
1039
+ Attends une **confirmation explicite** avant d'agir.
1040
+
1041
+ **Prochaines \xE9tapes (si confirm\xE9) :**
1042
+ - Rechercher les usages : \`haive memory for-files <fichiers concern\xE9s>\`
1043
+ - V\xE9rifier les m\xE9moires li\xE9es : \`haive memory query ${diff.contract}\``;
1044
+ const fm = buildFrontmatter({
1045
+ type: "gotcha",
1046
+ slug,
1047
+ scope: "team",
1048
+ status: "validated",
1049
+ tags: ["api-contract", "breaking-change", diff.contract, "auto-generated", "requires-human-approval"],
1050
+ paths: [diff.file],
1051
+ topic: `contract-breaking-${diff.contract}`
1052
+ });
1053
+ const teamDir = path6.join(paths.memoriesDir, "team");
1054
+ await mkdir3(teamDir, { recursive: true });
1055
+ await writeFile3(
1056
+ path6.join(teamDir, `${fm.id}.md`),
1057
+ serializeMemory({ frontmatter: { ...fm, requires_human_approval: true }, body }),
1058
+ "utf8"
1059
+ );
1060
+ log(ui.yellow(` \u2192 memory created: ${fm.id}`));
1061
+ }
1062
+ }
1063
+ } catch (err) {
1064
+ ui.warn(`contract watcher failed: ${String(err)}`);
1065
+ }
1066
+ }
881
1067
  const existingMap = await loadCodeMap2(paths);
882
1068
  if (existingMap) {
883
1069
  const mapAge = new Date(existingMap.generated_at).getTime();
@@ -1003,12 +1189,12 @@ function collectSinceChanges(root, ref) {
1003
1189
 
1004
1190
  // src/commands/memory-add.ts
1005
1191
  import { createHash } from "crypto";
1006
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1192
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1007
1193
  import { existsSync as existsSync7 } from "fs";
1008
1194
  import path7 from "path";
1009
1195
  import "commander";
1010
1196
  import {
1011
- buildFrontmatter,
1197
+ buildFrontmatter as buildFrontmatter2,
1012
1198
  findProjectRoot as findProjectRoot9,
1013
1199
  inferModulesFromPaths,
1014
1200
  loadMemoriesFromDir as loadMemoriesFromDir3,
@@ -1124,7 +1310,7 @@ TODO \u2014 write the memory body.
1124
1310
  return;
1125
1311
  }
1126
1312
  }
1127
- const frontmatter = buildFrontmatter({
1313
+ const frontmatter = buildFrontmatter2({
1128
1314
  type: opts.type,
1129
1315
  slug: opts.slug,
1130
1316
  scope,
@@ -1138,7 +1324,7 @@ TODO \u2014 write the memory body.
1138
1324
  topic: opts.topic
1139
1325
  });
1140
1326
  const file = memoryFilePath(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1141
- await mkdir3(path7.dirname(file), { recursive: true });
1327
+ await mkdir4(path7.dirname(file), { recursive: true });
1142
1328
  if (existsSync7(file)) {
1143
1329
  ui.error(`Memory already exists at ${file}`);
1144
1330
  process.exitCode = 1;
@@ -1271,7 +1457,7 @@ function matchesFilters(loaded, opts) {
1271
1457
  }
1272
1458
 
1273
1459
  // src/commands/memory-promote.ts
1274
- import { mkdir as mkdir4, unlink, writeFile as writeFile5 } from "fs/promises";
1460
+ import { mkdir as mkdir5, unlink, writeFile as writeFile5 } from "fs/promises";
1275
1461
  import { existsSync as existsSync9 } from "fs";
1276
1462
  import path9 from "path";
1277
1463
  import "commander";
@@ -1320,7 +1506,7 @@ function registerMemoryPromote(memory2) {
1320
1506
  body: found.memory.body
1321
1507
  };
1322
1508
  const newPath = memoryFilePath2(paths, "team", updated.frontmatter.id);
1323
- await mkdir4(path9.dirname(newPath), { recursive: true });
1509
+ await mkdir5(path9.dirname(newPath), { recursive: true });
1324
1510
  await writeFile5(newPath, serializeMemory3(updated), "utf8");
1325
1511
  await unlink(found.filePath);
1326
1512
  ui.success(`Promoted ${id} to team scope (status=proposed)`);
@@ -1780,12 +1966,12 @@ function registerMemoryHot(memory2) {
1780
1966
  }
1781
1967
 
1782
1968
  // src/commands/memory-tried.ts
1783
- import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
1969
+ import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
1784
1970
  import { existsSync as existsSync16 } from "fs";
1785
1971
  import path16 from "path";
1786
1972
  import "commander";
1787
1973
  import {
1788
- buildFrontmatter as buildFrontmatter2,
1974
+ buildFrontmatter as buildFrontmatter3,
1789
1975
  findProjectRoot as findProjectRoot18,
1790
1976
  memoryFilePath as memoryFilePath3,
1791
1977
  resolveHaivePaths as resolveHaivePaths15,
@@ -1816,7 +2002,7 @@ function registerMemoryTried(memory2) {
1816
2002
  return;
1817
2003
  }
1818
2004
  const slug = opts.what.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 5).join("-");
1819
- const baseFm = buildFrontmatter2({
2005
+ const baseFm = buildFrontmatter3({
1820
2006
  type: "attempt",
1821
2007
  slug,
1822
2008
  scope: opts.scope,
@@ -1833,7 +2019,7 @@ function registerMemoryTried(memory2) {
1833
2019
  }
1834
2020
  const body = lines.join("\n") + "\n";
1835
2021
  const file = memoryFilePath3(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
1836
- await mkdir5(path16.dirname(file), { recursive: true });
2022
+ await mkdir6(path16.dirname(file), { recursive: true });
1837
2023
  if (existsSync16(file)) {
1838
2024
  ui.error(`Memory already exists at ${file}`);
1839
2025
  process.exitCode = 1;
@@ -2331,18 +2517,178 @@ function registerMemoryImport(memory2) {
2331
2517
  });
2332
2518
  }
2333
2519
 
2334
- // src/commands/memory-digest.ts
2520
+ // src/commands/memory-import-changelog.ts
2335
2521
  import { existsSync as existsSync25 } from "fs";
2336
- import { writeFile as writeFile12 } from "fs/promises";
2522
+ import { readFile as readFile8, mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
2337
2523
  import path23 from "path";
2338
2524
  import "commander";
2339
2525
  import {
2340
- deriveConfidence as deriveConfidence4,
2526
+ buildFrontmatter as buildFrontmatter4,
2341
2527
  findProjectRoot as findProjectRoot27,
2528
+ resolveHaivePaths as resolveHaivePaths24,
2529
+ serializeMemory as serializeMemory10
2530
+ } from "@hiveai/core";
2531
+ function parseChangelog(content) {
2532
+ const entries = [];
2533
+ const versionRe = /^#{1,3}\s+(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/m;
2534
+ const sections = content.split(/^#{1,3}\s+/m).slice(1);
2535
+ for (const section of sections) {
2536
+ const versionMatch = section.match(/^(?:\[?)([0-9]+\.[0-9]+[.0-9]*)/);
2537
+ if (!versionMatch) continue;
2538
+ const version = versionMatch[1];
2539
+ const entry = {
2540
+ version,
2541
+ breaking: [],
2542
+ deprecated: [],
2543
+ removed: [],
2544
+ fixed: [],
2545
+ added: []
2546
+ };
2547
+ const subSections = section.split(/^#{2,4}\s+/m);
2548
+ for (const sub of subSections) {
2549
+ const firstLine = sub.split("\n")[0].toLowerCase().trim();
2550
+ const items = sub.split("\n").slice(1).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).map((l) => l.replace(/^[\s\-*]+/, "").trim()).filter(Boolean);
2551
+ if (/breaking/.test(firstLine)) {
2552
+ entry.breaking.push(...items);
2553
+ } else if (/deprecated/.test(firstLine)) {
2554
+ entry.deprecated.push(...items);
2555
+ } else if (/removed/.test(firstLine)) {
2556
+ entry.removed.push(...items);
2557
+ } else if (/fixed|bug/.test(firstLine)) {
2558
+ entry.fixed.push(...items);
2559
+ } else if (/added|new|feat/.test(firstLine)) {
2560
+ entry.added.push(...items);
2561
+ }
2562
+ for (const sub2 of subSections) {
2563
+ for (const line of sub2.split("\n")) {
2564
+ const breakingMatch = line.match(/BREAKING CHANGE[S]?:\s*(.+)/i);
2565
+ if (breakingMatch && !entry.breaking.includes(breakingMatch[1].trim())) {
2566
+ entry.breaking.push(breakingMatch[1].trim());
2567
+ }
2568
+ }
2569
+ }
2570
+ }
2571
+ if (entry.breaking.length === 0) {
2572
+ for (const line of section.split("\n")) {
2573
+ if (/breaking|⚠|deprecated|removed/.test(line.toLowerCase())) {
2574
+ const item = line.replace(/^[\s\-*#]+/, "").trim();
2575
+ if (item) entry.breaking.push(item);
2576
+ }
2577
+ }
2578
+ }
2579
+ const hasContent = entry.breaking.length > 0 || entry.deprecated.length > 0 || entry.removed.length > 0;
2580
+ if (hasContent) entries.push(entry);
2581
+ }
2582
+ void versionRe;
2583
+ return entries;
2584
+ }
2585
+ function registerMemoryImportChangelog(memory2) {
2586
+ memory2.command("import-changelog").description(
2587
+ "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"
2588
+ ).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(
2589
+ "--versions <csv>",
2590
+ "only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
2591
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
2592
+ const root = findProjectRoot27(opts.dir);
2593
+ const paths = resolveHaivePaths24(root);
2594
+ const changelogPath = path23.resolve(root, opts.fromChangelog);
2595
+ if (!existsSync25(changelogPath)) {
2596
+ ui.error(`CHANGELOG not found: ${changelogPath}`);
2597
+ process.exitCode = 1;
2598
+ return;
2599
+ }
2600
+ const content = await readFile8(changelogPath, "utf8");
2601
+ let entries = parseChangelog(content);
2602
+ if (entries.length === 0) {
2603
+ ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
2604
+ return;
2605
+ }
2606
+ if (opts.versions) {
2607
+ if (opts.versions === "latest") {
2608
+ entries = [entries[0]];
2609
+ } else {
2610
+ const requested = opts.versions.split(",").map((v) => v.trim());
2611
+ entries = entries.filter((e) => requested.includes(e.version));
2612
+ }
2613
+ }
2614
+ const pkgName = opts.package ?? path23.basename(path23.dirname(changelogPath));
2615
+ const scope = opts.scope ?? "team";
2616
+ const teamDir = path23.join(paths.memoriesDir, scope);
2617
+ await mkdir7(teamDir, { recursive: true });
2618
+ let saved = 0;
2619
+ for (const entry of entries) {
2620
+ const lines = [];
2621
+ lines.push(`## ${pkgName} v${entry.version} \u2014 Breaking Changes & Deprecations
2622
+ `);
2623
+ if (entry.breaking.length > 0) {
2624
+ lines.push("### \u{1F534} Breaking Changes\n");
2625
+ for (const item of entry.breaking) lines.push(`- ${item}`);
2626
+ lines.push("");
2627
+ }
2628
+ if (entry.deprecated.length > 0) {
2629
+ lines.push("### \u{1F7E1} Deprecated\n");
2630
+ for (const item of entry.deprecated) lines.push(`- ${item}`);
2631
+ lines.push("");
2632
+ }
2633
+ if (entry.removed.length > 0) {
2634
+ lines.push("### \u26AB Removed\n");
2635
+ for (const item of entry.removed) lines.push(`- ${item}`);
2636
+ lines.push("");
2637
+ }
2638
+ lines.push(
2639
+ `**Source:** \`${path23.relative(root, changelogPath)}\`
2640
+ **Action:** Update all usages of ${pkgName} if they rely on any of the above.`
2641
+ );
2642
+ const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
2643
+ const fm = buildFrontmatter4({
2644
+ type: "gotcha",
2645
+ slug,
2646
+ scope,
2647
+ status: "validated",
2648
+ tags: [
2649
+ "changelog",
2650
+ "breaking-change",
2651
+ pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
2652
+ `v${entry.version}`
2653
+ ],
2654
+ paths: [path23.relative(root, changelogPath)],
2655
+ topic: `changelog-${pkgName}-${entry.version}`
2656
+ });
2657
+ await writeFile12(
2658
+ path23.join(teamDir, `${fm.id}.md`),
2659
+ serializeMemory10({ frontmatter: fm, body: lines.join("\n") }),
2660
+ "utf8"
2661
+ );
2662
+ console.log(ui.green(` \u2713 ${fm.id}`));
2663
+ saved++;
2664
+ }
2665
+ console.log(
2666
+ `
2667
+ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${pkgName}`)}`
2668
+ );
2669
+ if (saved > 0) {
2670
+ console.log(
2671
+ ui.dim(` Memories saved to .ai/memories/${scope}/`)
2672
+ );
2673
+ console.log(
2674
+ ui.dim(` Run \`haive briefing --task "update ${pkgName}"\` to see them in context.`)
2675
+ );
2676
+ }
2677
+ });
2678
+ }
2679
+
2680
+ // src/commands/memory-digest.ts
2681
+ import { existsSync as existsSync26 } from "fs";
2682
+ import { writeFile as writeFile13 } from "fs/promises";
2683
+ import path24 from "path";
2684
+ import "commander";
2685
+ import {
2686
+ deriveConfidence as deriveConfidence4,
2687
+ findProjectRoot as findProjectRoot28,
2342
2688
  getUsage as getUsage8,
2343
2689
  loadMemoriesFromDir as loadMemoriesFromDir5,
2344
2690
  loadUsageIndex as loadUsageIndex10,
2345
- resolveHaivePaths as resolveHaivePaths24
2691
+ resolveHaivePaths as resolveHaivePaths25
2346
2692
  } from "@hiveai/core";
2347
2693
  var CONFIDENCE_EMOJI = {
2348
2694
  unverified: "\u2B1C",
@@ -2355,9 +2701,9 @@ function registerMemoryDigest(program2) {
2355
2701
  program2.command("digest").description(
2356
2702
  "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
2703
  ).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)) {
2704
+ const root = findProjectRoot28(opts.dir);
2705
+ const paths = resolveHaivePaths25(root);
2706
+ if (!existsSync26(paths.memoriesDir)) {
2361
2707
  ui.error("No .ai/memories found. Run `haive init` first.");
2362
2708
  process.exitCode = 1;
2363
2709
  return;
@@ -2429,8 +2775,8 @@ function registerMemoryDigest(program2) {
2429
2775
  );
2430
2776
  const digest = lines.join("\n");
2431
2777
  if (opts.out) {
2432
- const outPath = path23.resolve(process.cwd(), opts.out);
2433
- await writeFile12(outPath, digest, "utf8");
2778
+ const outPath = path24.resolve(process.cwd(), opts.out);
2779
+ await writeFile13(outPath, digest, "utf8");
2434
2780
  ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
2435
2781
  } else {
2436
2782
  console.log(digest);
@@ -2439,17 +2785,17 @@ function registerMemoryDigest(program2) {
2439
2785
  }
2440
2786
 
2441
2787
  // 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";
2788
+ import { writeFile as writeFile14, mkdir as mkdir8 } from "fs/promises";
2789
+ import { existsSync as existsSync27 } from "fs";
2790
+ import path25 from "path";
2445
2791
  import "commander";
2446
2792
  import {
2447
- buildFrontmatter as buildFrontmatter3,
2448
- findProjectRoot as findProjectRoot28,
2793
+ buildFrontmatter as buildFrontmatter5,
2794
+ findProjectRoot as findProjectRoot29,
2449
2795
  loadMemoriesFromDir as loadMemoriesFromDir6,
2450
2796
  memoryFilePath as memoryFilePath4,
2451
- resolveHaivePaths as resolveHaivePaths25,
2452
- serializeMemory as serializeMemory10
2797
+ resolveHaivePaths as resolveHaivePaths26,
2798
+ serializeMemory as serializeMemory11
2453
2799
  } from "@hiveai/core";
2454
2800
  function buildRecapBody(opts) {
2455
2801
  const lines = [];
@@ -2498,9 +2844,9 @@ function registerSessionEnd(session2) {
2498
2844
  --next "Add integration tests for webhook signature validation"
2499
2845
  `
2500
2846
  ).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)) {
2847
+ const root = findProjectRoot29(opts.dir);
2848
+ const paths = resolveHaivePaths26(root);
2849
+ if (!existsSync27(paths.haiveDir)) {
2504
2850
  ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
2505
2851
  process.exitCode = 1;
2506
2852
  return;
@@ -2509,12 +2855,12 @@ function registerSessionEnd(session2) {
2509
2855
  const body = buildRecapBody(opts);
2510
2856
  const topic = recapTopic(scope, opts.module);
2511
2857
  const filesTouched = parseCsv5(opts.files);
2512
- const missingPaths = filesTouched.filter((p) => !existsSync26(path24.resolve(root, p)));
2858
+ const missingPaths = filesTouched.filter((p) => !existsSync27(path25.resolve(root, p)));
2513
2859
  if (missingPaths.length > 0) {
2514
2860
  ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
2515
2861
  for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
2516
2862
  }
2517
- if (existsSync26(paths.memoriesDir)) {
2863
+ if (existsSync27(paths.memoriesDir)) {
2518
2864
  const existing = await loadMemoriesFromDir6(paths.memoriesDir);
2519
2865
  const topicMatch = existing.find(
2520
2866
  ({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
@@ -2530,13 +2876,13 @@ function registerSessionEnd(session2) {
2530
2876
  paths: filesTouched.length ? filesTouched : fm.anchor.paths
2531
2877
  }
2532
2878
  };
2533
- await writeFile13(topicMatch.filePath, serializeMemory10({ frontmatter: newFrontmatter, body }), "utf8");
2879
+ await writeFile14(topicMatch.filePath, serializeMemory11({ frontmatter: newFrontmatter, body }), "utf8");
2534
2880
  ui.success(`Session recap updated (revision #${revisionCount})`);
2535
- ui.info(`id=${fm.id} file=${path24.relative(root, topicMatch.filePath)}`);
2881
+ ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
2536
2882
  return;
2537
2883
  }
2538
2884
  }
2539
- const frontmatter = buildFrontmatter3({
2885
+ const frontmatter = buildFrontmatter5({
2540
2886
  type: "session_recap",
2541
2887
  slug: "recap",
2542
2888
  scope,
@@ -2547,10 +2893,10 @@ function registerSessionEnd(session2) {
2547
2893
  status: "validated"
2548
2894
  });
2549
2895
  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");
2896
+ await mkdir8(path25.dirname(file), { recursive: true });
2897
+ await writeFile14(file, serializeMemory11({ frontmatter, body }), "utf8");
2552
2898
  ui.success(`Session recap created`);
2553
- ui.info(`id=${frontmatter.id} scope=${scope} file=${path24.relative(root, file)}`);
2899
+ ui.info(`id=${frontmatter.id} scope=${scope} file=${path25.relative(root, file)}`);
2554
2900
  ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
2555
2901
  });
2556
2902
  }
@@ -2559,9 +2905,430 @@ function parseCsv5(value) {
2559
2905
  return value.split(",").map((s) => s.trim()).filter(Boolean);
2560
2906
  }
2561
2907
 
2908
+ // src/commands/snapshot.ts
2909
+ import { existsSync as existsSync28 } from "fs";
2910
+ import { readdir } from "fs/promises";
2911
+ import path26 from "path";
2912
+ import "commander";
2913
+ import {
2914
+ diffContract,
2915
+ findProjectRoot as findProjectRoot30,
2916
+ loadConfig as loadConfig2,
2917
+ resolveHaivePaths as resolveHaivePaths27,
2918
+ snapshotContract
2919
+ } from "@hiveai/core";
2920
+ function registerSnapshot(program2) {
2921
+ program2.command("snapshot").description(
2922
+ `Take or compare an API contract snapshot to detect breaking changes.
2923
+
2924
+ A snapshot captures the structure of a contract file (endpoints, types, fields).
2925
+ Running 'haive sync' automatically checks all configured contracts.
2926
+ This command lets you snapshot or diff a single contract on demand.
2927
+
2928
+ Supported formats: openapi, graphql, proto, typescript, json-schema
2929
+
2930
+ Examples:
2931
+ haive snapshot --contract docs/openapi.yaml --name payment-api
2932
+ haive snapshot --diff --name payment-api
2933
+ haive snapshot --list
2934
+
2935
+ To monitor contracts automatically on haive sync, add them to haive.config.json:
2936
+ { "contractFiles": [{ "name": "payment-api", "path": "docs/openapi.yaml", "format": "openapi" }] }
2937
+ `
2938
+ ).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(
2939
+ "--format <format>",
2940
+ "contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
2941
+ ).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) => {
2942
+ const root = findProjectRoot30(opts.dir);
2943
+ const paths = resolveHaivePaths27(root);
2944
+ if (!existsSync28(paths.haiveDir)) {
2945
+ ui.error("No .ai/ found. Run `haive init` first.");
2946
+ process.exitCode = 1;
2947
+ return;
2948
+ }
2949
+ if (opts.list) {
2950
+ const contractsDir = path26.join(paths.haiveDir, "contracts");
2951
+ if (!existsSync28(contractsDir)) {
2952
+ console.log(ui.dim("No contract snapshots found."));
2953
+ return;
2954
+ }
2955
+ const files = (await readdir(contractsDir)).filter(
2956
+ (f) => f.endsWith(".lock") && !f.startsWith("deps-")
2957
+ );
2958
+ if (files.length === 0) {
2959
+ console.log(ui.dim("No contract snapshots found."));
2960
+ return;
2961
+ }
2962
+ console.log(ui.bold(`Contract snapshots (${files.length}):`));
2963
+ for (const f of files) {
2964
+ const name2 = f.replace(".lock", "");
2965
+ console.log(` ${name2}`);
2966
+ }
2967
+ return;
2968
+ }
2969
+ if (opts.diff) {
2970
+ if (!opts.name) {
2971
+ const config2 = await loadConfig2(paths);
2972
+ const contracts = config2.contractFiles ?? [];
2973
+ if (contracts.length === 0) {
2974
+ ui.error("--diff requires --name, or configure contractFiles in haive.config.json");
2975
+ process.exitCode = 1;
2976
+ return;
2977
+ }
2978
+ for (const contract3 of contracts) {
2979
+ await runDiff(root, paths.haiveDir, contract3);
2980
+ }
2981
+ return;
2982
+ }
2983
+ const config = await loadConfig2(paths);
2984
+ const configured = (config.contractFiles ?? []).find((c) => c.name === opts.name);
2985
+ if (!configured && !opts.contract) {
2986
+ ui.error(
2987
+ `Contract "${opts.name}" not found in haive.config.json and --contract not provided.`
2988
+ );
2989
+ process.exitCode = 1;
2990
+ return;
2991
+ }
2992
+ const contract2 = configured ?? {
2993
+ name: opts.name,
2994
+ path: opts.contract,
2995
+ format: detectFormat(opts.contract ?? "") ?? "openapi"
2996
+ };
2997
+ await runDiff(root, paths.haiveDir, contract2);
2998
+ return;
2999
+ }
3000
+ if (!opts.contract) {
3001
+ ui.error("Provide --contract <file> or use --diff / --list.");
3002
+ process.exitCode = 1;
3003
+ return;
3004
+ }
3005
+ const contractPath = opts.contract;
3006
+ const name = opts.name ?? path26.basename(contractPath, path26.extname(contractPath));
3007
+ const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
3008
+ const contract = { name, path: contractPath, format };
3009
+ try {
3010
+ const snapshot = await snapshotContract(root, paths.haiveDir, contract);
3011
+ console.log(ui.green(`\u2713 snapshot saved: ${name}`));
3012
+ if (snapshot.endpoints) {
3013
+ console.log(ui.dim(` ${snapshot.endpoints.length} endpoint(s) captured`));
3014
+ }
3015
+ if (snapshot.types) {
3016
+ console.log(ui.dim(` ${snapshot.types.length} type(s) captured`));
3017
+ }
3018
+ console.log(ui.dim(` lock: .ai/contracts/${name}.lock`));
3019
+ console.log(ui.dim(" Next haive sync will detect changes automatically."));
3020
+ console.log(
3021
+ ui.dim(
3022
+ ` Tip: add to haive.config.json \u2192 contractFiles to monitor automatically:
3023
+ { "name": "${name}", "path": "${contractPath}", "format": "${format}" }`
3024
+ )
3025
+ );
3026
+ } catch (err) {
3027
+ ui.error(String(err));
3028
+ process.exitCode = 1;
3029
+ }
3030
+ });
3031
+ }
3032
+ async function runDiff(root, haiveDir, contract) {
3033
+ try {
3034
+ const result = await diffContract(root, haiveDir, contract);
3035
+ if (result.unchanged) {
3036
+ console.log(ui.green(`\u2713 ${contract.name}: no changes detected`));
3037
+ return;
3038
+ }
3039
+ const breaking = result.changes.filter((c) => c.severity === "breaking");
3040
+ const additive = result.changes.filter((c) => c.severity === "additive");
3041
+ const unknown = result.changes.filter((c) => c.severity === "unknown");
3042
+ console.log(
3043
+ ui.bold(`Contract diff: ${contract.name}`) + ` \u2014 ${breaking.length} breaking \xB7 ${additive.length} additive \xB7 ${unknown.length} unknown`
3044
+ );
3045
+ for (const c of result.changes) {
3046
+ const icon = c.severity === "breaking" ? "\u{1F534}" : c.severity === "additive" ? "\u{1F7E2}" : "\u{1F7E1}";
3047
+ console.log(` ${icon} ${c.description}`);
3048
+ }
3049
+ if (breaking.length > 0) {
3050
+ console.log(
3051
+ ui.yellow(
3052
+ "\n \u26A0 Breaking changes detected \u2014 run `haive sync` to create a gotcha memory for your team."
3053
+ )
3054
+ );
3055
+ }
3056
+ } catch (err) {
3057
+ ui.error(`diff failed for ${contract.name}: ${String(err)}`);
3058
+ }
3059
+ }
3060
+ function detectFormat(filePath) {
3061
+ const ext = path26.extname(filePath).toLowerCase();
3062
+ const base = path26.basename(filePath).toLowerCase();
3063
+ if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
3064
+ if (base.includes("openapi") || base.includes("swagger")) return "openapi";
3065
+ if (base.includes("schema") || base.includes("graphql")) return "graphql";
3066
+ return "openapi";
3067
+ }
3068
+ if (ext === ".graphql" || ext === ".gql") return "graphql";
3069
+ if (ext === ".proto") return "proto";
3070
+ if (ext === ".d.ts" || ext === ".ts") return "typescript";
3071
+ return null;
3072
+ }
3073
+
3074
+ // src/commands/hub.ts
3075
+ import { existsSync as existsSync29 } from "fs";
3076
+ import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile15, copyFile } from "fs/promises";
3077
+ import path27 from "path";
3078
+ import { spawnSync as spawnSync3 } from "child_process";
3079
+ import "commander";
3080
+ import {
3081
+ findProjectRoot as findProjectRoot31,
3082
+ loadConfig as loadConfig3,
3083
+ loadMemoriesFromDir as loadMemoriesFromDir7,
3084
+ resolveHaivePaths as resolveHaivePaths28,
3085
+ saveConfig as saveConfig2,
3086
+ serializeMemory as serializeMemory12
3087
+ } from "@hiveai/core";
3088
+ function registerHub(program2) {
3089
+ const hub = program2.command("hub").description(
3090
+ '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'
3091
+ );
3092
+ hub.action(() => hub.help());
3093
+ hub.command("init <hubPath>").description(
3094
+ "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"
3095
+ ).action(async (hubPath) => {
3096
+ const absPath = path27.resolve(hubPath);
3097
+ await mkdir9(absPath, { recursive: true });
3098
+ const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
3099
+ if (gitCheck.status !== 0) {
3100
+ const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
3101
+ if (init.status !== 0) {
3102
+ ui.error(`git init failed: ${init.stderr}`);
3103
+ process.exitCode = 1;
3104
+ return;
3105
+ }
3106
+ }
3107
+ const sharedDir = path27.join(absPath, ".ai", "memories", "shared");
3108
+ await mkdir9(sharedDir, { recursive: true });
3109
+ await writeFile15(
3110
+ path27.join(absPath, ".ai", "README.md"),
3111
+ `# hAIve Team Knowledge Hub
3112
+
3113
+ This repo is a shared knowledge hub for hAIve.
3114
+
3115
+ Each project contributes its \`shared\`-scoped memories here.
3116
+ Other projects pull from it via \`haive hub pull\`.
3117
+
3118
+ ## Structure
3119
+
3120
+ \`\`.ai/memories/shared/<project-name>/\`
3121
+
3122
+ ## Usage
3123
+
3124
+ \`\`\`bash
3125
+ haive hub push # publish from a project
3126
+ haive hub pull # import into a project
3127
+ \`\`\`
3128
+ `,
3129
+ "utf8"
3130
+ );
3131
+ await writeFile15(
3132
+ path27.join(absPath, ".gitignore"),
3133
+ ".ai/.cache/\n.ai/memories/personal/\n",
3134
+ "utf8"
3135
+ );
3136
+ spawnSync3("git", ["add", "."], { cwd: absPath });
3137
+ spawnSync3("git", ["commit", "-m", "chore: initialize hAIve team-knowledge hub"], {
3138
+ cwd: absPath,
3139
+ encoding: "utf8"
3140
+ });
3141
+ console.log(ui.green(`\u2713 Hub initialized at ${absPath}`));
3142
+ console.log(
3143
+ ui.dim(
3144
+ `
3145
+ Next steps:
3146
+ 1. Add hubPath to your project's .ai/haive.config.json:
3147
+ { "hubPath": "${path27.relative(process.cwd(), absPath)}" }
3148
+ 2. Run \`haive hub push\` to publish your shared memories
3149
+ 3. Share ${absPath} with teammates (git remote, NFS, etc.)
3150
+ `
3151
+ )
3152
+ );
3153
+ });
3154
+ hub.command("push").description(
3155
+ `Push this project's shared-scoped memories to the hub.
3156
+
3157
+ Copies all memories with scope=shared to hub/.ai/memories/shared/<project-name>/.
3158
+ Optionally commits to the hub repo.
3159
+
3160
+ Examples:
3161
+ haive hub push
3162
+ haive hub push --commit
3163
+ haive hub push --commit --message "feat: add payment API contract memories"
3164
+ `
3165
+ ).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) => {
3166
+ const root = findProjectRoot31(opts.dir);
3167
+ const paths = resolveHaivePaths28(root);
3168
+ const config = await loadConfig3(paths);
3169
+ if (!config.hubPath) {
3170
+ ui.error(
3171
+ 'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
3172
+ );
3173
+ process.exitCode = 1;
3174
+ return;
3175
+ }
3176
+ const hubRoot = path27.resolve(root, config.hubPath);
3177
+ if (!existsSync29(hubRoot)) {
3178
+ ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
3179
+ process.exitCode = 1;
3180
+ return;
3181
+ }
3182
+ const projectName = path27.basename(root);
3183
+ const destDir = path27.join(hubRoot, ".ai", "memories", "shared", projectName);
3184
+ await mkdir9(destDir, { recursive: true });
3185
+ const all = await loadMemoriesFromDir7(paths.memoriesDir);
3186
+ const shared = all.filter(
3187
+ ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
3188
+ !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
3189
+ );
3190
+ if (shared.length === 0) {
3191
+ ui.warn(
3192
+ `No shared-scoped memories found. Create memories with scope=shared to push to the hub.
3193
+ Example: haive memory add --type architecture --slug my-api --scope shared --body "..."
3194
+ Or with MCP: mem_save({ scope: 'shared', ... })`
3195
+ );
3196
+ return;
3197
+ }
3198
+ let pushed = 0;
3199
+ for (const { memory: memory2 } of shared) {
3200
+ const fm = memory2.frontmatter;
3201
+ const fileName = `${fm.id}.md`;
3202
+ const destPath = path27.join(destDir, fileName);
3203
+ await writeFile15(destPath, serializeMemory12(memory2), "utf8");
3204
+ pushed++;
3205
+ }
3206
+ console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
3207
+ console.log(ui.dim(` Location: ${destDir}`));
3208
+ if (opts.commit) {
3209
+ const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
3210
+ spawnSync3("git", ["add", path27.join(".ai", "memories", "shared", projectName)], {
3211
+ cwd: hubRoot
3212
+ });
3213
+ const commit = spawnSync3("git", ["commit", "-m", message], {
3214
+ cwd: hubRoot,
3215
+ encoding: "utf8"
3216
+ });
3217
+ if (commit.status === 0) {
3218
+ console.log(ui.green(`\u2713 Committed to hub: "${message}"`));
3219
+ } else if (commit.stdout?.includes("nothing to commit")) {
3220
+ console.log(ui.dim(" Hub already up to date \u2014 nothing to commit."));
3221
+ } else {
3222
+ ui.warn(`git commit in hub failed: ${commit.stderr}`);
3223
+ }
3224
+ } else {
3225
+ console.log(
3226
+ ui.dim(
3227
+ " Tip: use --commit to auto-commit to the hub repo, or commit manually."
3228
+ )
3229
+ );
3230
+ }
3231
+ });
3232
+ hub.command("pull").description(
3233
+ "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"
3234
+ ).option("-d, --dir <dir>", "project root").action(async (opts) => {
3235
+ const root = findProjectRoot31(opts.dir);
3236
+ const paths = resolveHaivePaths28(root);
3237
+ const config = await loadConfig3(paths);
3238
+ if (!config.hubPath) {
3239
+ ui.error(
3240
+ 'hubPath not configured in .ai/haive.config.json.\n Add: { "hubPath": "../team-hub" }\n Or run: haive hub init <path> first.'
3241
+ );
3242
+ process.exitCode = 1;
3243
+ return;
3244
+ }
3245
+ const hubRoot = path27.resolve(root, config.hubPath);
3246
+ const hubSharedDir = path27.join(hubRoot, ".ai", "memories", "shared");
3247
+ if (!existsSync29(hubSharedDir)) {
3248
+ ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
3249
+ return;
3250
+ }
3251
+ const projectName = path27.basename(root);
3252
+ const { readdir: readdir2 } = await import("fs/promises");
3253
+ const projectDirs = (await readdir2(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
3254
+ if (projectDirs.length === 0) {
3255
+ console.log(ui.dim("No other projects have pushed to the hub yet."));
3256
+ return;
3257
+ }
3258
+ let totalImported = 0;
3259
+ let totalUpdated = 0;
3260
+ for (const sourceName of projectDirs) {
3261
+ const sourceDir = path27.join(hubSharedDir, sourceName);
3262
+ const destDir = path27.join(paths.memoriesDir, "shared", sourceName);
3263
+ await mkdir9(destDir, { recursive: true });
3264
+ const sourceFiles = (await readdir2(sourceDir)).filter((f) => f.endsWith(".md"));
3265
+ const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
3266
+ const existingInDest = await loadDir(destDir);
3267
+ const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
3268
+ for (const file of sourceFiles) {
3269
+ const srcPath = path27.join(sourceDir, file);
3270
+ const destPath = path27.join(destDir, file);
3271
+ const fileContent = await readFile9(srcPath, "utf8");
3272
+ const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
3273
+ if (!alreadyTagged) {
3274
+ await copyFile(srcPath, destPath);
3275
+ } else {
3276
+ await copyFile(srcPath, destPath);
3277
+ }
3278
+ const memId = file.replace(".md", "");
3279
+ if (existingIds.has(memId)) {
3280
+ totalUpdated++;
3281
+ } else {
3282
+ totalImported++;
3283
+ }
3284
+ }
3285
+ console.log(
3286
+ ui.dim(` [${sourceName}]: ${sourceFiles.length} memor${sourceFiles.length === 1 ? "y" : "ies"} synced`)
3287
+ );
3288
+ }
3289
+ console.log(
3290
+ ui.green(`\u2713 Hub pull complete: ${totalImported} new \xB7 ${totalUpdated} updated`)
3291
+ );
3292
+ });
3293
+ hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
3294
+ const root = findProjectRoot31(opts.dir);
3295
+ const paths = resolveHaivePaths28(root);
3296
+ const config = await loadConfig3(paths);
3297
+ console.log(ui.bold("Hub status"));
3298
+ console.log(
3299
+ ` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
3300
+ );
3301
+ const sharedDir = path27.join(paths.memoriesDir, "shared");
3302
+ if (existsSync29(sharedDir)) {
3303
+ const { readdir: readdir2 } = await import("fs/promises");
3304
+ const sources = (await readdir2(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
3305
+ console.log(`
3306
+ Imported from ${sources.length} source(s):`);
3307
+ for (const src of sources) {
3308
+ const files = (await readdir2(path27.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
3309
+ console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
3310
+ }
3311
+ } else {
3312
+ console.log(ui.dim(" No imported shared memories yet."));
3313
+ }
3314
+ const all = await loadMemoriesFromDir7(paths.memoriesDir);
3315
+ const outgoing = all.filter(
3316
+ ({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
3317
+ );
3318
+ console.log(`
3319
+ This project's shared memories (ready to push): ${outgoing.length}`);
3320
+ if (outgoing.length > 0) {
3321
+ console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
3322
+ }
3323
+ void readFile9;
3324
+ void writeFile15;
3325
+ void saveConfig2;
3326
+ });
3327
+ }
3328
+
2562
3329
  // 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");
3330
+ var program = new Command32();
3331
+ program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.4.1");
2565
3332
  registerInit(program);
2566
3333
  registerMcp(program);
2567
3334
  registerBriefing(program);
@@ -2589,9 +3356,12 @@ registerMemoryUpdate(memory);
2589
3356
  registerMemoryHot(memory);
2590
3357
  registerMemoryTried(memory);
2591
3358
  registerMemoryImport(memory);
3359
+ registerMemoryImportChangelog(memory);
2592
3360
  registerMemoryDigest(memory);
2593
3361
  var session = program.command("session").description("Manage session lifecycle");
2594
3362
  registerSessionEnd(session);
3363
+ registerSnapshot(program);
3364
+ registerHub(program);
2595
3365
  program.parseAsync(process.argv).catch((err) => {
2596
3366
  if (isZodError(err)) {
2597
3367
  for (const issue of err.issues) {