@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 +813 -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";
|
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2359
|
-
const paths =
|
|
2360
|
-
if (!
|
|
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 =
|
|
2433
|
-
await
|
|
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
|
|
2443
|
-
import { existsSync as
|
|
2444
|
-
import
|
|
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
|
|
2448
|
-
findProjectRoot as
|
|
2793
|
+
buildFrontmatter as buildFrontmatter5,
|
|
2794
|
+
findProjectRoot as findProjectRoot29,
|
|
2449
2795
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
2450
2796
|
memoryFilePath as memoryFilePath4,
|
|
2451
|
-
resolveHaivePaths as
|
|
2452
|
-
serializeMemory as
|
|
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 =
|
|
2502
|
-
const paths =
|
|
2503
|
-
if (!
|
|
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) => !
|
|
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 (
|
|
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
|
|
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=${
|
|
2881
|
+
ui.info(`id=${fm.id} file=${path25.relative(root, topicMatch.filePath)}`);
|
|
2536
2882
|
return;
|
|
2537
2883
|
}
|
|
2538
2884
|
}
|
|
2539
|
-
const frontmatter =
|
|
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
|
|
2551
|
-
await
|
|
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=${
|
|
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
|
|
2564
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
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) {
|