@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 +772 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command32 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2359
|
-
const paths =
|
|
2360
|
-
if (!
|
|
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 =
|
|
2433
|
-
await
|
|
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
|
|
2443
|
-
import { existsSync as
|
|
2444
|
-
import
|
|
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
|
|
2448
|
-
findProjectRoot as
|
|
2752
|
+
buildFrontmatter as buildFrontmatter5,
|
|
2753
|
+
findProjectRoot as findProjectRoot29,
|
|
2449
2754
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
2450
2755
|
memoryFilePath as memoryFilePath4,
|
|
2451
|
-
resolveHaivePaths as
|
|
2452
|
-
serializeMemory as
|
|
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 =
|
|
2502
|
-
const paths =
|
|
2503
|
-
if (!
|
|
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) => !
|
|
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 (
|
|
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
|
|
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=${
|
|
2840
|
+
ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
|
|
2536
2841
|
return;
|
|
2537
2842
|
}
|
|
2538
2843
|
}
|
|
2539
|
-
const frontmatter =
|
|
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
|
|
2551
|
-
await
|
|
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=${
|
|
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
|
|
2564
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
3289
|
+
var program = new Command32();
|
|
3290
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.4.0");
|
|
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) {
|