@hiveai/cli 0.7.2 → 0.9.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/Dashboard-Y2AIWFZK.js +0 -0
- package/dist/index.js +1195 -471
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
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 Command40 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
literalMatchesAnyToken,
|
|
15
15
|
loadCodeMap,
|
|
16
16
|
loadMemoriesFromDir,
|
|
17
|
+
loadUsageIndex,
|
|
17
18
|
memoryMatchesAnchorPaths,
|
|
18
19
|
queryCodeMap,
|
|
19
20
|
resolveHaivePaths,
|
|
@@ -51,11 +52,240 @@ var ui = {
|
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
// src/utils/briefing-radar.ts
|
|
56
|
+
import { execFile } from "child_process";
|
|
57
|
+
import { promisify } from "util";
|
|
58
|
+
var exec = promisify(execFile);
|
|
59
|
+
var DEFAULT_DAYS_BACK = 14;
|
|
60
|
+
var DEFAULT_MAX_COMMITS = 5;
|
|
61
|
+
var DEFAULT_MAX_TODOS = 8;
|
|
62
|
+
var DEFAULT_MAX_HOT_FILES = 5;
|
|
63
|
+
var TODO_RE = /\b(?:TODO|FIXME|HACK|XXX)\b[: ]?(.{0,120})/i;
|
|
64
|
+
var SOURCE_GLOBS = [
|
|
65
|
+
"*.ts",
|
|
66
|
+
"*.tsx",
|
|
67
|
+
"*.js",
|
|
68
|
+
"*.jsx",
|
|
69
|
+
"*.py",
|
|
70
|
+
"*.go",
|
|
71
|
+
"*.rs",
|
|
72
|
+
"*.java",
|
|
73
|
+
"*.kt",
|
|
74
|
+
"*.swift",
|
|
75
|
+
"*.rb",
|
|
76
|
+
"*.php",
|
|
77
|
+
"*.cs",
|
|
78
|
+
"*.cpp",
|
|
79
|
+
"*.c",
|
|
80
|
+
"*.h"
|
|
81
|
+
];
|
|
82
|
+
async function isGitRepo(root) {
|
|
83
|
+
try {
|
|
84
|
+
await exec("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root });
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function getRecentCommits(root, daysBack, maxCommits, taskTokens, filePaths) {
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await exec(
|
|
93
|
+
"git",
|
|
94
|
+
[
|
|
95
|
+
"log",
|
|
96
|
+
`--since=${daysBack}.days.ago`,
|
|
97
|
+
"--name-only",
|
|
98
|
+
"--pretty=format:%x1f%h%x1f%ad%x1f%s",
|
|
99
|
+
"--date=short",
|
|
100
|
+
"-n",
|
|
101
|
+
"60"
|
|
102
|
+
],
|
|
103
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024 }
|
|
104
|
+
);
|
|
105
|
+
const blocks = stdout.split("").filter((b) => b.trim().length > 0);
|
|
106
|
+
const commits = [];
|
|
107
|
+
for (let i = 0; i + 2 < blocks.length; i += 3) {
|
|
108
|
+
const sha = blocks[i].trim();
|
|
109
|
+
const date = blocks[i + 1].trim();
|
|
110
|
+
const tail = blocks[i + 2];
|
|
111
|
+
const lines = tail.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
112
|
+
const subject = lines.shift() ?? "";
|
|
113
|
+
const files = lines;
|
|
114
|
+
commits.push({ sha, date, subject, files });
|
|
115
|
+
}
|
|
116
|
+
const lowerTokens = taskTokens?.map((t) => t.toLowerCase()) ?? [];
|
|
117
|
+
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
118
|
+
const scored = commits.map((c) => {
|
|
119
|
+
let score = 0;
|
|
120
|
+
const haystack = (c.subject + " " + c.files.join(" ")).toLowerCase();
|
|
121
|
+
for (const t of lowerTokens) if (haystack.includes(t)) score += 2;
|
|
122
|
+
for (const p of lowerPaths) if (c.files.some((f) => f.toLowerCase().includes(p))) score += 3;
|
|
123
|
+
return { c, score };
|
|
124
|
+
});
|
|
125
|
+
if (lowerTokens.length === 0 && lowerPaths.length === 0) {
|
|
126
|
+
return commits.slice(0, maxCommits);
|
|
127
|
+
}
|
|
128
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, maxCommits).map((s) => s.c);
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function getOpenTodos(root, maxTodos, taskTokens, filePaths) {
|
|
134
|
+
try {
|
|
135
|
+
const includeArgs = SOURCE_GLOBS.flatMap((g) => ["--include", g]);
|
|
136
|
+
const { stdout } = await exec(
|
|
137
|
+
"grep",
|
|
138
|
+
[
|
|
139
|
+
"-rnE",
|
|
140
|
+
"--exclude-dir=node_modules",
|
|
141
|
+
"--exclude-dir=.git",
|
|
142
|
+
"--exclude-dir=dist",
|
|
143
|
+
"--exclude-dir=build",
|
|
144
|
+
"--exclude-dir=.next",
|
|
145
|
+
"--exclude-dir=coverage",
|
|
146
|
+
...includeArgs,
|
|
147
|
+
"\\b(TODO|FIXME|HACK|XXX)\\b",
|
|
148
|
+
"."
|
|
149
|
+
],
|
|
150
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024 }
|
|
151
|
+
).catch((err) => ({ stdout: err.stdout ?? "" }));
|
|
152
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
153
|
+
const parsed = [];
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const m = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
156
|
+
if (!m) continue;
|
|
157
|
+
const [, file, lineNoStr, rest] = m;
|
|
158
|
+
const todoMatch = rest.match(TODO_RE);
|
|
159
|
+
if (!todoMatch) continue;
|
|
160
|
+
const text = (todoMatch[1] ?? "").trim() || rest.trim().slice(0, 120);
|
|
161
|
+
parsed.push({ file: file.replace(/^\.\//, ""), line: Number(lineNoStr), text });
|
|
162
|
+
}
|
|
163
|
+
const lowerTokens = taskTokens?.map((t) => t.toLowerCase()) ?? [];
|
|
164
|
+
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
165
|
+
if (lowerTokens.length === 0 && lowerPaths.length === 0) {
|
|
166
|
+
return parsed.slice(0, maxTodos);
|
|
167
|
+
}
|
|
168
|
+
const scored = parsed.map((t) => {
|
|
169
|
+
let score = 0;
|
|
170
|
+
const hay = (t.file + " " + t.text).toLowerCase();
|
|
171
|
+
for (const tok of lowerTokens) if (hay.includes(tok)) score += 1;
|
|
172
|
+
for (const p of lowerPaths) if (t.file.toLowerCase().includes(p)) score += 2;
|
|
173
|
+
return { t, score };
|
|
174
|
+
});
|
|
175
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, maxTodos).map((s) => s.t);
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
181
|
+
try {
|
|
182
|
+
const { stdout } = await exec(
|
|
183
|
+
"git",
|
|
184
|
+
[
|
|
185
|
+
"log",
|
|
186
|
+
`--since=${daysBack * 6}.days.ago`,
|
|
187
|
+
"--name-only",
|
|
188
|
+
"--pretty=format:"
|
|
189
|
+
],
|
|
190
|
+
{ cwd: root, maxBuffer: 4 * 1024 * 1024 }
|
|
191
|
+
);
|
|
192
|
+
const counts = /* @__PURE__ */ new Map();
|
|
193
|
+
for (const raw of stdout.split("\n")) {
|
|
194
|
+
const f = raw.trim();
|
|
195
|
+
if (!f) continue;
|
|
196
|
+
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
197
|
+
}
|
|
198
|
+
let entries = [...counts.entries()].map(([path37, changes]) => ({ path: path37, changes }));
|
|
199
|
+
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
200
|
+
if (lowerPaths.length > 0) {
|
|
201
|
+
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
202
|
+
}
|
|
203
|
+
return entries.sort((a, b) => b.changes - a.changes).slice(0, maxHotFiles);
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function buildRadar(opts) {
|
|
209
|
+
const inside = await isGitRepo(opts.root);
|
|
210
|
+
if (!inside) {
|
|
211
|
+
return { recentCommits: [], openTodos: [], hotFiles: [], insideGitRepo: false };
|
|
212
|
+
}
|
|
213
|
+
const daysBack = opts.daysBack ?? DEFAULT_DAYS_BACK;
|
|
214
|
+
const [recentCommits, openTodos, hotFiles] = await Promise.all([
|
|
215
|
+
getRecentCommits(opts.root, daysBack, opts.maxCommits ?? DEFAULT_MAX_COMMITS, opts.taskTokens, opts.filePaths),
|
|
216
|
+
getOpenTodos(opts.root, opts.maxTodos ?? DEFAULT_MAX_TODOS, opts.taskTokens, opts.filePaths),
|
|
217
|
+
getHotFiles(opts.root, daysBack, opts.maxHotFiles ?? DEFAULT_MAX_HOT_FILES, opts.filePaths)
|
|
218
|
+
]);
|
|
219
|
+
return { recentCommits, openTodos, hotFiles, insideGitRepo: true };
|
|
220
|
+
}
|
|
221
|
+
function radarHasContent(r) {
|
|
222
|
+
return r.recentCommits.length > 0 || r.openTodos.length > 0 || r.hotFiles.length > 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
54
225
|
// src/commands/briefing.ts
|
|
226
|
+
var RADAR_AUTO_THRESHOLD = 3;
|
|
227
|
+
var CHARS_PER_TOKEN = 4;
|
|
228
|
+
function printRadar(radar, out, reason) {
|
|
229
|
+
if (!radar.insideGitRepo) return;
|
|
230
|
+
if (!radarHasContent(radar)) return;
|
|
231
|
+
const header = reason === "low-memory-signal" ? "=== Project Radar (few relevant memories \u2014 surfacing live signals) ===" : "=== Project Radar ===";
|
|
232
|
+
out(`${ui.bold(header)}
|
|
233
|
+
`);
|
|
234
|
+
if (radar.recentCommits.length > 0) {
|
|
235
|
+
out(ui.bold("Recent commits:"));
|
|
236
|
+
for (const c of radar.recentCommits) {
|
|
237
|
+
const filesBlurb = c.files.slice(0, 3).join(", ");
|
|
238
|
+
const more = c.files.length > 3 ? ` (+${c.files.length - 3})` : "";
|
|
239
|
+
out(` ${ui.dim(c.date)} ${c.sha} ${c.subject}`);
|
|
240
|
+
if (filesBlurb) out(ui.dim(` ${filesBlurb}${more}`));
|
|
241
|
+
}
|
|
242
|
+
out("");
|
|
243
|
+
}
|
|
244
|
+
if (radar.openTodos.length > 0) {
|
|
245
|
+
out(ui.bold("Open TODOs/FIXMEs:"));
|
|
246
|
+
for (const t of radar.openTodos) {
|
|
247
|
+
out(` ${ui.dim(t.file + ":" + t.line)} ${t.text}`);
|
|
248
|
+
}
|
|
249
|
+
out("");
|
|
250
|
+
}
|
|
251
|
+
if (radar.hotFiles.length > 0) {
|
|
252
|
+
out(ui.bold("Hot files (most modified recently):"));
|
|
253
|
+
for (const f of radar.hotFiles) {
|
|
254
|
+
out(` ${f.changes}\xD7 ${ui.dim(f.path)}`);
|
|
255
|
+
}
|
|
256
|
+
out("");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
var TokenBudgetWriter = class {
|
|
260
|
+
constructor(budgetChars) {
|
|
261
|
+
this.budgetChars = budgetChars;
|
|
262
|
+
}
|
|
263
|
+
budgetChars;
|
|
264
|
+
used = 0;
|
|
265
|
+
truncated = false;
|
|
266
|
+
write(text) {
|
|
267
|
+
if (this.truncated) return false;
|
|
268
|
+
const next = this.used + text.length + 1;
|
|
269
|
+
if (next > this.budgetChars) {
|
|
270
|
+
console.log(ui.dim(`... [briefing truncated to fit --max-tokens budget \xB7 ${Math.round(this.used / CHARS_PER_TOKEN)} tokens used]`));
|
|
271
|
+
this.truncated = true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
console.log(text);
|
|
275
|
+
this.used = next;
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
isTruncated() {
|
|
279
|
+
return this.truncated;
|
|
280
|
+
}
|
|
281
|
+
remainingChars() {
|
|
282
|
+
return Math.max(0, this.budgetChars - this.used);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
55
285
|
function registerBriefing(program2) {
|
|
56
286
|
program2.command("briefing").description(
|
|
57
287
|
'Print the full project briefing: last session recap + project context + relevant memories.\n Equivalent to calling get_briefing via MCP. Run before starting any task.\n\n Examples:\n haive briefing\n haive briefing --task "add Stripe payment" --files src/payments/PaymentService.ts\n haive briefing --symbols PaymentService,TenantFilter # look up where symbols live\n'
|
|
58
|
-
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option(
|
|
288
|
+
).option("--task <text>", "what you are about to do \u2014 filters memories by relevance").option("--files <csv>", "comma-separated file paths being worked on (surfaces anchored memories)").option("--symbols <csv>", "symbol names to look up in the code-map (e.g. PaymentService,TenantFilter) \u2014 requires haive index code").option("--max-memories <n>", "cap on memories surfaced", "10").option("--max-tokens <n>", "approximate token budget for the entire briefing (truncates if exceeded)").option("--explain-source", "annotate each memory with [source: <relative-path> \xB7 anchors: <files>] for traceable citations").option("--radar", "force project radar (recent commits, open TODOs, hot files) even when memories are plentiful").option("--no-radar", "disable the project radar even when memories are scarce").option(
|
|
59
289
|
"--scope <scope>",
|
|
60
290
|
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
61
291
|
"all"
|
|
@@ -67,14 +297,29 @@ function registerBriefing(program2) {
|
|
|
67
297
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
68
298
|
const root = findProjectRoot(opts.dir);
|
|
69
299
|
const paths = resolveHaivePaths(root);
|
|
300
|
+
const budgetTokens = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
|
|
301
|
+
const writer = budgetTokens ? new TokenBudgetWriter(budgetTokens * CHARS_PER_TOKEN) : null;
|
|
302
|
+
const out = (text) => {
|
|
303
|
+
if (writer) return writer.write(text);
|
|
304
|
+
console.log(text);
|
|
305
|
+
return true;
|
|
306
|
+
};
|
|
307
|
+
const stopped = () => writer?.isTruncated() ?? false;
|
|
70
308
|
if (!existsSync(paths.memoriesDir)) {
|
|
71
309
|
if (existsSync(paths.projectContext)) {
|
|
72
|
-
|
|
310
|
+
out(`${ui.bold("=== Project Context ===")}
|
|
73
311
|
`);
|
|
74
|
-
|
|
312
|
+
out((await readFile(paths.projectContext, "utf8")).trim());
|
|
313
|
+
out("");
|
|
75
314
|
} else {
|
|
76
315
|
ui.warn("No project-context.md found. Run `haive init` and the `bootstrap_project` MCP prompt to set it up.");
|
|
77
316
|
}
|
|
317
|
+
if (opts.radar !== false && !stopped()) {
|
|
318
|
+
const filePathsEarly = parseCsv(opts.files);
|
|
319
|
+
const tokensEarly = opts.task ? tokenizeQuery(opts.task) : null;
|
|
320
|
+
const radar = await buildRadar({ root, taskTokens: tokensEarly, filePaths: filePathsEarly });
|
|
321
|
+
printRadar(radar, out, "low-memory-signal");
|
|
322
|
+
}
|
|
78
323
|
return;
|
|
79
324
|
}
|
|
80
325
|
const ownMemories = await loadMemoriesFromDir(paths.memoriesDir);
|
|
@@ -112,17 +357,17 @@ function registerBriefing(program2) {
|
|
|
112
357
|
const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
|
|
113
358
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
114
359
|
);
|
|
115
|
-
if (recaps.length > 0) {
|
|
360
|
+
if (recaps.length > 0 && !stopped()) {
|
|
116
361
|
const recap = recaps[0];
|
|
117
362
|
const fm = recap.memory.frontmatter;
|
|
118
363
|
const rev = fm.revision_count ? ` \xB7 revision #${fm.revision_count}` : "";
|
|
119
|
-
|
|
364
|
+
out(`${ui.bold("=== Last Session Recap ===")}
|
|
120
365
|
`);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
366
|
+
out(ui.dim(`${fm.id} (${fm.scope}${rev})`));
|
|
367
|
+
out(recap.memory.body.trim());
|
|
368
|
+
out("");
|
|
124
369
|
}
|
|
125
|
-
if (existsSync(paths.projectContext)) {
|
|
370
|
+
if (existsSync(paths.projectContext) && !stopped()) {
|
|
126
371
|
const ctx = await readFile(paths.projectContext, "utf8");
|
|
127
372
|
const isTemplate = ctx.includes("TODO \u2014 high-level overview") || ctx.includes("Generated by `haive init`");
|
|
128
373
|
if (isTemplate) {
|
|
@@ -132,14 +377,14 @@ function registerBriefing(program2) {
|
|
|
132
377
|
ui.warn(
|
|
133
378
|
"Fix: in your AI client, invoke the MCP prompt bootstrap_project to auto-fill it from your codebase."
|
|
134
379
|
);
|
|
135
|
-
|
|
380
|
+
out("");
|
|
136
381
|
} else {
|
|
137
|
-
|
|
382
|
+
out(`${ui.bold("=== Project Context ===")}
|
|
138
383
|
`);
|
|
139
|
-
|
|
140
|
-
|
|
384
|
+
out(ctx.trim());
|
|
385
|
+
out("");
|
|
141
386
|
}
|
|
142
|
-
} else {
|
|
387
|
+
} else if (!existsSync(paths.projectContext)) {
|
|
143
388
|
ui.warn(
|
|
144
389
|
"No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
|
|
145
390
|
);
|
|
@@ -177,54 +422,85 @@ function registerBriefing(program2) {
|
|
|
177
422
|
if (draftCount > 0) {
|
|
178
423
|
ui.info(`(${draftCount} draft memories excluded \u2014 use --include-draft to show)`);
|
|
179
424
|
}
|
|
425
|
+
if (opts.radar !== false && !stopped()) {
|
|
426
|
+
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
427
|
+
out("");
|
|
428
|
+
printRadar(radar, out, "low-memory-signal");
|
|
429
|
+
}
|
|
180
430
|
return;
|
|
181
431
|
}
|
|
182
|
-
|
|
432
|
+
if (stopped()) return;
|
|
433
|
+
const usageIndex = await loadUsageIndex(paths).catch(() => null);
|
|
434
|
+
out(`${ui.bold("=== Relevant Memories ===")}
|
|
183
435
|
`);
|
|
184
436
|
for (const item of top) {
|
|
437
|
+
if (stopped()) break;
|
|
185
438
|
const fm = item.memory.frontmatter;
|
|
186
439
|
const badge = ui.statusBadge(fm.status);
|
|
187
440
|
const draftMarker = fm.status === "draft" ? ui.yellow(" [DRAFT]") : "";
|
|
188
441
|
const unverifiedMarker = fm.status === "proposed" ? ui.yellow(" [UNVERIFIED]") : "";
|
|
189
442
|
const originMarker = item.origin ? ` ${ui.yellow("[from " + item.origin + "]")}` : "";
|
|
190
|
-
|
|
191
|
-
|
|
443
|
+
const reads = usageIndex?.by_id[fm.id]?.read_count ?? 0;
|
|
444
|
+
const hitMarker = reads > 0 ? ` ${ui.dim("\xB7 " + reads + "\xD7 read")}` : "";
|
|
445
|
+
out(
|
|
446
|
+
`${ui.bold(fm.id)} ${ui.dim(fm.scope + "/" + fm.type)} ${badge}${draftMarker}${unverifiedMarker}${originMarker}${hitMarker}`
|
|
192
447
|
);
|
|
193
|
-
|
|
194
|
-
|
|
448
|
+
if (opts.explainSource) {
|
|
449
|
+
const relPath = path.relative(root, item.filePath);
|
|
450
|
+
const anchorPaths = fm.anchor?.paths ?? [];
|
|
451
|
+
const anchorSymbols = fm.anchor?.symbols ?? [];
|
|
452
|
+
const parts = [`source: ${relPath}`];
|
|
453
|
+
if (anchorPaths.length > 0) parts.push(`paths: ${anchorPaths.join(", ")}`);
|
|
454
|
+
if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
|
|
455
|
+
out(ui.dim(` [${parts.join(" \xB7 ")}]`));
|
|
456
|
+
}
|
|
457
|
+
out(item.memory.body.trim());
|
|
458
|
+
out("");
|
|
195
459
|
}
|
|
196
|
-
|
|
460
|
+
if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
197
461
|
const ids = top.map(({ memory: mem }) => mem.frontmatter.id);
|
|
198
462
|
if (ids.length > 0) {
|
|
199
463
|
await trackReads(paths, ids).catch(() => {
|
|
200
464
|
});
|
|
201
465
|
}
|
|
466
|
+
const radarForced = opts.radar === true;
|
|
467
|
+
const radarAuto = opts.radar !== false && top.length < RADAR_AUTO_THRESHOLD;
|
|
468
|
+
if ((radarForced || radarAuto) && !stopped()) {
|
|
469
|
+
const radar = await buildRadar({ root, taskTokens: tokens, filePaths });
|
|
470
|
+
if (radarHasContent(radar)) {
|
|
471
|
+
out("");
|
|
472
|
+
printRadar(radar, out, radarForced ? "forced" : "low-memory-signal");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
202
475
|
const requestedSymbols = (opts.symbols ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
203
|
-
if (requestedSymbols.length > 0) {
|
|
476
|
+
if (requestedSymbols.length > 0 && !stopped()) {
|
|
204
477
|
const codeMap = await loadCodeMap(paths);
|
|
205
478
|
if (!codeMap) {
|
|
206
479
|
ui.warn("No code-map found. Run `haive index code` first to enable symbol lookup.");
|
|
207
480
|
} else {
|
|
208
|
-
|
|
481
|
+
out(`
|
|
209
482
|
${ui.bold("=== Symbol Locations ===")}
|
|
210
483
|
`);
|
|
211
484
|
for (const sym of requestedSymbols) {
|
|
485
|
+
if (stopped()) break;
|
|
212
486
|
const { files } = queryCodeMap(codeMap, { symbol: sym });
|
|
213
487
|
if (files.length === 0) {
|
|
214
|
-
|
|
488
|
+
out(`${ui.dim(sym)} (not found in code-map)`);
|
|
215
489
|
} else {
|
|
216
490
|
for (const f of files) {
|
|
491
|
+
if (stopped()) break;
|
|
217
492
|
const exports = f.entry.exports.filter(
|
|
218
493
|
(e) => e.name.toLowerCase().includes(sym.toLowerCase())
|
|
219
494
|
);
|
|
220
495
|
for (const e of exports) {
|
|
496
|
+
if (stopped()) break;
|
|
221
497
|
const desc = e.description ? ` \u2014 ${e.description}` : "";
|
|
222
|
-
|
|
498
|
+
out(`${ui.bold(e.name)} ${ui.dim(f.path + ":" + e.line)} [${e.kind}]${desc}`);
|
|
223
499
|
}
|
|
224
500
|
}
|
|
225
501
|
}
|
|
226
502
|
}
|
|
227
|
-
|
|
503
|
+
out("");
|
|
228
504
|
}
|
|
229
505
|
}
|
|
230
506
|
});
|
|
@@ -759,6 +1035,13 @@ var HAIVE_MCP_ENTRY = {
|
|
|
759
1035
|
command: "haive-mcp",
|
|
760
1036
|
args: []
|
|
761
1037
|
};
|
|
1038
|
+
function projectMcpEntry(root) {
|
|
1039
|
+
return {
|
|
1040
|
+
command: "haive-mcp",
|
|
1041
|
+
args: [],
|
|
1042
|
+
env: { HAIVE_PROJECT_ROOT: root }
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
762
1045
|
function cursorMcpPath() {
|
|
763
1046
|
return path5.join(HOME, ".cursor", "mcp.json");
|
|
764
1047
|
}
|
|
@@ -877,6 +1160,61 @@ async function autoConfigureMcpClients() {
|
|
|
877
1160
|
}
|
|
878
1161
|
return results;
|
|
879
1162
|
}
|
|
1163
|
+
async function configureProjectMcpClients(root) {
|
|
1164
|
+
const entry = projectMcpEntry(root);
|
|
1165
|
+
const results = [];
|
|
1166
|
+
try {
|
|
1167
|
+
const cursorPath = path5.join(root, ".cursor", "mcp.json");
|
|
1168
|
+
let config = {};
|
|
1169
|
+
if (existsSync4(cursorPath)) {
|
|
1170
|
+
try {
|
|
1171
|
+
config = JSON.parse(await readFile3(cursorPath, "utf8"));
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
config.mcpServers ??= {};
|
|
1176
|
+
config.mcpServers["haive"] = entry;
|
|
1177
|
+
await mkdir(path5.dirname(cursorPath), { recursive: true });
|
|
1178
|
+
await writeFile(cursorPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1179
|
+
results.push({ client: "Cursor (project)", status: "configured", path: cursorPath });
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
results.push({ client: "Cursor (project)", status: "error", error: String(err) });
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const vscodePath = path5.join(root, ".vscode", "mcp.json");
|
|
1185
|
+
let config = {};
|
|
1186
|
+
if (existsSync4(vscodePath)) {
|
|
1187
|
+
try {
|
|
1188
|
+
config = JSON.parse(await readFile3(vscodePath, "utf8"));
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
config.servers ??= {};
|
|
1193
|
+
config.servers["haive"] = { ...entry, type: "stdio" };
|
|
1194
|
+
await mkdir(path5.dirname(vscodePath), { recursive: true });
|
|
1195
|
+
await writeFile(vscodePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1196
|
+
results.push({ client: "VS Code (workspace)", status: "configured", path: vscodePath });
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
results.push({ client: "VS Code (workspace)", status: "error", error: String(err) });
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
const mcpPath = path5.join(root, ".mcp.json");
|
|
1202
|
+
let config = {};
|
|
1203
|
+
if (existsSync4(mcpPath)) {
|
|
1204
|
+
try {
|
|
1205
|
+
config = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
config.mcpServers ??= {};
|
|
1210
|
+
config.mcpServers["haive"] = { ...entry, type: "stdio" };
|
|
1211
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
1212
|
+
results.push({ client: "Claude Code (project)", status: "configured", path: mcpPath });
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
results.push({ client: "Claude Code (project)", status: "error", error: String(err) });
|
|
1215
|
+
}
|
|
1216
|
+
return results;
|
|
1217
|
+
}
|
|
880
1218
|
|
|
881
1219
|
// src/commands/init-stack-packs.ts
|
|
882
1220
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -1671,18 +2009,21 @@ jobs:
|
|
|
1671
2009
|
`;
|
|
1672
2010
|
function registerInit(program2) {
|
|
1673
2011
|
program2.command("init").description(
|
|
1674
|
-
"Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Add --manual
|
|
2012
|
+
"Initialize a hAIve project \u2014 autopilot mode ON by default (zero human intervention).\n Auto-bootstraps project-context.md from local files and seeds detected stack packs.\n Add --manual to control memory approval and session recaps yourself.\n Add --no-bootstrap and --stack none to disable the auto-features."
|
|
1675
2013
|
).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
|
|
1676
2014
|
"--manual",
|
|
1677
2015
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
1678
2016
|
).option(
|
|
1679
2017
|
"--bootstrap",
|
|
1680
|
-
"auto-generate .ai/project-context.md from package.json, README, and directory structure (
|
|
2018
|
+
"auto-generate .ai/project-context.md from package.json, README, and directory structure (ON by default in autopilot)"
|
|
2019
|
+
).option(
|
|
2020
|
+
"--no-bootstrap",
|
|
2021
|
+
"skip the project-context auto-generation (only the default template is written)"
|
|
1681
2022
|
).option(
|
|
1682
2023
|
"--stack <stacks>",
|
|
1683
2024
|
`pre-seed validated memory packs for the given stacks (comma-separated).
|
|
1684
2025
|
Supported: ${SUPPORTED_STACKS.join(", ")}.
|
|
1685
|
-
|
|
2026
|
+
Defaults to 'auto' in autopilot mode (detects from package.json). Pass 'none' to disable.`
|
|
1686
2027
|
).option(
|
|
1687
2028
|
"--no-mcp-setup",
|
|
1688
2029
|
"skip auto-configuring haive-mcp in Cursor / VS Code / Claude Code"
|
|
@@ -1690,6 +2031,8 @@ function registerInit(program2) {
|
|
|
1690
2031
|
const root = path7.resolve(opts.dir);
|
|
1691
2032
|
const paths = resolveHaivePaths4(root);
|
|
1692
2033
|
const autopilot = opts.manual !== true;
|
|
2034
|
+
const wantBootstrap = opts.bootstrap === void 0 ? autopilot : opts.bootstrap;
|
|
2035
|
+
const wantStack = opts.stack === void 0 ? autopilot ? "auto" : void 0 : opts.stack === "none" ? void 0 : opts.stack;
|
|
1693
2036
|
if (existsSync6(paths.haiveDir)) {
|
|
1694
2037
|
ui.warn(`.ai/ already exists at ${paths.haiveDir} \u2014 leaving existing files in place.`);
|
|
1695
2038
|
}
|
|
@@ -1698,7 +2041,7 @@ function registerInit(program2) {
|
|
|
1698
2041
|
await mkdir3(paths.moduleDir, { recursive: true });
|
|
1699
2042
|
await mkdir3(paths.modulesContextDir, { recursive: true });
|
|
1700
2043
|
if (!existsSync6(paths.projectContext)) {
|
|
1701
|
-
if (
|
|
2044
|
+
if (wantBootstrap) {
|
|
1702
2045
|
ui.info("Bootstrapping project context from local files\u2026");
|
|
1703
2046
|
try {
|
|
1704
2047
|
const context = await generateBootstrapContext(root);
|
|
@@ -1727,7 +2070,7 @@ function registerInit(program2) {
|
|
|
1727
2070
|
await writeBridge(root, ".cursorrules");
|
|
1728
2071
|
await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
|
|
1729
2072
|
}
|
|
1730
|
-
const stacksToSeed = await resolveStacksToSeed(root,
|
|
2073
|
+
const stacksToSeed = await resolveStacksToSeed(root, wantStack);
|
|
1731
2074
|
if (stacksToSeed.length > 0) {
|
|
1732
2075
|
let totalSeeded = 0;
|
|
1733
2076
|
for (const stack of stacksToSeed) {
|
|
@@ -1792,7 +2135,18 @@ function registerInit(program2) {
|
|
|
1792
2135
|
"No supported AI client detected (Cursor, VS Code, Claude Code, Windsurf).\n Configure manually: add haive-mcp to your client's MCP config.\n See: https://github.com/Doucs91/hAIve#mcp-setup"
|
|
1793
2136
|
);
|
|
1794
2137
|
}
|
|
1795
|
-
|
|
2138
|
+
const projectMcpResults = await configureProjectMcpClients(root);
|
|
2139
|
+
for (const r of projectMcpResults) {
|
|
2140
|
+
if (r.status === "configured") {
|
|
2141
|
+
ui.success(`haive-mcp project config written (${path7.relative(root, r.path)})`);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
await ensureGitignoreEntries(root, [
|
|
2145
|
+
".cursor/mcp.json",
|
|
2146
|
+
".vscode/mcp.json",
|
|
2147
|
+
".mcp.json"
|
|
2148
|
+
]);
|
|
2149
|
+
if (configured.length > 0 || projectMcpResults.some((r) => r.status === "configured")) {
|
|
1796
2150
|
ui.info(
|
|
1797
2151
|
ui.dim(" \u2192 Restart your AI client for MCP changes to take effect.")
|
|
1798
2152
|
);
|
|
@@ -1867,13 +2221,123 @@ async function writeBridge(root, relPath) {
|
|
|
1867
2221
|
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
1868
2222
|
ui.success(`Created bridge ${relPath}`);
|
|
1869
2223
|
}
|
|
2224
|
+
async function ensureGitignoreEntries(root, patterns) {
|
|
2225
|
+
try {
|
|
2226
|
+
const gitignorePath = path7.join(root, ".gitignore");
|
|
2227
|
+
let existing = "";
|
|
2228
|
+
if (existsSync6(gitignorePath)) {
|
|
2229
|
+
existing = await readFile4(gitignorePath, "utf8");
|
|
2230
|
+
}
|
|
2231
|
+
const lines = existing.split("\n");
|
|
2232
|
+
const missing = patterns.filter((p) => !lines.some((l) => l.trim() === p));
|
|
2233
|
+
if (missing.length === 0) return;
|
|
2234
|
+
const toAppend = (existing.endsWith("\n") || existing === "" ? "" : "\n") + "# hAIve project-level MCP configs (machine-specific absolute paths)\n" + missing.join("\n") + "\n";
|
|
2235
|
+
await writeFile3(gitignorePath, existing + toAppend, "utf8");
|
|
2236
|
+
} catch {
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
1870
2239
|
|
|
1871
2240
|
// src/commands/install-hooks.ts
|
|
1872
|
-
import { mkdir as
|
|
1873
|
-
import { existsSync as
|
|
1874
|
-
import
|
|
2241
|
+
import { mkdir as mkdir5, writeFile as writeFile5, chmod, readFile as readFile6 } from "fs/promises";
|
|
2242
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2243
|
+
import path9 from "path";
|
|
1875
2244
|
import "commander";
|
|
1876
2245
|
import { findProjectRoot as findProjectRoot6 } from "@hiveai/core";
|
|
2246
|
+
|
|
2247
|
+
// src/utils/claude-hooks.ts
|
|
2248
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2249
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
2250
|
+
import path8 from "path";
|
|
2251
|
+
var HAIVE_HOOK_TAG = "haive-passive-capture";
|
|
2252
|
+
var POST_TOOL_USE_GROUP = {
|
|
2253
|
+
matcher: "Edit|Write|Bash",
|
|
2254
|
+
hooks: [
|
|
2255
|
+
{
|
|
2256
|
+
type: "command",
|
|
2257
|
+
command: "haive observe",
|
|
2258
|
+
haive_tag: HAIVE_HOOK_TAG
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
2261
|
+
};
|
|
2262
|
+
var SESSION_END_GROUP = {
|
|
2263
|
+
hooks: [
|
|
2264
|
+
{
|
|
2265
|
+
type: "command",
|
|
2266
|
+
command: "haive session end --quiet --auto",
|
|
2267
|
+
haive_tag: HAIVE_HOOK_TAG
|
|
2268
|
+
}
|
|
2269
|
+
]
|
|
2270
|
+
};
|
|
2271
|
+
function dropHaiveGroups(groups) {
|
|
2272
|
+
return groups.filter(
|
|
2273
|
+
(g) => !g.hooks.some((h) => h.haive_tag === HAIVE_HOOK_TAG)
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
function patchClaudeSettings(input) {
|
|
2277
|
+
const settings = input ? { ...input } : {};
|
|
2278
|
+
const hooks = settings.hooks ? { ...settings.hooks } : {};
|
|
2279
|
+
hooks.PostToolUse = [
|
|
2280
|
+
...dropHaiveGroups(hooks.PostToolUse ?? []),
|
|
2281
|
+
POST_TOOL_USE_GROUP
|
|
2282
|
+
];
|
|
2283
|
+
hooks.SessionEnd = [
|
|
2284
|
+
...dropHaiveGroups(hooks.SessionEnd ?? []),
|
|
2285
|
+
SESSION_END_GROUP
|
|
2286
|
+
];
|
|
2287
|
+
settings.hooks = hooks;
|
|
2288
|
+
return settings;
|
|
2289
|
+
}
|
|
2290
|
+
function unpatchClaudeSettings(input) {
|
|
2291
|
+
const settings = input ? { ...input } : {};
|
|
2292
|
+
if (!settings.hooks) return settings;
|
|
2293
|
+
const hooks = { ...settings.hooks };
|
|
2294
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
2295
|
+
const cleaned = dropHaiveGroups(groups);
|
|
2296
|
+
if (cleaned.length === 0) {
|
|
2297
|
+
delete hooks[event];
|
|
2298
|
+
} else {
|
|
2299
|
+
hooks[event] = cleaned;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
settings.hooks = hooks;
|
|
2303
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
2304
|
+
return settings;
|
|
2305
|
+
}
|
|
2306
|
+
async function installClaudeHooksAtPath(settingsPath) {
|
|
2307
|
+
let raw = null;
|
|
2308
|
+
let created = false;
|
|
2309
|
+
if (existsSync7(settingsPath)) {
|
|
2310
|
+
try {
|
|
2311
|
+
raw = JSON.parse(await readFile5(settingsPath, "utf8"));
|
|
2312
|
+
} catch {
|
|
2313
|
+
throw new Error(`${settingsPath} exists but is not valid JSON. Fix it manually first.`);
|
|
2314
|
+
}
|
|
2315
|
+
} else {
|
|
2316
|
+
created = true;
|
|
2317
|
+
}
|
|
2318
|
+
const patched = patchClaudeSettings(raw);
|
|
2319
|
+
await mkdir4(path8.dirname(settingsPath), { recursive: true });
|
|
2320
|
+
await writeFile4(settingsPath, JSON.stringify(patched, null, 2) + "\n", "utf8");
|
|
2321
|
+
return { settingsPath, created };
|
|
2322
|
+
}
|
|
2323
|
+
async function uninstallClaudeHooksAtPath(settingsPath) {
|
|
2324
|
+
if (!existsSync7(settingsPath)) {
|
|
2325
|
+
return { settingsPath, created: false };
|
|
2326
|
+
}
|
|
2327
|
+
const raw = JSON.parse(await readFile5(settingsPath, "utf8"));
|
|
2328
|
+
const cleaned = unpatchClaudeSettings(raw);
|
|
2329
|
+
await writeFile4(settingsPath, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
|
|
2330
|
+
return { settingsPath, created: false };
|
|
2331
|
+
}
|
|
2332
|
+
function defaultClaudeSettingsPath(scope, projectRoot) {
|
|
2333
|
+
if (scope === "user") {
|
|
2334
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
2335
|
+
return path8.join(home, ".claude", "settings.json");
|
|
2336
|
+
}
|
|
2337
|
+
return path8.join(projectRoot, ".claude", "settings.local.json");
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/commands/install-hooks.ts
|
|
1877
2341
|
var HOOK_MARKER = "# hAIve auto-generated";
|
|
1878
2342
|
var POST_MERGE_BODY = `#!/bin/sh
|
|
1879
2343
|
${HOOK_MARKER} \u2014 keep this block to allow upgrades. Hand-edit anything outside it.
|
|
@@ -1922,50 +2386,192 @@ var HOOKS = [
|
|
|
1922
2386
|
{ name: "post-rewrite", body: POST_MERGE_BODY },
|
|
1923
2387
|
{ name: "pre-push", body: PRE_PUSH_BODY }
|
|
1924
2388
|
];
|
|
2389
|
+
async function installGitHooks(opts) {
|
|
2390
|
+
const root = findProjectRoot6(opts.dir);
|
|
2391
|
+
const gitDir = path9.join(root, ".git");
|
|
2392
|
+
if (!existsSync8(gitDir)) {
|
|
2393
|
+
ui.error(`No .git directory at ${root}.`);
|
|
2394
|
+
process.exitCode = 1;
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const hooksDir = path9.join(gitDir, "hooks");
|
|
2398
|
+
await mkdir5(hooksDir, { recursive: true });
|
|
2399
|
+
let installed = 0;
|
|
2400
|
+
let skipped = 0;
|
|
2401
|
+
for (const { name, body } of HOOKS) {
|
|
2402
|
+
const file = path9.join(hooksDir, name);
|
|
2403
|
+
if (existsSync8(file) && !opts.force) {
|
|
2404
|
+
const existing = await readFile6(file, "utf8");
|
|
2405
|
+
if (!existing.includes(HOOK_MARKER)) {
|
|
2406
|
+
ui.warn(`${name} already exists and was not written by hAIve. Re-run with --force to overwrite.`);
|
|
2407
|
+
skipped++;
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
await writeFile5(file, body, "utf8");
|
|
2412
|
+
await chmod(file, 493);
|
|
2413
|
+
installed++;
|
|
2414
|
+
}
|
|
2415
|
+
ui.success(`Installed ${installed} git hook(s) in .git/hooks/${skipped ? `, skipped ${skipped}` : ""}`);
|
|
2416
|
+
ui.info("post-merge: haive sync runs after every pull/merge.");
|
|
2417
|
+
ui.info("pre-push: haive precommit runs before every push (advisory, never blocks).");
|
|
2418
|
+
ui.info(" Set HAIVE_BLOCK=1 in your shell to make pre-push blocking.");
|
|
2419
|
+
}
|
|
2420
|
+
async function installClaudeHooks(opts) {
|
|
2421
|
+
const root = findProjectRoot6(opts.dir);
|
|
2422
|
+
const scope = opts.scope ?? "user";
|
|
2423
|
+
const settingsPath = opts.settings ?? defaultClaudeSettingsPath(scope, root);
|
|
2424
|
+
if (opts.uninstall) {
|
|
2425
|
+
const result = await uninstallClaudeHooksAtPath(settingsPath);
|
|
2426
|
+
ui.success(`Removed hAIve hooks from ${result.settingsPath}`);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
try {
|
|
2430
|
+
const result = await installClaudeHooksAtPath(settingsPath);
|
|
2431
|
+
if (result.created) {
|
|
2432
|
+
ui.success(`Created ${result.settingsPath} with hAIve passive-capture hooks`);
|
|
2433
|
+
} else {
|
|
2434
|
+
ui.success(`Patched ${result.settingsPath} (existing user hooks preserved)`);
|
|
2435
|
+
}
|
|
2436
|
+
} catch (err) {
|
|
2437
|
+
ui.error(err instanceof Error ? err.message : String(err));
|
|
2438
|
+
process.exitCode = 1;
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
ui.info("PostToolUse hook: `haive observe` runs after every Edit/Write/Bash");
|
|
2442
|
+
ui.info(" (appends a JSON line to .ai/.cache/observations.jsonl)");
|
|
2443
|
+
ui.info("SessionEnd hook: `haive session end --auto --quiet` distills observations");
|
|
2444
|
+
ui.info(" into a session_recap memory at session close");
|
|
2445
|
+
ui.info("Restart Claude Code (or open a new conversation) for the hooks to take effect.");
|
|
2446
|
+
ui.info(`Run \`haive install-hooks claude --uninstall\` to remove.`);
|
|
2447
|
+
}
|
|
1925
2448
|
function registerInstallHooks(program2) {
|
|
1926
|
-
program2.command("install-hooks").description(
|
|
1927
|
-
"Install
|
|
1928
|
-
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks").action(async (opts) => {
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2449
|
+
program2.command("install-hooks [target]").description(
|
|
2450
|
+
"Install hAIve hooks. Targets:\n\n git (default) post-merge / post-rewrite / pre-push for haive sync + precommit\n claude PostToolUse + SessionEnd hooks in ~/.claude/settings.json\n for passive observation capture (Claude Code only)\n\n Examples:\n haive install-hooks # git hooks (legacy default)\n haive install-hooks git\n haive install-hooks claude\n haive install-hooks claude --scope project\n haive install-hooks claude --uninstall\n"
|
|
2451
|
+
).option("-d, --dir <dir>", "project root").option("--force", "overwrite existing hooks (git target only)").option("--scope <scope>", "claude target: 'user' (~/.claude) or 'project' (.claude/)", "user").option("--uninstall", "remove previously installed hAIve hooks (claude target only)").option("--settings <path>", "explicit path to settings.json (claude target only)").action(async (target, opts) => {
|
|
2452
|
+
const t = (target ?? "git").toLowerCase();
|
|
2453
|
+
if (t === "git") {
|
|
2454
|
+
await installGitHooks(opts);
|
|
2455
|
+
} else if (t === "claude") {
|
|
2456
|
+
await installClaudeHooks(opts);
|
|
2457
|
+
} else {
|
|
2458
|
+
ui.error(`Unknown target: ${target}. Available: git, claude`);
|
|
1933
2459
|
process.exitCode = 1;
|
|
1934
|
-
return;
|
|
1935
2460
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// src/commands/observe.ts
|
|
2465
|
+
import { appendFile, mkdir as mkdir6 } from "fs/promises";
|
|
2466
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2467
|
+
import path10 from "path";
|
|
2468
|
+
import "commander";
|
|
2469
|
+
import { findProjectRoot as findProjectRoot7, resolveHaivePaths as resolveHaivePaths5 } from "@hiveai/core";
|
|
2470
|
+
var MAX_STDIN_BYTES = 256 * 1024;
|
|
2471
|
+
var TRUNCATE_FIELD = 800;
|
|
2472
|
+
function truncate(s, max = TRUNCATE_FIELD) {
|
|
2473
|
+
if (s == null) return "";
|
|
2474
|
+
const str = typeof s === "string" ? s : JSON.stringify(s);
|
|
2475
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str;
|
|
2476
|
+
}
|
|
2477
|
+
function extractFiles(payload) {
|
|
2478
|
+
const files = /* @__PURE__ */ new Set();
|
|
2479
|
+
const input = payload.tool_input ?? {};
|
|
2480
|
+
for (const k of ["file_path", "path", "notebook_path"]) {
|
|
2481
|
+
const v = input[k];
|
|
2482
|
+
if (typeof v === "string") files.add(v);
|
|
2483
|
+
}
|
|
2484
|
+
const cmd = input["command"];
|
|
2485
|
+
if (typeof cmd === "string") {
|
|
2486
|
+
const matches = cmd.match(/[\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|kt|swift|rb|php|cs|cpp|c|h|md|json|yml|yaml)\b/g);
|
|
2487
|
+
if (matches) for (const m of matches) files.add(m);
|
|
2488
|
+
}
|
|
2489
|
+
return [...files].slice(0, 8);
|
|
2490
|
+
}
|
|
2491
|
+
function buildSummary(payload) {
|
|
2492
|
+
const tool = payload.tool_name ?? "?";
|
|
2493
|
+
const input = payload.tool_input ?? {};
|
|
2494
|
+
if (tool === "Bash") return `Bash: ${truncate(input["command"], 200)}`;
|
|
2495
|
+
if (tool === "Edit") return `Edit ${truncate(input["file_path"], 200)}`;
|
|
2496
|
+
if (tool === "Write") return `Write ${truncate(input["file_path"], 200)}`;
|
|
2497
|
+
return `${tool}: ${truncate(input, 200)}`;
|
|
2498
|
+
}
|
|
2499
|
+
async function readStdin(maxBytes) {
|
|
2500
|
+
if (process.stdin.isTTY) return "";
|
|
2501
|
+
return await new Promise((resolve) => {
|
|
2502
|
+
const chunks = [];
|
|
2503
|
+
let total = 0;
|
|
2504
|
+
let done = false;
|
|
2505
|
+
const finish = () => {
|
|
2506
|
+
if (done) return;
|
|
2507
|
+
done = true;
|
|
2508
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
2509
|
+
};
|
|
2510
|
+
process.stdin.on("data", (c) => {
|
|
2511
|
+
total += c.length;
|
|
2512
|
+
if (total > maxBytes) {
|
|
2513
|
+
process.stdin.destroy();
|
|
2514
|
+
finish();
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
chunks.push(c);
|
|
2518
|
+
});
|
|
2519
|
+
process.stdin.on("end", finish);
|
|
2520
|
+
process.stdin.on("error", finish);
|
|
2521
|
+
setTimeout(finish, 2e3);
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
function registerObserve(program2) {
|
|
2525
|
+
program2.command("observe").description(
|
|
2526
|
+
"Passive-capture endpoint for Claude Code PostToolUse hooks.\n\n Reads a JSON payload on stdin and appends an observation record to\n .ai/.cache/observations.jsonl. Always exits 0; never blocks the agent.\n Wired up automatically by `haive install-hooks claude`."
|
|
2527
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2528
|
+
try {
|
|
2529
|
+
const raw = await readStdin(MAX_STDIN_BYTES);
|
|
2530
|
+
if (!raw.trim()) return;
|
|
2531
|
+
let payload;
|
|
2532
|
+
try {
|
|
2533
|
+
payload = JSON.parse(raw);
|
|
2534
|
+
} catch {
|
|
2535
|
+
return;
|
|
1949
2536
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2537
|
+
const root = (() => {
|
|
2538
|
+
try {
|
|
2539
|
+
return findProjectRoot7(opts.dir ?? payload.cwd);
|
|
2540
|
+
} catch {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
})();
|
|
2544
|
+
if (!root) return;
|
|
2545
|
+
const paths = resolveHaivePaths5(root);
|
|
2546
|
+
if (!existsSync9(paths.haiveDir)) return;
|
|
2547
|
+
const observation = {
|
|
2548
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2549
|
+
session_id: payload.session_id,
|
|
2550
|
+
cwd: payload.cwd,
|
|
2551
|
+
tool: payload.tool_name ?? "?",
|
|
2552
|
+
summary: buildSummary(payload),
|
|
2553
|
+
files: extractFiles(payload)
|
|
2554
|
+
};
|
|
2555
|
+
const cacheDir = path10.join(paths.haiveDir, ".cache");
|
|
2556
|
+
await mkdir6(cacheDir, { recursive: true });
|
|
2557
|
+
await appendFile(
|
|
2558
|
+
path10.join(cacheDir, "observations.jsonl"),
|
|
2559
|
+
JSON.stringify(observation) + "\n",
|
|
2560
|
+
"utf8"
|
|
2561
|
+
);
|
|
2562
|
+
} catch {
|
|
2563
|
+
}
|
|
1958
2564
|
});
|
|
1959
2565
|
}
|
|
1960
2566
|
|
|
1961
2567
|
// src/commands/mcp.ts
|
|
1962
2568
|
import { spawn } from "child_process";
|
|
1963
|
-
import { existsSync as
|
|
2569
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1964
2570
|
import { createRequire } from "module";
|
|
1965
|
-
import
|
|
2571
|
+
import path11 from "path";
|
|
1966
2572
|
import { fileURLToPath } from "url";
|
|
1967
2573
|
import "commander";
|
|
1968
|
-
import { findProjectRoot as
|
|
2574
|
+
import { findProjectRoot as findProjectRoot8 } from "@hiveai/core";
|
|
1969
2575
|
var require2 = createRequire(import.meta.url);
|
|
1970
2576
|
function registerMcp(program2) {
|
|
1971
2577
|
program2.command("mcp").description(
|
|
@@ -1982,7 +2588,7 @@ function registerMcp(program2) {
|
|
|
1982
2588
|
VS Code: code --add-mcp '{"name":"haive","command":"haive-mcp",...}'
|
|
1983
2589
|
`
|
|
1984
2590
|
).option("-d, --dir <dir>", "project root (defaults to nearest .ai/ or .git/)").action((opts) => {
|
|
1985
|
-
const root =
|
|
2591
|
+
const root = findProjectRoot8(opts.dir);
|
|
1986
2592
|
const bin = locateMcpBin();
|
|
1987
2593
|
if (!bin) {
|
|
1988
2594
|
ui.error(
|
|
@@ -2000,36 +2606,36 @@ function registerMcp(program2) {
|
|
|
2000
2606
|
function locateMcpBin() {
|
|
2001
2607
|
try {
|
|
2002
2608
|
const pkgPath = require2.resolve("@hiveai/mcp/package.json");
|
|
2003
|
-
const pkgDir =
|
|
2004
|
-
const candidate =
|
|
2005
|
-
if (
|
|
2609
|
+
const pkgDir = path11.dirname(pkgPath);
|
|
2610
|
+
const candidate = path11.join(pkgDir, "dist", "index.js");
|
|
2611
|
+
if (existsSync10(candidate)) return candidate;
|
|
2006
2612
|
} catch {
|
|
2007
2613
|
}
|
|
2008
|
-
const here =
|
|
2009
|
-
const sibling =
|
|
2010
|
-
if (
|
|
2614
|
+
const here = path11.dirname(fileURLToPath(import.meta.url));
|
|
2615
|
+
const sibling = path11.resolve(here, "..", "..", "..", "mcp", "dist", "index.js");
|
|
2616
|
+
if (existsSync10(sibling)) return sibling;
|
|
2011
2617
|
return null;
|
|
2012
2618
|
}
|
|
2013
2619
|
|
|
2014
2620
|
// src/commands/sync.ts
|
|
2015
2621
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2016
|
-
import { readFile as
|
|
2017
|
-
import { existsSync as
|
|
2018
|
-
import
|
|
2622
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
|
|
2623
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2624
|
+
import path12 from "path";
|
|
2019
2625
|
import "commander";
|
|
2020
2626
|
import {
|
|
2021
2627
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
2022
2628
|
buildFrontmatter as buildFrontmatter2,
|
|
2023
|
-
findProjectRoot as
|
|
2629
|
+
findProjectRoot as findProjectRoot9,
|
|
2024
2630
|
getUsage,
|
|
2025
2631
|
isAutoPromoteEligible,
|
|
2026
2632
|
isDecaying,
|
|
2027
2633
|
loadCodeMap as loadCodeMap2,
|
|
2028
2634
|
loadConfig,
|
|
2029
2635
|
loadMemoriesFromDir as loadMemoriesFromDir2,
|
|
2030
|
-
loadUsageIndex,
|
|
2636
|
+
loadUsageIndex as loadUsageIndex2,
|
|
2031
2637
|
pullCrossRepoSources,
|
|
2032
|
-
resolveHaivePaths as
|
|
2638
|
+
resolveHaivePaths as resolveHaivePaths6,
|
|
2033
2639
|
resolveManifestFiles,
|
|
2034
2640
|
serializeMemory as serializeMemory2,
|
|
2035
2641
|
trackDependencies,
|
|
@@ -2048,9 +2654,9 @@ function registerSync(program2) {
|
|
|
2048
2654
|
"--inject-bridge",
|
|
2049
2655
|
"inject top validated memories into CLAUDE.md (or --bridge-file) between <!-- haive:memories-start/end --> markers"
|
|
2050
2656
|
).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) => {
|
|
2051
|
-
const root =
|
|
2052
|
-
const paths =
|
|
2053
|
-
if (!
|
|
2657
|
+
const root = findProjectRoot9(opts.dir);
|
|
2658
|
+
const paths = resolveHaivePaths6(root);
|
|
2659
|
+
if (!existsSync11(paths.memoriesDir)) {
|
|
2054
2660
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2055
2661
|
process.exitCode = 1;
|
|
2056
2662
|
return;
|
|
@@ -2070,7 +2676,7 @@ function registerSync(program2) {
|
|
|
2070
2676
|
for (const { memory: memory2, filePath } of memories) {
|
|
2071
2677
|
if (memory2.frontmatter.type === "session_recap") {
|
|
2072
2678
|
if (memory2.frontmatter.status === "stale") {
|
|
2073
|
-
await
|
|
2679
|
+
await writeFile6(
|
|
2074
2680
|
filePath,
|
|
2075
2681
|
serializeMemory2({
|
|
2076
2682
|
frontmatter: {
|
|
@@ -2093,7 +2699,7 @@ function registerSync(program2) {
|
|
|
2093
2699
|
const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2094
2700
|
if (result.stale) {
|
|
2095
2701
|
if (memory2.frontmatter.status !== "stale") {
|
|
2096
|
-
await
|
|
2702
|
+
await writeFile6(
|
|
2097
2703
|
filePath,
|
|
2098
2704
|
serializeMemory2({
|
|
2099
2705
|
frontmatter: {
|
|
@@ -2109,7 +2715,7 @@ function registerSync(program2) {
|
|
|
2109
2715
|
staleMarked++;
|
|
2110
2716
|
}
|
|
2111
2717
|
} else if (memory2.frontmatter.status === "stale") {
|
|
2112
|
-
await
|
|
2718
|
+
await writeFile6(
|
|
2113
2719
|
filePath,
|
|
2114
2720
|
serializeMemory2({
|
|
2115
2721
|
frontmatter: {
|
|
@@ -2128,7 +2734,7 @@ function registerSync(program2) {
|
|
|
2128
2734
|
}
|
|
2129
2735
|
if (opts.promote !== false) {
|
|
2130
2736
|
const memories = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
2131
|
-
const usage = await
|
|
2737
|
+
const usage = await loadUsageIndex2(paths);
|
|
2132
2738
|
const nowMs = Date.now();
|
|
2133
2739
|
for (const { memory: memory2, filePath } of memories) {
|
|
2134
2740
|
const fm = memory2.frontmatter;
|
|
@@ -2137,7 +2743,7 @@ function registerSync(program2) {
|
|
|
2137
2743
|
minReads: autoPromoteMinReads,
|
|
2138
2744
|
maxRejections: DEFAULT_AUTO_PROMOTE_RULE.maxRejections
|
|
2139
2745
|
})) {
|
|
2140
|
-
await
|
|
2746
|
+
await writeFile6(
|
|
2141
2747
|
filePath,
|
|
2142
2748
|
serializeMemory2({ frontmatter: { ...fm, status: "validated" }, body: memory2.body }),
|
|
2143
2749
|
"utf8"
|
|
@@ -2148,7 +2754,7 @@ function registerSync(program2) {
|
|
|
2148
2754
|
if (autoApproveDelayHours !== null && fm.status === "proposed" && fm.scope === "team") {
|
|
2149
2755
|
const ageHours = (nowMs - new Date(fm.created_at).getTime()) / (1e3 * 60 * 60);
|
|
2150
2756
|
if (ageHours >= autoApproveDelayHours) {
|
|
2151
|
-
await
|
|
2757
|
+
await writeFile6(
|
|
2152
2758
|
filePath,
|
|
2153
2759
|
serializeMemory2({
|
|
2154
2760
|
frontmatter: {
|
|
@@ -2182,7 +2788,7 @@ function registerSync(program2) {
|
|
|
2182
2788
|
);
|
|
2183
2789
|
}
|
|
2184
2790
|
if (opts.injectBridge) {
|
|
2185
|
-
const bridgeFile = opts.bridgeFile ?
|
|
2791
|
+
const bridgeFile = opts.bridgeFile ? path12.resolve(opts.bridgeFile) : path12.join(root, "CLAUDE.md");
|
|
2186
2792
|
const maxInject = Math.max(1, Number(opts.bridgeMaxMemories ?? 5));
|
|
2187
2793
|
await injectBridge(bridgeFile, paths.memoriesDir, maxInject, root, opts.quiet);
|
|
2188
2794
|
}
|
|
@@ -2202,7 +2808,7 @@ function registerSync(program2) {
|
|
|
2202
2808
|
}
|
|
2203
2809
|
if (!opts.quiet) {
|
|
2204
2810
|
const allForDecay = await loadMemoriesFromDir2(paths.memoriesDir);
|
|
2205
|
-
const usageForDecay = await
|
|
2811
|
+
const usageForDecay = await loadUsageIndex2(paths);
|
|
2206
2812
|
const decaying = allForDecay.filter(({ memory: memory2 }) => {
|
|
2207
2813
|
const fm = memory2.frontmatter;
|
|
2208
2814
|
if (fm.status === "rejected" || fm.status === "deprecated" || fm.status === "stale") return false;
|
|
@@ -2288,10 +2894,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
2288
2894
|
paths: [result.file],
|
|
2289
2895
|
topic: `dep-bump-${slugParts}`
|
|
2290
2896
|
});
|
|
2291
|
-
const teamDir =
|
|
2292
|
-
await
|
|
2293
|
-
await
|
|
2294
|
-
|
|
2897
|
+
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
2898
|
+
await mkdir7(teamDir, { recursive: true });
|
|
2899
|
+
await writeFile6(
|
|
2900
|
+
path12.join(teamDir, `${fm.id}.md`),
|
|
2295
2901
|
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
2296
2902
|
"utf8"
|
|
2297
2903
|
);
|
|
@@ -2355,10 +2961,10 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
2355
2961
|
paths: [diff.file],
|
|
2356
2962
|
topic: `contract-breaking-${diff.contract}`
|
|
2357
2963
|
});
|
|
2358
|
-
const teamDir =
|
|
2359
|
-
await
|
|
2360
|
-
await
|
|
2361
|
-
|
|
2964
|
+
const teamDir = path12.join(paths.memoriesDir, "team");
|
|
2965
|
+
await mkdir7(teamDir, { recursive: true });
|
|
2966
|
+
await writeFile6(
|
|
2967
|
+
path12.join(teamDir, `${fm.id}.md`),
|
|
2362
2968
|
serializeMemory2({ frontmatter: { ...fm, requires_human_approval: true }, body }),
|
|
2363
2969
|
"utf8"
|
|
2364
2970
|
);
|
|
@@ -2419,7 +3025,7 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
2419
3025
|
});
|
|
2420
3026
|
}
|
|
2421
3027
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
2422
|
-
if (!
|
|
3028
|
+
if (!existsSync11(memoriesDir)) return;
|
|
2423
3029
|
const all = await loadMemoriesFromDir2(memoriesDir);
|
|
2424
3030
|
const top = all.filter(({ memory: memory2 }) => {
|
|
2425
3031
|
const s = memory2.frontmatter.status;
|
|
@@ -2444,17 +3050,17 @@ ${m.memory.body.trim()}`;
|
|
|
2444
3050
|
` + block + `
|
|
2445
3051
|
|
|
2446
3052
|
${BRIDGE_END}`;
|
|
2447
|
-
const fileExists =
|
|
2448
|
-
let existing = fileExists ? await
|
|
3053
|
+
const fileExists = existsSync11(bridgeFile);
|
|
3054
|
+
let existing = fileExists ? await readFile7(bridgeFile, "utf8") : "";
|
|
2449
3055
|
existing = existing.replace(/\r\n/g, "\n");
|
|
2450
3056
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
2451
3057
|
const endIdx = existing.indexOf(BRIDGE_END);
|
|
2452
3058
|
if (startIdx !== -1 && endIdx === -1) {
|
|
2453
|
-
ui.warn(`${
|
|
3059
|
+
ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_START} without ${BRIDGE_END}. Fix the file manually before running --inject-bridge.`);
|
|
2454
3060
|
return;
|
|
2455
3061
|
}
|
|
2456
3062
|
if (startIdx === -1 && endIdx !== -1) {
|
|
2457
|
-
ui.warn(`${
|
|
3063
|
+
ui.warn(`${path12.relative(root, bridgeFile)}: found ${BRIDGE_END} without ${BRIDGE_START}. Fix the file manually before running --inject-bridge.`);
|
|
2458
3064
|
return;
|
|
2459
3065
|
}
|
|
2460
3066
|
let updated;
|
|
@@ -2462,14 +3068,14 @@ ${BRIDGE_END}`;
|
|
|
2462
3068
|
updated = existing.slice(0, startIdx) + injected + existing.slice(endIdx + BRIDGE_END.length);
|
|
2463
3069
|
} else {
|
|
2464
3070
|
if (!fileExists && !quiet) {
|
|
2465
|
-
ui.info(`Creating ${
|
|
3071
|
+
ui.info(`Creating ${path12.relative(root, bridgeFile)} with haive memory block.`);
|
|
2466
3072
|
}
|
|
2467
3073
|
updated = existing + (existing.endsWith("\n") ? "" : "\n") + "\n" + injected + "\n";
|
|
2468
3074
|
}
|
|
2469
|
-
await
|
|
3075
|
+
await writeFile6(bridgeFile, updated, "utf8");
|
|
2470
3076
|
if (!quiet) {
|
|
2471
3077
|
console.log(
|
|
2472
|
-
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${
|
|
3078
|
+
ui.dim(`bridge: injected ${top.length} memor${top.length === 1 ? "y" : "ies"} into ${path12.relative(root, bridgeFile)}`)
|
|
2473
3079
|
);
|
|
2474
3080
|
}
|
|
2475
3081
|
}
|
|
@@ -2494,17 +3100,17 @@ function collectSinceChanges(root, ref) {
|
|
|
2494
3100
|
|
|
2495
3101
|
// src/commands/memory-add.ts
|
|
2496
3102
|
import { createHash } from "crypto";
|
|
2497
|
-
import { mkdir as
|
|
2498
|
-
import { existsSync as
|
|
2499
|
-
import
|
|
3103
|
+
import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
|
|
3104
|
+
import { existsSync as existsSync12 } from "fs";
|
|
3105
|
+
import path13 from "path";
|
|
2500
3106
|
import "commander";
|
|
2501
3107
|
import {
|
|
2502
3108
|
buildFrontmatter as buildFrontmatter3,
|
|
2503
|
-
findProjectRoot as
|
|
3109
|
+
findProjectRoot as findProjectRoot10,
|
|
2504
3110
|
inferModulesFromPaths,
|
|
2505
3111
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
2506
3112
|
memoryFilePath as memoryFilePath2,
|
|
2507
|
-
resolveHaivePaths as
|
|
3113
|
+
resolveHaivePaths as resolveHaivePaths7,
|
|
2508
3114
|
serializeMemory as serializeMemory3
|
|
2509
3115
|
} from "@hiveai/core";
|
|
2510
3116
|
function registerMemoryAdd(memory2) {
|
|
@@ -2532,9 +3138,9 @@ function registerMemoryAdd(memory2) {
|
|
|
2532
3138
|
--scope team --body "Never modify existing migrations. Create V{n+1}__desc.sql."
|
|
2533
3139
|
`
|
|
2534
3140
|
).requiredOption("--type <type>", "convention | decision | gotcha | architecture | glossary | attempt").requiredOption("--slug <slug>", "short kebab-case identifier used in the file name").option("--title <text>", "memory title \u2014 becomes the first heading of the body").option("--scope <scope>", "personal | team | module (default: personal, or team in autopilot)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags for easier retrieval").option("--domain <domain>", "domain (e.g. transactions)").option("--author <author>", "author email or handle").option("--paths <csv>", "anchor to source files \u2014 used for staleness detection by haive sync").option("--symbols <csv>", "anchor to specific symbols (class/function names)").option("--commit <sha>", "anchor to a specific commit SHA").option("--body <text>", "memory body content (Markdown) \u2014 overrides --title default body").option("--body-file <path>", "read memory body from a Markdown file \u2014 for long content").option("--no-auto-tag", "disable automatic tag suggestions inferred from anchor paths").option("--topic <key>", "stable key for upsert: if a memory with this topic+scope already exists, update it in-place (revision_count++)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2535
|
-
const root =
|
|
2536
|
-
const paths =
|
|
2537
|
-
if (!
|
|
3141
|
+
const root = findProjectRoot10(opts.dir);
|
|
3142
|
+
const paths = resolveHaivePaths7(root);
|
|
3143
|
+
if (!existsSync12(paths.haiveDir)) {
|
|
2538
3144
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
2539
3145
|
process.exitCode = 1;
|
|
2540
3146
|
return;
|
|
@@ -2545,7 +3151,7 @@ function registerMemoryAdd(memory2) {
|
|
|
2545
3151
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths(anchorPaths) : [];
|
|
2546
3152
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
2547
3153
|
if (anchorPaths.length > 0) {
|
|
2548
|
-
const missing = anchorPaths.filter((p) => !
|
|
3154
|
+
const missing = anchorPaths.filter((p) => !existsSync12(path13.resolve(root, p)));
|
|
2549
3155
|
if (missing.length > 0) {
|
|
2550
3156
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
2551
3157
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -2557,12 +3163,12 @@ function registerMemoryAdd(memory2) {
|
|
|
2557
3163
|
const title = opts.title ?? opts.slug;
|
|
2558
3164
|
let body;
|
|
2559
3165
|
if (opts.bodyFile !== void 0) {
|
|
2560
|
-
if (!
|
|
3166
|
+
if (!existsSync12(opts.bodyFile)) {
|
|
2561
3167
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
2562
3168
|
process.exitCode = 1;
|
|
2563
3169
|
return;
|
|
2564
3170
|
}
|
|
2565
|
-
const fileContent = await
|
|
3171
|
+
const fileContent = await readFile8(opts.bodyFile, "utf8");
|
|
2566
3172
|
body = opts.title ? `# ${opts.title}
|
|
2567
3173
|
|
|
2568
3174
|
${fileContent.trim()}
|
|
@@ -2578,7 +3184,7 @@ TODO \u2014 write the memory body.
|
|
|
2578
3184
|
`;
|
|
2579
3185
|
}
|
|
2580
3186
|
const scope = opts.scope ?? "personal";
|
|
2581
|
-
if (
|
|
3187
|
+
if (existsSync12(paths.memoriesDir)) {
|
|
2582
3188
|
const incomingHash = createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
2583
3189
|
const allForHash = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
2584
3190
|
const hashDup = allForHash.find(
|
|
@@ -2591,7 +3197,7 @@ TODO \u2014 write the memory body.
|
|
|
2591
3197
|
return;
|
|
2592
3198
|
}
|
|
2593
3199
|
}
|
|
2594
|
-
if (opts.topic &&
|
|
3200
|
+
if (opts.topic && existsSync12(paths.memoriesDir)) {
|
|
2595
3201
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
2596
3202
|
const topicMatch = existing.find(
|
|
2597
3203
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
@@ -2609,8 +3215,8 @@ TODO \u2014 write the memory body.
|
|
|
2609
3215
|
symbols: parseCsv2(opts.symbols).length ? parseCsv2(opts.symbols) : fm.anchor.symbols
|
|
2610
3216
|
}
|
|
2611
3217
|
};
|
|
2612
|
-
await
|
|
2613
|
-
ui.success(`Updated (topic upsert) ${
|
|
3218
|
+
await writeFile7(topicMatch.filePath, serializeMemory3({ frontmatter: newFrontmatter, body }), "utf8");
|
|
3219
|
+
ui.success(`Updated (topic upsert) ${path13.relative(root, topicMatch.filePath)}`);
|
|
2614
3220
|
ui.info(`id=${fm.id} revision=${revisionCount}`);
|
|
2615
3221
|
return;
|
|
2616
3222
|
}
|
|
@@ -2629,13 +3235,13 @@ TODO \u2014 write the memory body.
|
|
|
2629
3235
|
topic: opts.topic
|
|
2630
3236
|
});
|
|
2631
3237
|
const file = memoryFilePath2(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
2632
|
-
await
|
|
2633
|
-
if (
|
|
3238
|
+
await mkdir8(path13.dirname(file), { recursive: true });
|
|
3239
|
+
if (existsSync12(file)) {
|
|
2634
3240
|
ui.error(`Memory already exists at ${file}`);
|
|
2635
3241
|
process.exitCode = 1;
|
|
2636
3242
|
return;
|
|
2637
3243
|
}
|
|
2638
|
-
if (
|
|
3244
|
+
if (existsSync12(paths.memoriesDir)) {
|
|
2639
3245
|
const existing = await loadMemoriesFromDir3(paths.memoriesDir);
|
|
2640
3246
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
2641
3247
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
@@ -2647,8 +3253,8 @@ TODO \u2014 write the memory body.
|
|
|
2647
3253
|
ui.warn("Consider updating one of these with `haive memory update` instead.");
|
|
2648
3254
|
}
|
|
2649
3255
|
}
|
|
2650
|
-
await
|
|
2651
|
-
ui.success(`Created ${
|
|
3256
|
+
await writeFile7(file, serializeMemory3({ frontmatter, body }), "utf8");
|
|
3257
|
+
ui.success(`Created ${path13.relative(root, file)}`);
|
|
2652
3258
|
ui.info(`id=${frontmatter.id} scope=${frontmatter.scope} status=${frontmatter.status}`);
|
|
2653
3259
|
if (inferredTags.length > 0) {
|
|
2654
3260
|
ui.info(`auto-tagged: ${inferredTags.join(", ")} (use --no-auto-tag to disable)`);
|
|
@@ -2678,10 +3284,10 @@ function parseCsv2(value) {
|
|
|
2678
3284
|
}
|
|
2679
3285
|
|
|
2680
3286
|
// src/commands/memory-list.ts
|
|
2681
|
-
import { existsSync as
|
|
2682
|
-
import
|
|
3287
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3288
|
+
import path14 from "path";
|
|
2683
3289
|
import "commander";
|
|
2684
|
-
import { findProjectRoot as
|
|
3290
|
+
import { findProjectRoot as findProjectRoot11, resolveHaivePaths as resolveHaivePaths8 } from "@hiveai/core";
|
|
2685
3291
|
|
|
2686
3292
|
// src/utils/fs.ts
|
|
2687
3293
|
import {
|
|
@@ -2693,9 +3299,9 @@ import {
|
|
|
2693
3299
|
// src/commands/memory-list.ts
|
|
2694
3300
|
function registerMemoryList(memory2) {
|
|
2695
3301
|
memory2.command("list").description("List memories with optional filters").option("--scope <scope>", "personal | team | module").option("--type <type>", "filter by type").option("--tag <tag>", "filter by tag").option("--module <name>", "filter by module name").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected,deprecated)").option("--show-rejected", "include rejected memories (hidden by default)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2696
|
-
const root =
|
|
2697
|
-
const paths =
|
|
2698
|
-
if (!
|
|
3302
|
+
const root = findProjectRoot11(opts.dir);
|
|
3303
|
+
const paths = resolveHaivePaths8(root);
|
|
3304
|
+
if (!existsSync13(paths.memoriesDir)) {
|
|
2699
3305
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
2700
3306
|
process.exitCode = 1;
|
|
2701
3307
|
return;
|
|
@@ -2727,7 +3333,7 @@ function registerMemoryList(memory2) {
|
|
|
2727
3333
|
console.log(
|
|
2728
3334
|
`${ui.bold(fm.id)} ${ui.dim(fm.scope)}/${ui.dim(fm.type)} ${statusBadge}${moduleStr}${tagStr}`
|
|
2729
3335
|
);
|
|
2730
|
-
console.log(` ${ui.dim(
|
|
3336
|
+
console.log(` ${ui.dim(path14.relative(root, filePath))}`);
|
|
2731
3337
|
}
|
|
2732
3338
|
console.log(ui.dim(`
|
|
2733
3339
|
${filtered.length} memor${filtered.length === 1 ? "y" : "ies"}`));
|
|
@@ -2762,21 +3368,21 @@ function matchesFilters(loaded, opts) {
|
|
|
2762
3368
|
}
|
|
2763
3369
|
|
|
2764
3370
|
// src/commands/memory-promote.ts
|
|
2765
|
-
import { mkdir as
|
|
2766
|
-
import { existsSync as
|
|
2767
|
-
import
|
|
3371
|
+
import { mkdir as mkdir9, unlink, writeFile as writeFile8 } from "fs/promises";
|
|
3372
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3373
|
+
import path15 from "path";
|
|
2768
3374
|
import "commander";
|
|
2769
3375
|
import {
|
|
2770
|
-
findProjectRoot as
|
|
3376
|
+
findProjectRoot as findProjectRoot12,
|
|
2771
3377
|
memoryFilePath as memoryFilePath3,
|
|
2772
|
-
resolveHaivePaths as
|
|
3378
|
+
resolveHaivePaths as resolveHaivePaths9,
|
|
2773
3379
|
serializeMemory as serializeMemory4
|
|
2774
3380
|
} from "@hiveai/core";
|
|
2775
3381
|
function registerMemoryPromote(memory2) {
|
|
2776
3382
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2777
|
-
const root =
|
|
2778
|
-
const paths =
|
|
2779
|
-
if (!
|
|
3383
|
+
const root = findProjectRoot12(opts.dir);
|
|
3384
|
+
const paths = resolveHaivePaths9(root);
|
|
3385
|
+
if (!existsSync14(paths.memoriesDir)) {
|
|
2780
3386
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
2781
3387
|
process.exitCode = 1;
|
|
2782
3388
|
return;
|
|
@@ -2811,30 +3417,30 @@ function registerMemoryPromote(memory2) {
|
|
|
2811
3417
|
body: found.memory.body
|
|
2812
3418
|
};
|
|
2813
3419
|
const newPath = memoryFilePath3(paths, "team", updated.frontmatter.id);
|
|
2814
|
-
await
|
|
2815
|
-
await
|
|
3420
|
+
await mkdir9(path15.dirname(newPath), { recursive: true });
|
|
3421
|
+
await writeFile8(newPath, serializeMemory4(updated), "utf8");
|
|
2816
3422
|
await unlink(found.filePath);
|
|
2817
3423
|
ui.success(`Promoted ${id} to team scope (status=proposed)`);
|
|
2818
|
-
ui.info(`Now at ${
|
|
3424
|
+
ui.info(`Now at ${path15.relative(root, newPath)}`);
|
|
2819
3425
|
console.log(ui.dim(`\u2192 next: haive memory approve ${id} (validate for team use)`));
|
|
2820
3426
|
});
|
|
2821
3427
|
}
|
|
2822
3428
|
|
|
2823
3429
|
// src/commands/memory-approve.ts
|
|
2824
|
-
import { existsSync as
|
|
2825
|
-
import { writeFile as
|
|
2826
|
-
import
|
|
3430
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3431
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
3432
|
+
import path16 from "path";
|
|
2827
3433
|
import "commander";
|
|
2828
3434
|
import {
|
|
2829
|
-
findProjectRoot as
|
|
2830
|
-
resolveHaivePaths as
|
|
3435
|
+
findProjectRoot as findProjectRoot13,
|
|
3436
|
+
resolveHaivePaths as resolveHaivePaths10,
|
|
2831
3437
|
serializeMemory as serializeMemory5
|
|
2832
3438
|
} from "@hiveai/core";
|
|
2833
3439
|
function registerMemoryApprove(memory2) {
|
|
2834
3440
|
memory2.command("approve [id]").description("Mark a memory as 'validated'. Use --all to bulk-approve all proposed/draft memories.").option("--all", "approve all proposed and draft memories at once").option("--pending", "approve all memories with status 'proposed'").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2835
|
-
const root =
|
|
2836
|
-
const paths =
|
|
2837
|
-
if (!
|
|
3441
|
+
const root = findProjectRoot13(opts.dir);
|
|
3442
|
+
const paths = resolveHaivePaths10(root);
|
|
3443
|
+
if (!existsSync15(paths.memoriesDir)) {
|
|
2838
3444
|
ui.error(`No .ai/memories at ${root}.`);
|
|
2839
3445
|
process.exitCode = 1;
|
|
2840
3446
|
return;
|
|
@@ -2856,7 +3462,7 @@ function registerMemoryApprove(memory2) {
|
|
|
2856
3462
|
frontmatter: { ...found2.memory.frontmatter, status: "validated" },
|
|
2857
3463
|
body: found2.memory.body
|
|
2858
3464
|
};
|
|
2859
|
-
await
|
|
3465
|
+
await writeFile9(found2.filePath, serializeMemory5(next2), "utf8");
|
|
2860
3466
|
count++;
|
|
2861
3467
|
}
|
|
2862
3468
|
ui.success(`Approved ${count} memor${count === 1 ? "y" : "ies"} (status=validated)`);
|
|
@@ -2885,27 +3491,27 @@ function registerMemoryApprove(memory2) {
|
|
|
2885
3491
|
frontmatter: { ...found.memory.frontmatter, status: "validated" },
|
|
2886
3492
|
body: found.memory.body
|
|
2887
3493
|
};
|
|
2888
|
-
await
|
|
3494
|
+
await writeFile9(found.filePath, serializeMemory5(next), "utf8");
|
|
2889
3495
|
ui.success(`Approved ${id} (status=validated)`);
|
|
2890
|
-
ui.info(
|
|
3496
|
+
ui.info(path16.relative(root, found.filePath));
|
|
2891
3497
|
});
|
|
2892
3498
|
}
|
|
2893
3499
|
|
|
2894
3500
|
// src/commands/memory-update.ts
|
|
2895
|
-
import { writeFile as
|
|
2896
|
-
import { existsSync as
|
|
2897
|
-
import
|
|
3501
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
3502
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3503
|
+
import path17 from "path";
|
|
2898
3504
|
import "commander";
|
|
2899
3505
|
import {
|
|
2900
|
-
findProjectRoot as
|
|
2901
|
-
resolveHaivePaths as
|
|
3506
|
+
findProjectRoot as findProjectRoot14,
|
|
3507
|
+
resolveHaivePaths as resolveHaivePaths11,
|
|
2902
3508
|
serializeMemory as serializeMemory6
|
|
2903
3509
|
} from "@hiveai/core";
|
|
2904
3510
|
function registerMemoryUpdate(memory2) {
|
|
2905
3511
|
memory2.command("update <id>").description("Update body, tags, or anchor of an existing memory (preserves id and usage history)").option("--title <text>", "new title \u2014 replaces the first heading of the body").option("--body <text>", "new Markdown body \u2014 replaces the existing body").option("--tags <csv>", "new tags, comma-separated \u2014 fully replaces existing tags").option("--paths <csv>", "new anchor paths, comma-separated").option("--symbols <csv>", "new anchor symbols, comma-separated").option("--commit <sha>", "new anchor commit SHA").option("--domain <domain>", "new domain label").option("--author <author>", "new author handle or email").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
2906
|
-
const root =
|
|
2907
|
-
const paths =
|
|
2908
|
-
if (!
|
|
3512
|
+
const root = findProjectRoot14(opts.dir);
|
|
3513
|
+
const paths = resolveHaivePaths11(root);
|
|
3514
|
+
if (!existsSync16(paths.memoriesDir)) {
|
|
2909
3515
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
2910
3516
|
process.exitCode = 1;
|
|
2911
3517
|
return;
|
|
@@ -2952,12 +3558,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
2952
3558
|
ui.warn("Nothing to update \u2014 provide at least one option.");
|
|
2953
3559
|
return;
|
|
2954
3560
|
}
|
|
2955
|
-
await
|
|
3561
|
+
await writeFile10(
|
|
2956
3562
|
loaded.filePath,
|
|
2957
3563
|
serializeMemory6({ frontmatter: newFrontmatter, body: newBody }),
|
|
2958
3564
|
"utf8"
|
|
2959
3565
|
);
|
|
2960
|
-
ui.success(`Updated ${
|
|
3566
|
+
ui.success(`Updated ${path17.relative(root, loaded.filePath)}`);
|
|
2961
3567
|
ui.info(`fields: ${updated.join(", ")}`);
|
|
2962
3568
|
});
|
|
2963
3569
|
}
|
|
@@ -2976,17 +3582,17 @@ function parseCsv3(value) {
|
|
|
2976
3582
|
}
|
|
2977
3583
|
|
|
2978
3584
|
// src/commands/memory-auto-promote.ts
|
|
2979
|
-
import { writeFile as
|
|
2980
|
-
import { existsSync as
|
|
2981
|
-
import
|
|
3585
|
+
import { writeFile as writeFile11 } from "fs/promises";
|
|
3586
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3587
|
+
import path18 from "path";
|
|
2982
3588
|
import "commander";
|
|
2983
3589
|
import {
|
|
2984
3590
|
DEFAULT_AUTO_PROMOTE_RULE as DEFAULT_AUTO_PROMOTE_RULE2,
|
|
2985
|
-
findProjectRoot as
|
|
3591
|
+
findProjectRoot as findProjectRoot15,
|
|
2986
3592
|
getUsage as getUsage2,
|
|
2987
3593
|
isAutoPromoteEligible as isAutoPromoteEligible2,
|
|
2988
|
-
loadUsageIndex as
|
|
2989
|
-
resolveHaivePaths as
|
|
3594
|
+
loadUsageIndex as loadUsageIndex3,
|
|
3595
|
+
resolveHaivePaths as resolveHaivePaths12,
|
|
2990
3596
|
serializeMemory as serializeMemory7
|
|
2991
3597
|
} from "@hiveai/core";
|
|
2992
3598
|
function registerMemoryAutoPromote(memory2) {
|
|
@@ -2995,9 +3601,9 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
2995
3601
|
"memories with more rejections than this are skipped",
|
|
2996
3602
|
String(DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
|
|
2997
3603
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
2998
|
-
const root =
|
|
2999
|
-
const paths =
|
|
3000
|
-
if (!
|
|
3604
|
+
const root = findProjectRoot15(opts.dir);
|
|
3605
|
+
const paths = resolveHaivePaths12(root);
|
|
3606
|
+
if (!existsSync17(paths.memoriesDir)) {
|
|
3001
3607
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3002
3608
|
process.exitCode = 1;
|
|
3003
3609
|
return;
|
|
@@ -3007,7 +3613,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
3007
3613
|
maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE2.maxRejections)
|
|
3008
3614
|
};
|
|
3009
3615
|
const memories = await loadMemoriesFromDir4(paths.memoriesDir);
|
|
3010
|
-
const usage = await
|
|
3616
|
+
const usage = await loadUsageIndex3(paths);
|
|
3011
3617
|
const eligible = memories.filter(
|
|
3012
3618
|
({ memory: memory3 }) => isAutoPromoteEligible2(memory3.frontmatter, getUsage2(usage, memory3.frontmatter.id), rule)
|
|
3013
3619
|
);
|
|
@@ -3023,13 +3629,13 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
3023
3629
|
console.log(
|
|
3024
3630
|
`${ui.bold(opts.apply ? "PROMOTE" : "would promote")} ${mem.frontmatter.id} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
3025
3631
|
);
|
|
3026
|
-
console.log(` ${ui.dim(
|
|
3632
|
+
console.log(` ${ui.dim(path18.relative(root, filePath))}`);
|
|
3027
3633
|
if (opts.apply) {
|
|
3028
3634
|
const next = {
|
|
3029
3635
|
frontmatter: { ...mem.frontmatter, status: "validated" },
|
|
3030
3636
|
body: mem.body
|
|
3031
3637
|
};
|
|
3032
|
-
await
|
|
3638
|
+
await writeFile11(filePath, serializeMemory7(next), "utf8");
|
|
3033
3639
|
written++;
|
|
3034
3640
|
}
|
|
3035
3641
|
}
|
|
@@ -3040,20 +3646,20 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
3040
3646
|
|
|
3041
3647
|
// src/commands/memory-edit.ts
|
|
3042
3648
|
import { spawn as spawn2 } from "child_process";
|
|
3043
|
-
import { existsSync as
|
|
3044
|
-
import { readFile as
|
|
3045
|
-
import
|
|
3649
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3650
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3651
|
+
import path19 from "path";
|
|
3046
3652
|
import "commander";
|
|
3047
3653
|
import {
|
|
3048
|
-
findProjectRoot as
|
|
3654
|
+
findProjectRoot as findProjectRoot16,
|
|
3049
3655
|
parseMemory,
|
|
3050
|
-
resolveHaivePaths as
|
|
3656
|
+
resolveHaivePaths as resolveHaivePaths13
|
|
3051
3657
|
} from "@hiveai/core";
|
|
3052
3658
|
function registerMemoryEdit(memory2) {
|
|
3053
3659
|
memory2.command("edit <id>").description("Open a memory in $EDITOR and re-validate when you save").option("-e, --editor <cmd>", "editor command (defaults to $EDITOR or 'vi')").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
3054
|
-
const root =
|
|
3055
|
-
const paths =
|
|
3056
|
-
if (!
|
|
3660
|
+
const root = findProjectRoot16(opts.dir);
|
|
3661
|
+
const paths = resolveHaivePaths13(root);
|
|
3662
|
+
if (!existsSync18(paths.memoriesDir)) {
|
|
3057
3663
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3058
3664
|
process.exitCode = 1;
|
|
3059
3665
|
return;
|
|
@@ -3066,13 +3672,13 @@ function registerMemoryEdit(memory2) {
|
|
|
3066
3672
|
return;
|
|
3067
3673
|
}
|
|
3068
3674
|
const editor = opts.editor ?? process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
3069
|
-
ui.info(`Opening ${
|
|
3675
|
+
ui.info(`Opening ${path19.relative(root, found.filePath)} with ${editor}\u2026`);
|
|
3070
3676
|
const code = await runEditor(editor, found.filePath);
|
|
3071
3677
|
if (code !== 0) {
|
|
3072
3678
|
ui.warn(`Editor exited with status ${code}.`);
|
|
3073
3679
|
}
|
|
3074
3680
|
try {
|
|
3075
|
-
const fresh = await
|
|
3681
|
+
const fresh = await readFile9(found.filePath, "utf8");
|
|
3076
3682
|
parseMemory(fresh);
|
|
3077
3683
|
ui.success("Memory still parses cleanly.");
|
|
3078
3684
|
} catch (err) {
|
|
@@ -3093,29 +3699,29 @@ function runEditor(editor, file) {
|
|
|
3093
3699
|
}
|
|
3094
3700
|
|
|
3095
3701
|
// src/commands/memory-for-files.ts
|
|
3096
|
-
import { existsSync as
|
|
3097
|
-
import
|
|
3702
|
+
import { existsSync as existsSync19 } from "fs";
|
|
3703
|
+
import path20 from "path";
|
|
3098
3704
|
import "commander";
|
|
3099
3705
|
import {
|
|
3100
3706
|
deriveConfidence,
|
|
3101
|
-
findProjectRoot as
|
|
3707
|
+
findProjectRoot as findProjectRoot17,
|
|
3102
3708
|
getUsage as getUsage3,
|
|
3103
3709
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
3104
|
-
loadUsageIndex as
|
|
3710
|
+
loadUsageIndex as loadUsageIndex4,
|
|
3105
3711
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
|
|
3106
|
-
resolveHaivePaths as
|
|
3712
|
+
resolveHaivePaths as resolveHaivePaths14
|
|
3107
3713
|
} from "@hiveai/core";
|
|
3108
3714
|
function registerMemoryForFiles(memory2) {
|
|
3109
3715
|
memory2.command("for-files <files...>").description("Show memories relevant to the given files (anchor overlap, module, domain)").option("-d, --dir <dir>", "project root").action(async (files, opts) => {
|
|
3110
|
-
const root =
|
|
3111
|
-
const paths =
|
|
3112
|
-
if (!
|
|
3716
|
+
const root = findProjectRoot17(opts.dir);
|
|
3717
|
+
const paths = resolveHaivePaths14(root);
|
|
3718
|
+
if (!existsSync19(paths.memoriesDir)) {
|
|
3113
3719
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3114
3720
|
process.exitCode = 1;
|
|
3115
3721
|
return;
|
|
3116
3722
|
}
|
|
3117
3723
|
const all = await loadMemoriesFromDir4(paths.memoriesDir);
|
|
3118
|
-
const usage = await
|
|
3724
|
+
const usage = await loadUsageIndex4(paths);
|
|
3119
3725
|
const inferred = inferModulesFromPaths2(files);
|
|
3120
3726
|
const byAnchor = [];
|
|
3121
3727
|
const byModule = [];
|
|
@@ -3216,32 +3822,32 @@ function printGroup(root, label, loaded, usage) {
|
|
|
3216
3822
|
const u = getUsage3(usage, fm.id);
|
|
3217
3823
|
const conf = deriveConfidence(fm, u);
|
|
3218
3824
|
console.log(`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(conf)}`);
|
|
3219
|
-
console.log(` ${ui.dim(
|
|
3825
|
+
console.log(` ${ui.dim(path20.relative(root, filePath))}`);
|
|
3220
3826
|
}
|
|
3221
3827
|
}
|
|
3222
3828
|
|
|
3223
3829
|
// src/commands/memory-hot.ts
|
|
3224
|
-
import { existsSync as
|
|
3225
|
-
import
|
|
3830
|
+
import { existsSync as existsSync20 } from "fs";
|
|
3831
|
+
import path21 from "path";
|
|
3226
3832
|
import "commander";
|
|
3227
3833
|
import {
|
|
3228
|
-
findProjectRoot as
|
|
3834
|
+
findProjectRoot as findProjectRoot18,
|
|
3229
3835
|
getUsage as getUsage4,
|
|
3230
|
-
loadUsageIndex as
|
|
3231
|
-
resolveHaivePaths as
|
|
3836
|
+
loadUsageIndex as loadUsageIndex5,
|
|
3837
|
+
resolveHaivePaths as resolveHaivePaths15
|
|
3232
3838
|
} from "@hiveai/core";
|
|
3233
3839
|
function registerMemoryHot(memory2) {
|
|
3234
3840
|
memory2.command("hot").description("List memories actively used but not yet validated (good promotion candidates)").option("--threshold <n>", "minimum read_count to qualify", "3").option("--status <status>", "limit to one status (default: draft + proposed)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3235
|
-
const root =
|
|
3236
|
-
const paths =
|
|
3237
|
-
if (!
|
|
3841
|
+
const root = findProjectRoot18(opts.dir);
|
|
3842
|
+
const paths = resolveHaivePaths15(root);
|
|
3843
|
+
if (!existsSync20(paths.memoriesDir)) {
|
|
3238
3844
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3239
3845
|
process.exitCode = 1;
|
|
3240
3846
|
return;
|
|
3241
3847
|
}
|
|
3242
3848
|
const threshold = Math.max(1, Number(opts.threshold ?? 3));
|
|
3243
3849
|
const all = await loadMemoriesFromDir4(paths.memoriesDir);
|
|
3244
|
-
const usage = await
|
|
3850
|
+
const usage = await loadUsageIndex5(paths);
|
|
3245
3851
|
const candidates = all.filter(({ memory: mem }) => {
|
|
3246
3852
|
const fm = mem.frontmatter;
|
|
3247
3853
|
if (opts.status && fm.status !== opts.status) return false;
|
|
@@ -3262,7 +3868,7 @@ function registerMemoryHot(memory2) {
|
|
|
3262
3868
|
console.log(
|
|
3263
3869
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.bold(fm.status)} ${ui.dim(`reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
3264
3870
|
);
|
|
3265
|
-
console.log(` ${ui.dim(
|
|
3871
|
+
console.log(` ${ui.dim(path21.relative(root, filePath))}`);
|
|
3266
3872
|
}
|
|
3267
3873
|
ui.info(
|
|
3268
3874
|
`${candidates.length} hot \u2014 promote drafts with \`haive memory promote <id>\`, then \`haive memory auto-promote --apply\`.`
|
|
@@ -3271,15 +3877,15 @@ function registerMemoryHot(memory2) {
|
|
|
3271
3877
|
}
|
|
3272
3878
|
|
|
3273
3879
|
// src/commands/memory-tried.ts
|
|
3274
|
-
import { mkdir as
|
|
3275
|
-
import { existsSync as
|
|
3276
|
-
import
|
|
3880
|
+
import { mkdir as mkdir10, writeFile as writeFile12 } from "fs/promises";
|
|
3881
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3882
|
+
import path22 from "path";
|
|
3277
3883
|
import "commander";
|
|
3278
3884
|
import {
|
|
3279
3885
|
buildFrontmatter as buildFrontmatter4,
|
|
3280
|
-
findProjectRoot as
|
|
3886
|
+
findProjectRoot as findProjectRoot19,
|
|
3281
3887
|
memoryFilePath as memoryFilePath4,
|
|
3282
|
-
resolveHaivePaths as
|
|
3888
|
+
resolveHaivePaths as resolveHaivePaths16,
|
|
3283
3889
|
serializeMemory as serializeMemory8
|
|
3284
3890
|
} from "@hiveai/core";
|
|
3285
3891
|
function registerMemoryTried(memory2) {
|
|
@@ -3299,9 +3905,9 @@ function registerMemoryTried(memory2) {
|
|
|
3299
3905
|
--paths packages/cli/src/index.ts
|
|
3300
3906
|
`
|
|
3301
3907
|
).requiredOption("--what <text>", "what approach was tried (short, descriptive title)").requiredOption("--why-failed <text>", "why it failed or should NOT be used (include the exact error if possible)").option("--instead <text>", "the correct approach to use instead").option("--scope <scope>", "personal | team | module (default: personal)", "personal").option("--module <name>", "module name (required when scope=module)").option("--tags <csv>", "comma-separated tags").option("--paths <csv>", "anchor paths, comma-separated").option("--author <author>", "author email or handle").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3302
|
-
const root =
|
|
3303
|
-
const paths =
|
|
3304
|
-
if (!
|
|
3908
|
+
const root = findProjectRoot19(opts.dir);
|
|
3909
|
+
const paths = resolveHaivePaths16(root);
|
|
3910
|
+
if (!existsSync21(paths.haiveDir)) {
|
|
3305
3911
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
3306
3912
|
process.exitCode = 1;
|
|
3307
3913
|
return;
|
|
@@ -3324,14 +3930,14 @@ function registerMemoryTried(memory2) {
|
|
|
3324
3930
|
}
|
|
3325
3931
|
const body = lines.join("\n") + "\n";
|
|
3326
3932
|
const file = memoryFilePath4(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
3327
|
-
await
|
|
3328
|
-
if (
|
|
3933
|
+
await mkdir10(path22.dirname(file), { recursive: true });
|
|
3934
|
+
if (existsSync21(file)) {
|
|
3329
3935
|
ui.error(`Memory already exists at ${file}`);
|
|
3330
3936
|
process.exitCode = 1;
|
|
3331
3937
|
return;
|
|
3332
3938
|
}
|
|
3333
|
-
await
|
|
3334
|
-
ui.success(`Recorded: ${
|
|
3939
|
+
await writeFile12(file, serializeMemory8({ frontmatter, body }), "utf8");
|
|
3940
|
+
ui.success(`Recorded: ${path22.relative(root, file)}`);
|
|
3335
3941
|
ui.info(`id=${frontmatter.id} type=attempt status=validated (auto-approved)`);
|
|
3336
3942
|
});
|
|
3337
3943
|
}
|
|
@@ -3341,26 +3947,26 @@ function parseCsv4(value) {
|
|
|
3341
3947
|
}
|
|
3342
3948
|
|
|
3343
3949
|
// src/commands/memory-pending.ts
|
|
3344
|
-
import { existsSync as
|
|
3345
|
-
import
|
|
3950
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3951
|
+
import path23 from "path";
|
|
3346
3952
|
import "commander";
|
|
3347
3953
|
import {
|
|
3348
|
-
findProjectRoot as
|
|
3954
|
+
findProjectRoot as findProjectRoot20,
|
|
3349
3955
|
getUsage as getUsage5,
|
|
3350
|
-
loadUsageIndex as
|
|
3351
|
-
resolveHaivePaths as
|
|
3956
|
+
loadUsageIndex as loadUsageIndex6,
|
|
3957
|
+
resolveHaivePaths as resolveHaivePaths17
|
|
3352
3958
|
} from "@hiveai/core";
|
|
3353
3959
|
function registerMemoryPending(memory2) {
|
|
3354
3960
|
memory2.command("pending").description("List 'proposed' memories awaiting review (sorted by reads desc)").option("--scope <scope>", "filter by scope (personal | team | module)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3355
|
-
const root =
|
|
3356
|
-
const paths =
|
|
3357
|
-
if (!
|
|
3961
|
+
const root = findProjectRoot20(opts.dir);
|
|
3962
|
+
const paths = resolveHaivePaths17(root);
|
|
3963
|
+
if (!existsSync22(paths.memoriesDir)) {
|
|
3358
3964
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3359
3965
|
process.exitCode = 1;
|
|
3360
3966
|
return;
|
|
3361
3967
|
}
|
|
3362
3968
|
const all = await loadMemoriesFromDir4(paths.memoriesDir);
|
|
3363
|
-
const usage = await
|
|
3969
|
+
const usage = await loadUsageIndex6(paths);
|
|
3364
3970
|
const proposed = all.filter(({ memory: mem }) => {
|
|
3365
3971
|
if (mem.frontmatter.status !== "proposed") return false;
|
|
3366
3972
|
if (opts.scope && mem.frontmatter.scope !== opts.scope) return false;
|
|
@@ -3382,31 +3988,31 @@ function registerMemoryPending(memory2) {
|
|
|
3382
3988
|
console.log(
|
|
3383
3989
|
`${ui.bold(fm.id)} ${ui.dim(`${fm.scope}/${fm.type}`)} ${ui.dim(`age=${ageStr} reads=${u.read_count} rejections=${u.rejected_count}`)}`
|
|
3384
3990
|
);
|
|
3385
|
-
console.log(` ${ui.dim(
|
|
3991
|
+
console.log(` ${ui.dim(path23.relative(root, filePath))}`);
|
|
3386
3992
|
}
|
|
3387
3993
|
ui.info(`${proposed.length} pending`);
|
|
3388
3994
|
});
|
|
3389
3995
|
}
|
|
3390
3996
|
|
|
3391
3997
|
// src/commands/memory-query.ts
|
|
3392
|
-
import { existsSync as
|
|
3393
|
-
import
|
|
3998
|
+
import { existsSync as existsSync23 } from "fs";
|
|
3999
|
+
import path24 from "path";
|
|
3394
4000
|
import "commander";
|
|
3395
4001
|
import {
|
|
3396
4002
|
extractSnippet,
|
|
3397
|
-
findProjectRoot as
|
|
4003
|
+
findProjectRoot as findProjectRoot21,
|
|
3398
4004
|
literalMatchesAllTokens as literalMatchesAllTokens2,
|
|
3399
4005
|
literalMatchesAnyToken as literalMatchesAnyToken2,
|
|
3400
4006
|
pickSnippetNeedle,
|
|
3401
|
-
resolveHaivePaths as
|
|
4007
|
+
resolveHaivePaths as resolveHaivePaths18,
|
|
3402
4008
|
tokenizeQuery as tokenizeQuery2,
|
|
3403
4009
|
trackReads as trackReads2
|
|
3404
4010
|
} from "@hiveai/core";
|
|
3405
4011
|
function registerMemoryQuery(memory2) {
|
|
3406
|
-
memory2.command("query <text>").description("Search memories by id, tag, or substring (AND, OR fallback)").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
|
|
3407
|
-
const root =
|
|
3408
|
-
const paths =
|
|
3409
|
-
if (!
|
|
4012
|
+
memory2.command("query <text>").alias("search").description("Search memories by id, tag, or substring (AND, OR fallback). Alias: search").option("-d, --dir <dir>", "project root").option("--limit <n>", "max results", "20").option("--scope <scope>", "personal | team | module").option("--status <csv>", "filter by status (draft,proposed,validated,stale,rejected)").option("--show-rejected", "include rejected memories (hidden by default)").action(async (text, opts) => {
|
|
4013
|
+
const root = findProjectRoot21(opts.dir);
|
|
4014
|
+
const paths = resolveHaivePaths18(root);
|
|
4015
|
+
if (!existsSync23(paths.memoriesDir)) {
|
|
3410
4016
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
3411
4017
|
process.exitCode = 1;
|
|
3412
4018
|
return;
|
|
@@ -3447,7 +4053,7 @@ function registerMemoryQuery(memory2) {
|
|
|
3447
4053
|
const fm = mem.frontmatter;
|
|
3448
4054
|
const statusBadge = ui.statusBadge(fm.status);
|
|
3449
4055
|
console.log(`${ui.bold(fm.id)} ${ui.dim(fm.scope)} ${statusBadge}`);
|
|
3450
|
-
console.log(` ${ui.dim(
|
|
4056
|
+
console.log(` ${ui.dim(path24.relative(root, filePath))}`);
|
|
3451
4057
|
const snippet = extractSnippet(mem.body, snippetNeedle);
|
|
3452
4058
|
if (snippet) console.log(` ${snippet}`);
|
|
3453
4059
|
}
|
|
@@ -3464,22 +4070,22 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
3464
4070
|
}
|
|
3465
4071
|
|
|
3466
4072
|
// src/commands/memory-reject.ts
|
|
3467
|
-
import { writeFile as
|
|
3468
|
-
import { existsSync as
|
|
4073
|
+
import { writeFile as writeFile13 } from "fs/promises";
|
|
4074
|
+
import { existsSync as existsSync24 } from "fs";
|
|
3469
4075
|
import "commander";
|
|
3470
4076
|
import {
|
|
3471
|
-
findProjectRoot as
|
|
3472
|
-
loadUsageIndex as
|
|
4077
|
+
findProjectRoot as findProjectRoot22,
|
|
4078
|
+
loadUsageIndex as loadUsageIndex7,
|
|
3473
4079
|
recordRejection,
|
|
3474
|
-
resolveHaivePaths as
|
|
4080
|
+
resolveHaivePaths as resolveHaivePaths19,
|
|
3475
4081
|
saveUsageIndex,
|
|
3476
4082
|
serializeMemory as serializeMemory9
|
|
3477
4083
|
} from "@hiveai/core";
|
|
3478
4084
|
function registerMemoryReject(memory2) {
|
|
3479
4085
|
memory2.command("reject <id>").description("Record a rejection (blocks auto-promotion and lowers confidence)").option("-r, --reason <reason>", "why this memory is being rejected").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
3480
|
-
const root =
|
|
3481
|
-
const paths =
|
|
3482
|
-
if (!
|
|
4086
|
+
const root = findProjectRoot22(opts.dir);
|
|
4087
|
+
const paths = resolveHaivePaths19(root);
|
|
4088
|
+
if (!existsSync24(paths.memoriesDir)) {
|
|
3483
4089
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3484
4090
|
process.exitCode = 1;
|
|
3485
4091
|
return;
|
|
@@ -3491,7 +4097,7 @@ function registerMemoryReject(memory2) {
|
|
|
3491
4097
|
process.exitCode = 1;
|
|
3492
4098
|
return;
|
|
3493
4099
|
}
|
|
3494
|
-
await
|
|
4100
|
+
await writeFile13(
|
|
3495
4101
|
loaded.filePath,
|
|
3496
4102
|
serializeMemory9({
|
|
3497
4103
|
frontmatter: {
|
|
@@ -3503,7 +4109,7 @@ function registerMemoryReject(memory2) {
|
|
|
3503
4109
|
}),
|
|
3504
4110
|
"utf8"
|
|
3505
4111
|
);
|
|
3506
|
-
const idx = await
|
|
4112
|
+
const idx = await loadUsageIndex7(paths);
|
|
3507
4113
|
recordRejection(idx, id, opts.reason ?? null);
|
|
3508
4114
|
await saveUsageIndex(paths, idx);
|
|
3509
4115
|
const u = idx.by_id[id];
|
|
@@ -3515,22 +4121,22 @@ function registerMemoryReject(memory2) {
|
|
|
3515
4121
|
}
|
|
3516
4122
|
|
|
3517
4123
|
// src/commands/memory-rm.ts
|
|
3518
|
-
import { existsSync as
|
|
4124
|
+
import { existsSync as existsSync25 } from "fs";
|
|
3519
4125
|
import { unlink as unlink2 } from "fs/promises";
|
|
3520
|
-
import
|
|
4126
|
+
import path25 from "path";
|
|
3521
4127
|
import { createInterface } from "readline/promises";
|
|
3522
4128
|
import "commander";
|
|
3523
4129
|
import {
|
|
3524
|
-
findProjectRoot as
|
|
3525
|
-
loadUsageIndex as
|
|
3526
|
-
resolveHaivePaths as
|
|
4130
|
+
findProjectRoot as findProjectRoot23,
|
|
4131
|
+
loadUsageIndex as loadUsageIndex8,
|
|
4132
|
+
resolveHaivePaths as resolveHaivePaths20,
|
|
3527
4133
|
saveUsageIndex as saveUsageIndex2
|
|
3528
4134
|
} from "@hiveai/core";
|
|
3529
4135
|
function registerMemoryRm(memory2) {
|
|
3530
4136
|
memory2.command("rm <id>").description("Delete a memory file (and its usage entry by default)").option("-y, --yes", "skip the confirmation prompt").option("--keep-usage", "do not remove the usage.json entry").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
3531
|
-
const root =
|
|
3532
|
-
const paths =
|
|
3533
|
-
if (!
|
|
4137
|
+
const root = findProjectRoot23(opts.dir);
|
|
4138
|
+
const paths = resolveHaivePaths20(root);
|
|
4139
|
+
if (!existsSync25(paths.memoriesDir)) {
|
|
3534
4140
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3535
4141
|
process.exitCode = 1;
|
|
3536
4142
|
return;
|
|
@@ -3542,7 +4148,7 @@ function registerMemoryRm(memory2) {
|
|
|
3542
4148
|
process.exitCode = 1;
|
|
3543
4149
|
return;
|
|
3544
4150
|
}
|
|
3545
|
-
const rel =
|
|
4151
|
+
const rel = path25.relative(root, found.filePath);
|
|
3546
4152
|
if (!opts.yes) {
|
|
3547
4153
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3548
4154
|
const answer = (await rl.question(`Delete ${rel}? [y/N] `)).trim().toLowerCase();
|
|
@@ -3555,7 +4161,7 @@ function registerMemoryRm(memory2) {
|
|
|
3555
4161
|
await unlink2(found.filePath);
|
|
3556
4162
|
ui.success(`Deleted ${rel}`);
|
|
3557
4163
|
if (!opts.keepUsage) {
|
|
3558
|
-
const idx = await
|
|
4164
|
+
const idx = await loadUsageIndex8(paths);
|
|
3559
4165
|
if (idx.by_id[id]) {
|
|
3560
4166
|
delete idx.by_id[id];
|
|
3561
4167
|
await saveUsageIndex2(paths, idx);
|
|
@@ -3566,22 +4172,22 @@ function registerMemoryRm(memory2) {
|
|
|
3566
4172
|
}
|
|
3567
4173
|
|
|
3568
4174
|
// src/commands/memory-show.ts
|
|
3569
|
-
import { existsSync as
|
|
3570
|
-
import { readFile as
|
|
3571
|
-
import
|
|
4175
|
+
import { existsSync as existsSync26 } from "fs";
|
|
4176
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4177
|
+
import path26 from "path";
|
|
3572
4178
|
import "commander";
|
|
3573
4179
|
import {
|
|
3574
4180
|
deriveConfidence as deriveConfidence2,
|
|
3575
|
-
findProjectRoot as
|
|
4181
|
+
findProjectRoot as findProjectRoot24,
|
|
3576
4182
|
getUsage as getUsage6,
|
|
3577
|
-
loadUsageIndex as
|
|
3578
|
-
resolveHaivePaths as
|
|
4183
|
+
loadUsageIndex as loadUsageIndex9,
|
|
4184
|
+
resolveHaivePaths as resolveHaivePaths21
|
|
3579
4185
|
} from "@hiveai/core";
|
|
3580
4186
|
function registerMemoryShow(memory2) {
|
|
3581
4187
|
memory2.command("show <id>").description("Print a memory's frontmatter, body, and confidence/usage").option("--raw", "print the raw file contents instead of a summary").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
3582
|
-
const root =
|
|
3583
|
-
const paths =
|
|
3584
|
-
if (!
|
|
4188
|
+
const root = findProjectRoot24(opts.dir);
|
|
4189
|
+
const paths = resolveHaivePaths21(root);
|
|
4190
|
+
if (!existsSync26(paths.memoriesDir)) {
|
|
3585
4191
|
ui.error(`No .ai/memories at ${root}.`);
|
|
3586
4192
|
process.exitCode = 1;
|
|
3587
4193
|
return;
|
|
@@ -3594,11 +4200,11 @@ function registerMemoryShow(memory2) {
|
|
|
3594
4200
|
return;
|
|
3595
4201
|
}
|
|
3596
4202
|
if (opts.raw) {
|
|
3597
|
-
console.log(await
|
|
4203
|
+
console.log(await readFile10(found.filePath, "utf8"));
|
|
3598
4204
|
return;
|
|
3599
4205
|
}
|
|
3600
4206
|
const fm = found.memory.frontmatter;
|
|
3601
|
-
const usage = await
|
|
4207
|
+
const usage = await loadUsageIndex9(paths);
|
|
3602
4208
|
const u = getUsage6(usage, fm.id);
|
|
3603
4209
|
const conf = deriveConfidence2(fm, u);
|
|
3604
4210
|
console.log(ui.bold(fm.id));
|
|
@@ -3610,7 +4216,7 @@ function registerMemoryShow(memory2) {
|
|
|
3610
4216
|
if (fm.verified_at) console.log(`${ui.dim("verified:")} ${fm.verified_at}`);
|
|
3611
4217
|
if (fm.stale_reason) console.log(`${ui.dim("stale:")} ${fm.stale_reason}`);
|
|
3612
4218
|
console.log(`${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`);
|
|
3613
|
-
console.log(`${ui.dim("file:")} ${
|
|
4219
|
+
console.log(`${ui.dim("file:")} ${path26.relative(root, found.filePath)}`);
|
|
3614
4220
|
if (fm.anchor.paths.length || fm.anchor.symbols.length) {
|
|
3615
4221
|
console.log(ui.dim("anchor:"));
|
|
3616
4222
|
if (fm.anchor.commit) console.log(` ${ui.dim("commit:")} ${fm.anchor.commit}`);
|
|
@@ -3625,27 +4231,27 @@ function registerMemoryShow(memory2) {
|
|
|
3625
4231
|
}
|
|
3626
4232
|
|
|
3627
4233
|
// src/commands/memory-stats.ts
|
|
3628
|
-
import { existsSync as
|
|
3629
|
-
import
|
|
4234
|
+
import { existsSync as existsSync27 } from "fs";
|
|
4235
|
+
import path27 from "path";
|
|
3630
4236
|
import "commander";
|
|
3631
4237
|
import {
|
|
3632
4238
|
deriveConfidence as deriveConfidence3,
|
|
3633
|
-
findProjectRoot as
|
|
4239
|
+
findProjectRoot as findProjectRoot25,
|
|
3634
4240
|
getUsage as getUsage7,
|
|
3635
|
-
loadUsageIndex as
|
|
3636
|
-
resolveHaivePaths as
|
|
4241
|
+
loadUsageIndex as loadUsageIndex10,
|
|
4242
|
+
resolveHaivePaths as resolveHaivePaths22
|
|
3637
4243
|
} from "@hiveai/core";
|
|
3638
4244
|
function registerMemoryStats(memory2) {
|
|
3639
4245
|
memory2.command("stats").description("Show usage stats and confidence levels per memory").option("--id <id>", "show stats for a single memory id").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3640
|
-
const root =
|
|
3641
|
-
const paths =
|
|
3642
|
-
if (!
|
|
4246
|
+
const root = findProjectRoot25(opts.dir);
|
|
4247
|
+
const paths = resolveHaivePaths22(root);
|
|
4248
|
+
if (!existsSync27(paths.memoriesDir)) {
|
|
3643
4249
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
3644
4250
|
process.exitCode = 1;
|
|
3645
4251
|
return;
|
|
3646
4252
|
}
|
|
3647
4253
|
const all = await loadMemoriesFromDir4(paths.memoriesDir);
|
|
3648
|
-
const usage = await
|
|
4254
|
+
const usage = await loadUsageIndex10(paths);
|
|
3649
4255
|
const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
3650
4256
|
if (target.length === 0) {
|
|
3651
4257
|
ui.info(opts.id ? `No memory with id "${opts.id}".` : "No memories.");
|
|
@@ -3664,19 +4270,19 @@ function registerMemoryStats(memory2) {
|
|
|
3664
4270
|
console.log(
|
|
3665
4271
|
` ${ui.dim("status:")} ${fm.status} ${ui.dim("reads:")} ${u.read_count} ${ui.dim("rejections:")} ${u.rejected_count}`
|
|
3666
4272
|
);
|
|
3667
|
-
console.log(` ${ui.dim(
|
|
4273
|
+
console.log(` ${ui.dim(path27.relative(root, filePath))}`);
|
|
3668
4274
|
}
|
|
3669
4275
|
});
|
|
3670
4276
|
}
|
|
3671
4277
|
|
|
3672
4278
|
// src/commands/memory-verify.ts
|
|
3673
|
-
import { writeFile as
|
|
3674
|
-
import { existsSync as
|
|
3675
|
-
import
|
|
4279
|
+
import { writeFile as writeFile14 } from "fs/promises";
|
|
4280
|
+
import { existsSync as existsSync28 } from "fs";
|
|
4281
|
+
import path28 from "path";
|
|
3676
4282
|
import "commander";
|
|
3677
4283
|
import {
|
|
3678
|
-
findProjectRoot as
|
|
3679
|
-
resolveHaivePaths as
|
|
4284
|
+
findProjectRoot as findProjectRoot26,
|
|
4285
|
+
resolveHaivePaths as resolveHaivePaths23,
|
|
3680
4286
|
serializeMemory as serializeMemory10,
|
|
3681
4287
|
verifyAnchor as verifyAnchor2
|
|
3682
4288
|
} from "@hiveai/core";
|
|
@@ -3684,9 +4290,9 @@ function registerMemoryVerify(memory2) {
|
|
|
3684
4290
|
memory2.command("verify").description(
|
|
3685
4291
|
"Check that memory anchor paths still exist in the current codebase.\n\n A memory is 'stale' when its anchored file or symbol was moved, deleted, or renamed.\n Stale memories are shown with a warning in get_briefing and should be updated or deleted.\n\n haive sync runs this automatically. Use this command for on-demand checks or in CI.\n\n CI recommendation: add 'haive memory verify' to your haive-sync.yml PR check job\n to catch stale memories before they reach main.\n\n Examples:\n haive memory verify # check all, report only\n haive memory verify --update # mark stale/fresh on disk\n haive memory verify --id 2026-04-28-gotcha-x # check one memory\n"
|
|
3686
4292
|
).option("--id <id>", "verify a single memory by id").option("--all", "verify every memory (default if --id is omitted)").option("--update", "write status=stale or status=validated back to disk").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3687
|
-
const root =
|
|
3688
|
-
const paths =
|
|
3689
|
-
if (!
|
|
4293
|
+
const root = findProjectRoot26(opts.dir);
|
|
4294
|
+
const paths = resolveHaivePaths23(root);
|
|
4295
|
+
if (!existsSync28(paths.memoriesDir)) {
|
|
3690
4296
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
3691
4297
|
process.exitCode = 1;
|
|
3692
4298
|
return;
|
|
@@ -3709,7 +4315,7 @@ function registerMemoryVerify(memory2) {
|
|
|
3709
4315
|
anchorlessIds.push(mem.frontmatter.id);
|
|
3710
4316
|
continue;
|
|
3711
4317
|
}
|
|
3712
|
-
const rel =
|
|
4318
|
+
const rel = path28.relative(root, filePath);
|
|
3713
4319
|
if (result.stale) {
|
|
3714
4320
|
staleCount++;
|
|
3715
4321
|
console.log(`${ui.bold("STALE")} ${mem.frontmatter.id}`);
|
|
@@ -3724,7 +4330,7 @@ function registerMemoryVerify(memory2) {
|
|
|
3724
4330
|
}
|
|
3725
4331
|
if (opts.update) {
|
|
3726
4332
|
const next = applyVerification(mem, result);
|
|
3727
|
-
await
|
|
4333
|
+
await writeFile14(filePath, serializeMemory10(next), "utf8");
|
|
3728
4334
|
updated++;
|
|
3729
4335
|
}
|
|
3730
4336
|
}
|
|
@@ -3772,30 +4378,30 @@ function applyVerification(mem, result) {
|
|
|
3772
4378
|
}
|
|
3773
4379
|
|
|
3774
4380
|
// src/commands/memory-import.ts
|
|
3775
|
-
import { readFile as
|
|
3776
|
-
import { existsSync as
|
|
4381
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
4382
|
+
import { existsSync as existsSync29 } from "fs";
|
|
3777
4383
|
import "commander";
|
|
3778
4384
|
import {
|
|
3779
|
-
findProjectRoot as
|
|
3780
|
-
resolveHaivePaths as
|
|
4385
|
+
findProjectRoot as findProjectRoot27,
|
|
4386
|
+
resolveHaivePaths as resolveHaivePaths24
|
|
3781
4387
|
} from "@hiveai/core";
|
|
3782
4388
|
function registerMemoryImport(memory2) {
|
|
3783
4389
|
memory2.command("import").description(
|
|
3784
4390
|
"Parse a Markdown file and suggest memories via the import_docs MCP prompt (prints a ready-to-use prompt invocation)"
|
|
3785
4391
|
).requiredOption("--from <file>", "Markdown/text file to import from").option("--scope <scope>", "personal | team (default: team)", "team").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3786
|
-
const root =
|
|
3787
|
-
const paths =
|
|
3788
|
-
if (!
|
|
4392
|
+
const root = findProjectRoot27(opts.dir);
|
|
4393
|
+
const paths = resolveHaivePaths24(root);
|
|
4394
|
+
if (!existsSync29(paths.haiveDir)) {
|
|
3789
4395
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
3790
4396
|
process.exitCode = 1;
|
|
3791
4397
|
return;
|
|
3792
4398
|
}
|
|
3793
|
-
if (!
|
|
4399
|
+
if (!existsSync29(opts.from)) {
|
|
3794
4400
|
ui.error(`File not found: ${opts.from}`);
|
|
3795
4401
|
process.exitCode = 1;
|
|
3796
4402
|
return;
|
|
3797
4403
|
}
|
|
3798
|
-
const content = await
|
|
4404
|
+
const content = await readFile11(opts.from, "utf8");
|
|
3799
4405
|
const scope = opts.scope ?? "team";
|
|
3800
4406
|
ui.info(`Preparing import from: ${opts.from} (scope=${scope})`);
|
|
3801
4407
|
ui.info(`Content length: ${content.length} chars`);
|
|
@@ -3823,14 +4429,14 @@ function registerMemoryImport(memory2) {
|
|
|
3823
4429
|
}
|
|
3824
4430
|
|
|
3825
4431
|
// src/commands/memory-import-changelog.ts
|
|
3826
|
-
import { existsSync as
|
|
3827
|
-
import { readFile as
|
|
3828
|
-
import
|
|
4432
|
+
import { existsSync as existsSync30 } from "fs";
|
|
4433
|
+
import { readFile as readFile12, mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
|
|
4434
|
+
import path29 from "path";
|
|
3829
4435
|
import "commander";
|
|
3830
4436
|
import {
|
|
3831
4437
|
buildFrontmatter as buildFrontmatter5,
|
|
3832
|
-
findProjectRoot as
|
|
3833
|
-
resolveHaivePaths as
|
|
4438
|
+
findProjectRoot as findProjectRoot28,
|
|
4439
|
+
resolveHaivePaths as resolveHaivePaths25,
|
|
3834
4440
|
serializeMemory as serializeMemory11
|
|
3835
4441
|
} from "@hiveai/core";
|
|
3836
4442
|
function parseChangelog(content) {
|
|
@@ -3894,15 +4500,15 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
3894
4500
|
"--versions <csv>",
|
|
3895
4501
|
"only import specific versions (comma-separated), or 'latest' for the most recent breaking version"
|
|
3896
4502
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
3897
|
-
const root =
|
|
3898
|
-
const paths =
|
|
3899
|
-
const changelogPath =
|
|
3900
|
-
if (!
|
|
4503
|
+
const root = findProjectRoot28(opts.dir);
|
|
4504
|
+
const paths = resolveHaivePaths25(root);
|
|
4505
|
+
const changelogPath = path29.resolve(root, opts.fromChangelog);
|
|
4506
|
+
if (!existsSync30(changelogPath)) {
|
|
3901
4507
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
3902
4508
|
process.exitCode = 1;
|
|
3903
4509
|
return;
|
|
3904
4510
|
}
|
|
3905
|
-
const content = await
|
|
4511
|
+
const content = await readFile12(changelogPath, "utf8");
|
|
3906
4512
|
let entries = parseChangelog(content);
|
|
3907
4513
|
if (entries.length === 0) {
|
|
3908
4514
|
ui.warn("No breaking changes, deprecations, or removals found in the CHANGELOG.");
|
|
@@ -3916,10 +4522,10 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
3916
4522
|
entries = entries.filter((e) => requested.includes(e.version));
|
|
3917
4523
|
}
|
|
3918
4524
|
}
|
|
3919
|
-
const pkgName = opts.package ??
|
|
4525
|
+
const pkgName = opts.package ?? path29.basename(path29.dirname(changelogPath));
|
|
3920
4526
|
const scope = opts.scope ?? "team";
|
|
3921
|
-
const teamDir =
|
|
3922
|
-
await
|
|
4527
|
+
const teamDir = path29.join(paths.memoriesDir, scope);
|
|
4528
|
+
await mkdir11(teamDir, { recursive: true });
|
|
3923
4529
|
let saved = 0;
|
|
3924
4530
|
for (const entry of entries) {
|
|
3925
4531
|
const lines = [];
|
|
@@ -3941,7 +4547,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
3941
4547
|
lines.push("");
|
|
3942
4548
|
}
|
|
3943
4549
|
lines.push(
|
|
3944
|
-
`**Source:** \`${
|
|
4550
|
+
`**Source:** \`${path29.relative(root, changelogPath)}\`
|
|
3945
4551
|
**Action:** Update all usages of ${pkgName} if they rely on any of the above.`
|
|
3946
4552
|
);
|
|
3947
4553
|
const slug = `changelog-${pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase()}-v${entry.version.replace(/\./g, "-")}`;
|
|
@@ -3956,11 +4562,11 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
3956
4562
|
pkgName.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
|
|
3957
4563
|
`v${entry.version}`
|
|
3958
4564
|
],
|
|
3959
|
-
paths: [
|
|
4565
|
+
paths: [path29.relative(root, changelogPath)],
|
|
3960
4566
|
topic: `changelog-${pkgName}-${entry.version}`
|
|
3961
4567
|
});
|
|
3962
|
-
await
|
|
3963
|
-
|
|
4568
|
+
await writeFile15(
|
|
4569
|
+
path29.join(teamDir, `${fm.id}.md`),
|
|
3964
4570
|
serializeMemory11({ frontmatter: fm, body: lines.join("\n") }),
|
|
3965
4571
|
"utf8"
|
|
3966
4572
|
);
|
|
@@ -3983,17 +4589,17 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
3983
4589
|
}
|
|
3984
4590
|
|
|
3985
4591
|
// src/commands/memory-digest.ts
|
|
3986
|
-
import { existsSync as
|
|
3987
|
-
import { writeFile as
|
|
3988
|
-
import
|
|
4592
|
+
import { existsSync as existsSync31 } from "fs";
|
|
4593
|
+
import { writeFile as writeFile16 } from "fs/promises";
|
|
4594
|
+
import path30 from "path";
|
|
3989
4595
|
import "commander";
|
|
3990
4596
|
import {
|
|
3991
4597
|
deriveConfidence as deriveConfidence4,
|
|
3992
|
-
findProjectRoot as
|
|
4598
|
+
findProjectRoot as findProjectRoot29,
|
|
3993
4599
|
getUsage as getUsage8,
|
|
3994
4600
|
loadMemoriesFromDir as loadMemoriesFromDir5,
|
|
3995
|
-
loadUsageIndex as
|
|
3996
|
-
resolveHaivePaths as
|
|
4601
|
+
loadUsageIndex as loadUsageIndex11,
|
|
4602
|
+
resolveHaivePaths as resolveHaivePaths26
|
|
3997
4603
|
} from "@hiveai/core";
|
|
3998
4604
|
var CONFIDENCE_EMOJI = {
|
|
3999
4605
|
unverified: "\u2B1C",
|
|
@@ -4006,9 +4612,9 @@ function registerMemoryDigest(program2) {
|
|
|
4006
4612
|
program2.command("digest").description(
|
|
4007
4613
|
"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"
|
|
4008
4614
|
).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) => {
|
|
4009
|
-
const root =
|
|
4010
|
-
const paths =
|
|
4011
|
-
if (!
|
|
4615
|
+
const root = findProjectRoot29(opts.dir);
|
|
4616
|
+
const paths = resolveHaivePaths26(root);
|
|
4617
|
+
if (!existsSync31(paths.memoriesDir)) {
|
|
4012
4618
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
4013
4619
|
process.exitCode = 1;
|
|
4014
4620
|
return;
|
|
@@ -4017,7 +4623,7 @@ function registerMemoryDigest(program2) {
|
|
|
4017
4623
|
const scopeFilter = opts.scope ?? "team";
|
|
4018
4624
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
4019
4625
|
const all = await loadMemoriesFromDir5(paths.memoriesDir);
|
|
4020
|
-
const usage = await
|
|
4626
|
+
const usage = await loadUsageIndex11(paths);
|
|
4021
4627
|
const recent = all.filter(({ memory: mem }) => {
|
|
4022
4628
|
const fm = mem.frontmatter;
|
|
4023
4629
|
if (fm.type === "session_recap") return false;
|
|
@@ -4080,8 +4686,8 @@ function registerMemoryDigest(program2) {
|
|
|
4080
4686
|
);
|
|
4081
4687
|
const digest = lines.join("\n");
|
|
4082
4688
|
if (opts.out) {
|
|
4083
|
-
const outPath =
|
|
4084
|
-
await
|
|
4689
|
+
const outPath = path30.resolve(process.cwd(), opts.out);
|
|
4690
|
+
await writeFile16(outPath, digest, "utf8");
|
|
4085
4691
|
ui.success(`Digest written to ${opts.out} (${recent.length} memor${recent.length === 1 ? "y" : "ies"})`);
|
|
4086
4692
|
} else {
|
|
4087
4693
|
console.log(digest);
|
|
@@ -4090,18 +4696,52 @@ function registerMemoryDigest(program2) {
|
|
|
4090
4696
|
}
|
|
4091
4697
|
|
|
4092
4698
|
// src/commands/session-end.ts
|
|
4093
|
-
import { writeFile as
|
|
4094
|
-
import { existsSync as
|
|
4095
|
-
import
|
|
4699
|
+
import { writeFile as writeFile17, mkdir as mkdir12, readFile as readFile13, rm } from "fs/promises";
|
|
4700
|
+
import { existsSync as existsSync32 } from "fs";
|
|
4701
|
+
import path31 from "path";
|
|
4096
4702
|
import "commander";
|
|
4097
4703
|
import {
|
|
4098
4704
|
buildFrontmatter as buildFrontmatter6,
|
|
4099
|
-
findProjectRoot as
|
|
4705
|
+
findProjectRoot as findProjectRoot30,
|
|
4100
4706
|
loadMemoriesFromDir as loadMemoriesFromDir6,
|
|
4101
4707
|
memoryFilePath as memoryFilePath5,
|
|
4102
|
-
resolveHaivePaths as
|
|
4708
|
+
resolveHaivePaths as resolveHaivePaths27,
|
|
4103
4709
|
serializeMemory as serializeMemory12
|
|
4104
4710
|
} from "@hiveai/core";
|
|
4711
|
+
async function buildAutoRecap(paths) {
|
|
4712
|
+
const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
4713
|
+
if (!existsSync32(obsFile)) return null;
|
|
4714
|
+
const raw = await readFile13(obsFile, "utf8").catch(() => "");
|
|
4715
|
+
if (!raw.trim()) return null;
|
|
4716
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
4717
|
+
const obs = [];
|
|
4718
|
+
for (const line of lines) {
|
|
4719
|
+
try {
|
|
4720
|
+
obs.push(JSON.parse(line));
|
|
4721
|
+
} catch {
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
if (obs.length === 0) return null;
|
|
4725
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
4726
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
4727
|
+
const summaries = [];
|
|
4728
|
+
for (const o of obs) {
|
|
4729
|
+
toolCounts.set(o.tool, (toolCounts.get(o.tool) ?? 0) + 1);
|
|
4730
|
+
for (const f of o.files ?? []) fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
4731
|
+
if (summaries.length < 10) summaries.push(`- ${o.summary}`);
|
|
4732
|
+
}
|
|
4733
|
+
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([t, c]) => `${t} \xD7${c}`).join(", ");
|
|
4734
|
+
const topFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
|
|
4735
|
+
const goal = `Auto-captured session \u2014 ${obs.length} tool calls (${topTools})`;
|
|
4736
|
+
const accomplished = summaries.length ? `Recent activity:
|
|
4737
|
+
${summaries.join("\n")}` : `Activity captured but no parseable summaries.`;
|
|
4738
|
+
return {
|
|
4739
|
+
goal,
|
|
4740
|
+
accomplished,
|
|
4741
|
+
files: topFiles.map(([f]) => f),
|
|
4742
|
+
rawCount: obs.length
|
|
4743
|
+
};
|
|
4744
|
+
}
|
|
4105
4745
|
function buildRecapBody(opts) {
|
|
4106
4746
|
const lines = [];
|
|
4107
4747
|
lines.push(`## Goal
|
|
@@ -4148,24 +4788,53 @@ function registerSessionEnd(session2) {
|
|
|
4148
4788
|
--files src/payments/WebhookController.ts,src/payments/WebhookService.ts \\\\
|
|
4149
4789
|
--next "Add integration tests for webhook signature validation"
|
|
4150
4790
|
`
|
|
4151
|
-
).
|
|
4152
|
-
const root =
|
|
4153
|
-
const paths =
|
|
4154
|
-
if (!
|
|
4791
|
+
).option("--goal <text>", "what you were trying to accomplish (1\u20132 sentences)").option("--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("--auto", "synthesize the recap from .ai/.cache/observations.jsonl (used by Claude Code SessionEnd hook)").option("--quiet", "suppress non-error output (for hook use)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4792
|
+
const root = findProjectRoot30(opts.dir);
|
|
4793
|
+
const paths = resolveHaivePaths27(root);
|
|
4794
|
+
if (!existsSync32(paths.haiveDir)) {
|
|
4795
|
+
if (opts.auto || opts.quiet) return;
|
|
4155
4796
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
4156
4797
|
process.exitCode = 1;
|
|
4157
4798
|
return;
|
|
4158
4799
|
}
|
|
4800
|
+
let resolvedFiles = opts.files;
|
|
4801
|
+
let goal = opts.goal;
|
|
4802
|
+
let accomplished = opts.accomplished;
|
|
4803
|
+
if (opts.auto) {
|
|
4804
|
+
const synth = await buildAutoRecap(paths);
|
|
4805
|
+
if (!synth) return;
|
|
4806
|
+
goal = goal ?? synth.goal;
|
|
4807
|
+
accomplished = accomplished ?? synth.accomplished;
|
|
4808
|
+
if (!resolvedFiles && synth.files.length) resolvedFiles = synth.files.join(",");
|
|
4809
|
+
}
|
|
4810
|
+
if (!goal || !accomplished) {
|
|
4811
|
+
if (opts.quiet) return;
|
|
4812
|
+
ui.error("session-end requires --goal and --accomplished (or pass --auto with captured observations).");
|
|
4813
|
+
process.exitCode = 1;
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4159
4816
|
const scope = opts.scope ?? "personal";
|
|
4160
|
-
const body = buildRecapBody(
|
|
4817
|
+
const body = buildRecapBody({
|
|
4818
|
+
goal,
|
|
4819
|
+
accomplished,
|
|
4820
|
+
discoveries: opts.discoveries,
|
|
4821
|
+
files: resolvedFiles,
|
|
4822
|
+
next: opts.next
|
|
4823
|
+
});
|
|
4161
4824
|
const topic = recapTopic(scope, opts.module);
|
|
4162
|
-
const filesTouched = parseCsv5(
|
|
4163
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
4164
|
-
if (missingPaths.length > 0) {
|
|
4825
|
+
const filesTouched = parseCsv5(resolvedFiles);
|
|
4826
|
+
const missingPaths = filesTouched.filter((p) => !existsSync32(path31.resolve(root, p)));
|
|
4827
|
+
if (missingPaths.length > 0 && !opts.quiet) {
|
|
4165
4828
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
4166
4829
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
4167
4830
|
}
|
|
4168
|
-
|
|
4831
|
+
const cleanupObservations = async () => {
|
|
4832
|
+
if (!opts.auto) return;
|
|
4833
|
+
const obsFile = path31.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
4834
|
+
if (existsSync32(obsFile)) await rm(obsFile).catch(() => {
|
|
4835
|
+
});
|
|
4836
|
+
};
|
|
4837
|
+
if (existsSync32(paths.memoriesDir)) {
|
|
4169
4838
|
const existing = await loadMemoriesFromDir6(paths.memoriesDir);
|
|
4170
4839
|
const topicMatch = existing.find(
|
|
4171
4840
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
@@ -4181,9 +4850,12 @@ function registerSessionEnd(session2) {
|
|
|
4181
4850
|
paths: filesTouched.length ? filesTouched : fm.anchor.paths
|
|
4182
4851
|
}
|
|
4183
4852
|
};
|
|
4184
|
-
await
|
|
4185
|
-
|
|
4186
|
-
|
|
4853
|
+
await writeFile17(topicMatch.filePath, serializeMemory12({ frontmatter: newFrontmatter, body }), "utf8");
|
|
4854
|
+
await cleanupObservations();
|
|
4855
|
+
if (!opts.quiet) {
|
|
4856
|
+
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
4857
|
+
ui.info(`id=${fm.id} file=${path31.relative(root, topicMatch.filePath)}`);
|
|
4858
|
+
}
|
|
4187
4859
|
return;
|
|
4188
4860
|
}
|
|
4189
4861
|
}
|
|
@@ -4198,11 +4870,14 @@ function registerSessionEnd(session2) {
|
|
|
4198
4870
|
status: "validated"
|
|
4199
4871
|
});
|
|
4200
4872
|
const file = memoryFilePath5(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
4201
|
-
await
|
|
4202
|
-
await
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4873
|
+
await mkdir12(path31.dirname(file), { recursive: true });
|
|
4874
|
+
await writeFile17(file, serializeMemory12({ frontmatter, body }), "utf8");
|
|
4875
|
+
await cleanupObservations();
|
|
4876
|
+
if (!opts.quiet) {
|
|
4877
|
+
ui.success(`Session recap created`);
|
|
4878
|
+
ui.info(`id=${frontmatter.id} scope=${scope} file=${path31.relative(root, file)}`);
|
|
4879
|
+
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
4880
|
+
}
|
|
4206
4881
|
});
|
|
4207
4882
|
}
|
|
4208
4883
|
function parseCsv5(value) {
|
|
@@ -4211,15 +4886,15 @@ function parseCsv5(value) {
|
|
|
4211
4886
|
}
|
|
4212
4887
|
|
|
4213
4888
|
// src/commands/snapshot.ts
|
|
4214
|
-
import { existsSync as
|
|
4889
|
+
import { existsSync as existsSync33 } from "fs";
|
|
4215
4890
|
import { readdir as readdir2 } from "fs/promises";
|
|
4216
|
-
import
|
|
4891
|
+
import path32 from "path";
|
|
4217
4892
|
import "commander";
|
|
4218
4893
|
import {
|
|
4219
4894
|
diffContract,
|
|
4220
|
-
findProjectRoot as
|
|
4895
|
+
findProjectRoot as findProjectRoot31,
|
|
4221
4896
|
loadConfig as loadConfig2,
|
|
4222
|
-
resolveHaivePaths as
|
|
4897
|
+
resolveHaivePaths as resolveHaivePaths28,
|
|
4223
4898
|
snapshotContract
|
|
4224
4899
|
} from "@hiveai/core";
|
|
4225
4900
|
function registerSnapshot(program2) {
|
|
@@ -4244,16 +4919,16 @@ function registerSnapshot(program2) {
|
|
|
4244
4919
|
"--format <format>",
|
|
4245
4920
|
"contract format: openapi | graphql | proto | typescript | json-schema (auto-detected if omitted)"
|
|
4246
4921
|
).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) => {
|
|
4247
|
-
const root =
|
|
4248
|
-
const paths =
|
|
4249
|
-
if (!
|
|
4922
|
+
const root = findProjectRoot31(opts.dir);
|
|
4923
|
+
const paths = resolveHaivePaths28(root);
|
|
4924
|
+
if (!existsSync33(paths.haiveDir)) {
|
|
4250
4925
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
4251
4926
|
process.exitCode = 1;
|
|
4252
4927
|
return;
|
|
4253
4928
|
}
|
|
4254
4929
|
if (opts.list) {
|
|
4255
|
-
const contractsDir =
|
|
4256
|
-
if (!
|
|
4930
|
+
const contractsDir = path32.join(paths.haiveDir, "contracts");
|
|
4931
|
+
if (!existsSync33(contractsDir)) {
|
|
4257
4932
|
console.log(ui.dim("No contract snapshots found."));
|
|
4258
4933
|
return;
|
|
4259
4934
|
}
|
|
@@ -4308,7 +4983,7 @@ function registerSnapshot(program2) {
|
|
|
4308
4983
|
return;
|
|
4309
4984
|
}
|
|
4310
4985
|
const contractPath = opts.contract;
|
|
4311
|
-
const name = opts.name ??
|
|
4986
|
+
const name = opts.name ?? path32.basename(contractPath, path32.extname(contractPath));
|
|
4312
4987
|
const format = opts.format ?? detectFormat(contractPath) ?? "openapi";
|
|
4313
4988
|
const contract = { name, path: contractPath, format };
|
|
4314
4989
|
try {
|
|
@@ -4363,8 +5038,8 @@ async function runDiff(root, haiveDir, contract) {
|
|
|
4363
5038
|
}
|
|
4364
5039
|
}
|
|
4365
5040
|
function detectFormat(filePath) {
|
|
4366
|
-
const ext =
|
|
4367
|
-
const base =
|
|
5041
|
+
const ext = path32.extname(filePath).toLowerCase();
|
|
5042
|
+
const base = path32.basename(filePath).toLowerCase();
|
|
4368
5043
|
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
4369
5044
|
if (base.includes("openapi") || base.includes("swagger")) return "openapi";
|
|
4370
5045
|
if (base.includes("schema") || base.includes("graphql")) return "graphql";
|
|
@@ -4377,16 +5052,16 @@ function detectFormat(filePath) {
|
|
|
4377
5052
|
}
|
|
4378
5053
|
|
|
4379
5054
|
// src/commands/hub.ts
|
|
4380
|
-
import { existsSync as
|
|
4381
|
-
import { mkdir as
|
|
4382
|
-
import
|
|
5055
|
+
import { existsSync as existsSync34 } from "fs";
|
|
5056
|
+
import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile18, copyFile } from "fs/promises";
|
|
5057
|
+
import path33 from "path";
|
|
4383
5058
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4384
5059
|
import "commander";
|
|
4385
5060
|
import {
|
|
4386
|
-
findProjectRoot as
|
|
5061
|
+
findProjectRoot as findProjectRoot32,
|
|
4387
5062
|
loadConfig as loadConfig3,
|
|
4388
5063
|
loadMemoriesFromDir as loadMemoriesFromDir7,
|
|
4389
|
-
resolveHaivePaths as
|
|
5064
|
+
resolveHaivePaths as resolveHaivePaths29,
|
|
4390
5065
|
saveConfig as saveConfig2,
|
|
4391
5066
|
serializeMemory as serializeMemory13
|
|
4392
5067
|
} from "@hiveai/core";
|
|
@@ -4398,8 +5073,8 @@ function registerHub(program2) {
|
|
|
4398
5073
|
hub.command("init <hubPath>").description(
|
|
4399
5074
|
"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"
|
|
4400
5075
|
).action(async (hubPath) => {
|
|
4401
|
-
const absPath =
|
|
4402
|
-
await
|
|
5076
|
+
const absPath = path33.resolve(hubPath);
|
|
5077
|
+
await mkdir13(absPath, { recursive: true });
|
|
4403
5078
|
const gitCheck = spawnSync3("git", ["rev-parse", "--git-dir"], { cwd: absPath });
|
|
4404
5079
|
if (gitCheck.status !== 0) {
|
|
4405
5080
|
const init = spawnSync3("git", ["init"], { cwd: absPath, encoding: "utf8" });
|
|
@@ -4409,10 +5084,10 @@ function registerHub(program2) {
|
|
|
4409
5084
|
return;
|
|
4410
5085
|
}
|
|
4411
5086
|
}
|
|
4412
|
-
const sharedDir =
|
|
4413
|
-
await
|
|
4414
|
-
await
|
|
4415
|
-
|
|
5087
|
+
const sharedDir = path33.join(absPath, ".ai", "memories", "shared");
|
|
5088
|
+
await mkdir13(sharedDir, { recursive: true });
|
|
5089
|
+
await writeFile18(
|
|
5090
|
+
path33.join(absPath, ".ai", "README.md"),
|
|
4416
5091
|
`# hAIve Team Knowledge Hub
|
|
4417
5092
|
|
|
4418
5093
|
This repo is a shared knowledge hub for hAIve.
|
|
@@ -4433,8 +5108,8 @@ haive hub pull # import into a project
|
|
|
4433
5108
|
`,
|
|
4434
5109
|
"utf8"
|
|
4435
5110
|
);
|
|
4436
|
-
await
|
|
4437
|
-
|
|
5111
|
+
await writeFile18(
|
|
5112
|
+
path33.join(absPath, ".gitignore"),
|
|
4438
5113
|
".ai/.cache/\n.ai/memories/personal/\n",
|
|
4439
5114
|
"utf8"
|
|
4440
5115
|
);
|
|
@@ -4449,7 +5124,7 @@ haive hub pull # import into a project
|
|
|
4449
5124
|
`
|
|
4450
5125
|
Next steps:
|
|
4451
5126
|
1. Add hubPath to your project's .ai/haive.config.json:
|
|
4452
|
-
{ "hubPath": "${
|
|
5127
|
+
{ "hubPath": "${path33.relative(process.cwd(), absPath)}" }
|
|
4453
5128
|
2. Run \`haive hub push\` to publish your shared memories
|
|
4454
5129
|
3. Share ${absPath} with teammates (git remote, NFS, etc.)
|
|
4455
5130
|
`
|
|
@@ -4468,8 +5143,8 @@ Next steps:
|
|
|
4468
5143
|
haive hub push --commit --message "feat: add payment API contract memories"
|
|
4469
5144
|
`
|
|
4470
5145
|
).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) => {
|
|
4471
|
-
const root =
|
|
4472
|
-
const paths =
|
|
5146
|
+
const root = findProjectRoot32(opts.dir);
|
|
5147
|
+
const paths = resolveHaivePaths29(root);
|
|
4473
5148
|
const config = await loadConfig3(paths);
|
|
4474
5149
|
if (!config.hubPath) {
|
|
4475
5150
|
ui.error(
|
|
@@ -4478,15 +5153,15 @@ Next steps:
|
|
|
4478
5153
|
process.exitCode = 1;
|
|
4479
5154
|
return;
|
|
4480
5155
|
}
|
|
4481
|
-
const hubRoot =
|
|
4482
|
-
if (!
|
|
5156
|
+
const hubRoot = path33.resolve(root, config.hubPath);
|
|
5157
|
+
if (!existsSync34(hubRoot)) {
|
|
4483
5158
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
4484
5159
|
process.exitCode = 1;
|
|
4485
5160
|
return;
|
|
4486
5161
|
}
|
|
4487
|
-
const projectName =
|
|
4488
|
-
const destDir =
|
|
4489
|
-
await
|
|
5162
|
+
const projectName = path33.basename(root);
|
|
5163
|
+
const destDir = path33.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
5164
|
+
await mkdir13(destDir, { recursive: true });
|
|
4490
5165
|
const all = await loadMemoriesFromDir7(paths.memoriesDir);
|
|
4491
5166
|
const shared = all.filter(
|
|
4492
5167
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
@@ -4504,15 +5179,15 @@ Next steps:
|
|
|
4504
5179
|
for (const { memory: memory2 } of shared) {
|
|
4505
5180
|
const fm = memory2.frontmatter;
|
|
4506
5181
|
const fileName = `${fm.id}.md`;
|
|
4507
|
-
const destPath =
|
|
4508
|
-
await
|
|
5182
|
+
const destPath = path33.join(destDir, fileName);
|
|
5183
|
+
await writeFile18(destPath, serializeMemory13(memory2), "utf8");
|
|
4509
5184
|
pushed++;
|
|
4510
5185
|
}
|
|
4511
5186
|
console.log(ui.green(`\u2713 Pushed ${pushed} shared memor${pushed === 1 ? "y" : "ies"} to hub`));
|
|
4512
5187
|
console.log(ui.dim(` Location: ${destDir}`));
|
|
4513
5188
|
if (opts.commit) {
|
|
4514
5189
|
const message = opts.message ?? `haive: sync shared memories from ${projectName} (${pushed} memories)`;
|
|
4515
|
-
spawnSync3("git", ["add",
|
|
5190
|
+
spawnSync3("git", ["add", path33.join(".ai", "memories", "shared", projectName)], {
|
|
4516
5191
|
cwd: hubRoot
|
|
4517
5192
|
});
|
|
4518
5193
|
const commit = spawnSync3("git", ["commit", "-m", message], {
|
|
@@ -4537,8 +5212,8 @@ Next steps:
|
|
|
4537
5212
|
hub.command("pull").description(
|
|
4538
5213
|
"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"
|
|
4539
5214
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4540
|
-
const root =
|
|
4541
|
-
const paths =
|
|
5215
|
+
const root = findProjectRoot32(opts.dir);
|
|
5216
|
+
const paths = resolveHaivePaths29(root);
|
|
4542
5217
|
const config = await loadConfig3(paths);
|
|
4543
5218
|
if (!config.hubPath) {
|
|
4544
5219
|
ui.error(
|
|
@@ -4547,13 +5222,13 @@ Next steps:
|
|
|
4547
5222
|
process.exitCode = 1;
|
|
4548
5223
|
return;
|
|
4549
5224
|
}
|
|
4550
|
-
const hubRoot =
|
|
4551
|
-
const hubSharedDir =
|
|
4552
|
-
if (!
|
|
5225
|
+
const hubRoot = path33.resolve(root, config.hubPath);
|
|
5226
|
+
const hubSharedDir = path33.join(hubRoot, ".ai", "memories", "shared");
|
|
5227
|
+
if (!existsSync34(hubSharedDir)) {
|
|
4553
5228
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
4554
5229
|
return;
|
|
4555
5230
|
}
|
|
4556
|
-
const projectName =
|
|
5231
|
+
const projectName = path33.basename(root);
|
|
4557
5232
|
const { readdir: readdir3 } = await import("fs/promises");
|
|
4558
5233
|
const projectDirs = (await readdir3(hubSharedDir, { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== projectName).map((d) => d.name);
|
|
4559
5234
|
if (projectDirs.length === 0) {
|
|
@@ -4563,17 +5238,17 @@ Next steps:
|
|
|
4563
5238
|
let totalImported = 0;
|
|
4564
5239
|
let totalUpdated = 0;
|
|
4565
5240
|
for (const sourceName of projectDirs) {
|
|
4566
|
-
const sourceDir =
|
|
4567
|
-
const destDir =
|
|
4568
|
-
await
|
|
5241
|
+
const sourceDir = path33.join(hubSharedDir, sourceName);
|
|
5242
|
+
const destDir = path33.join(paths.memoriesDir, "shared", sourceName);
|
|
5243
|
+
await mkdir13(destDir, { recursive: true });
|
|
4569
5244
|
const sourceFiles = (await readdir3(sourceDir)).filter((f) => f.endsWith(".md"));
|
|
4570
5245
|
const { loadMemoriesFromDir: loadDir } = await import("@hiveai/core");
|
|
4571
5246
|
const existingInDest = await loadDir(destDir);
|
|
4572
5247
|
const existingIds = new Set(existingInDest.map(({ memory: memory2 }) => memory2.frontmatter.id));
|
|
4573
5248
|
for (const file of sourceFiles) {
|
|
4574
|
-
const srcPath =
|
|
4575
|
-
const destPath =
|
|
4576
|
-
const fileContent = await
|
|
5249
|
+
const srcPath = path33.join(sourceDir, file);
|
|
5250
|
+
const destPath = path33.join(destDir, file);
|
|
5251
|
+
const fileContent = await readFile14(srcPath, "utf8");
|
|
4577
5252
|
const alreadyTagged = fileContent.includes(`cross-repo:${sourceName}`);
|
|
4578
5253
|
if (!alreadyTagged) {
|
|
4579
5254
|
await copyFile(srcPath, destPath);
|
|
@@ -4596,21 +5271,21 @@ Next steps:
|
|
|
4596
5271
|
);
|
|
4597
5272
|
});
|
|
4598
5273
|
hub.command("status").description("Show hub sync status.").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4599
|
-
const root =
|
|
4600
|
-
const paths =
|
|
5274
|
+
const root = findProjectRoot32(opts.dir);
|
|
5275
|
+
const paths = resolveHaivePaths29(root);
|
|
4601
5276
|
const config = await loadConfig3(paths);
|
|
4602
5277
|
console.log(ui.bold("Hub status"));
|
|
4603
5278
|
console.log(
|
|
4604
5279
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
4605
5280
|
);
|
|
4606
|
-
const sharedDir =
|
|
4607
|
-
if (
|
|
5281
|
+
const sharedDir = path33.join(paths.memoriesDir, "shared");
|
|
5282
|
+
if (existsSync34(sharedDir)) {
|
|
4608
5283
|
const { readdir: readdir3 } = await import("fs/promises");
|
|
4609
5284
|
const sources = (await readdir3(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4610
5285
|
console.log(`
|
|
4611
5286
|
Imported from ${sources.length} source(s):`);
|
|
4612
5287
|
for (const src of sources) {
|
|
4613
|
-
const files = (await readdir3(
|
|
5288
|
+
const files = (await readdir3(path33.join(sharedDir, src))).filter((f) => f.endsWith(".md"));
|
|
4614
5289
|
console.log(` ${src}: ${files.length} memor${files.length === 1 ? "y" : "ies"}`);
|
|
4615
5290
|
}
|
|
4616
5291
|
} else {
|
|
@@ -4625,8 +5300,8 @@ Next steps:
|
|
|
4625
5300
|
if (outgoing.length > 0) {
|
|
4626
5301
|
console.log(ui.dim(" Run `haive hub push` to publish them to the hub."));
|
|
4627
5302
|
}
|
|
4628
|
-
void
|
|
4629
|
-
void
|
|
5303
|
+
void readFile14;
|
|
5304
|
+
void writeFile18;
|
|
4630
5305
|
void saveConfig2;
|
|
4631
5306
|
});
|
|
4632
5307
|
}
|
|
@@ -4635,16 +5310,21 @@ Next steps:
|
|
|
4635
5310
|
import "commander";
|
|
4636
5311
|
import {
|
|
4637
5312
|
aggregateUsage,
|
|
4638
|
-
findProjectRoot as
|
|
5313
|
+
findProjectRoot as findProjectRoot33,
|
|
5314
|
+
loadUsageIndex as loadUsageIndex12,
|
|
4639
5315
|
parseSince,
|
|
4640
5316
|
readUsageEvents,
|
|
4641
|
-
resolveHaivePaths as
|
|
5317
|
+
resolveHaivePaths as resolveHaivePaths30,
|
|
4642
5318
|
usageLogSize
|
|
4643
5319
|
} from "@hiveai/core";
|
|
4644
5320
|
function registerStats(program2) {
|
|
4645
|
-
program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4646
|
-
const root =
|
|
4647
|
-
const paths =
|
|
5321
|
+
program2.command("stats").description("Show MCP tool-usage stats over a window (e.g. --since 7d).").option("--since <window>", "ISO date or relative (e.g. '7d', '24h', '30m')", "30d").option("--json", "emit JSON instead of human-readable output", false).option("--memory-hits", "show top-read memories (which mems are actually being used)", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
5322
|
+
const root = findProjectRoot33(opts.dir);
|
|
5323
|
+
const paths = resolveHaivePaths30(root);
|
|
5324
|
+
if (opts.memoryHits) {
|
|
5325
|
+
await renderMemoryHits(paths, opts);
|
|
5326
|
+
return;
|
|
5327
|
+
}
|
|
4648
5328
|
const size = await usageLogSize(paths);
|
|
4649
5329
|
if (!size.exists) {
|
|
4650
5330
|
if (opts.json) {
|
|
@@ -4689,14 +5369,57 @@ function registerStats(program2) {
|
|
|
4689
5369
|
}
|
|
4690
5370
|
});
|
|
4691
5371
|
}
|
|
5372
|
+
async function renderMemoryHits(paths, opts) {
|
|
5373
|
+
const index = await loadUsageIndex12(paths);
|
|
5374
|
+
const since = parseSince(opts.since ?? "30d");
|
|
5375
|
+
const sinceMs = since ? new Date(since).getTime() : null;
|
|
5376
|
+
const entries = Object.entries(index.by_id).map(([id, usage]) => ({ id, ...usage })).filter((e) => e.read_count > 0).filter((e) => {
|
|
5377
|
+
if (!sinceMs || !e.last_read_at) return !sinceMs;
|
|
5378
|
+
return new Date(e.last_read_at).getTime() >= sinceMs;
|
|
5379
|
+
}).sort((a, b) => b.read_count - a.read_count);
|
|
5380
|
+
if (opts.json) {
|
|
5381
|
+
console.log(JSON.stringify({
|
|
5382
|
+
window: opts.since ?? "30d",
|
|
5383
|
+
total_mems_with_hits: entries.length,
|
|
5384
|
+
top: entries.slice(0, 50)
|
|
5385
|
+
}, null, 2));
|
|
5386
|
+
return;
|
|
5387
|
+
}
|
|
5388
|
+
const window = opts.since ?? "30d";
|
|
5389
|
+
console.log(ui.bold(`Memory hits (${window})`));
|
|
5390
|
+
if (entries.length === 0) {
|
|
5391
|
+
ui.info(
|
|
5392
|
+
`No memory reads recorded in window. Reads are logged when \`haive briefing\` or \`haive memory query\` surface a memory.`
|
|
5393
|
+
);
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
console.log(
|
|
5397
|
+
` ${ui.dim("memories with hits:")} ${entries.length} ${ui.dim("total reads:")} ${entries.reduce((a, e) => a + e.read_count, 0)}`
|
|
5398
|
+
);
|
|
5399
|
+
console.log();
|
|
5400
|
+
console.log(ui.bold("Top read memories:"));
|
|
5401
|
+
const maxCount = entries[0].read_count;
|
|
5402
|
+
for (const e of entries.slice(0, 25)) {
|
|
5403
|
+
const bar = "\u2588".repeat(Math.max(1, Math.round(e.read_count / maxCount * 20)));
|
|
5404
|
+
const lastRead = e.last_read_at?.slice(0, 10) ?? "?";
|
|
5405
|
+
console.log(
|
|
5406
|
+
` ${ui.bold(String(e.read_count).padStart(4))} ${ui.green(bar.padEnd(20))} ${e.id} ${ui.dim(`last: ${lastRead}`)}`
|
|
5407
|
+
);
|
|
5408
|
+
}
|
|
5409
|
+
const dead = Object.keys(index.by_id).length - entries.length;
|
|
5410
|
+
if (dead > 0) {
|
|
5411
|
+
console.log();
|
|
5412
|
+
ui.info(`${dead} memor${dead === 1 ? "y" : "ies"} never read in window \u2014 candidates for cleanup (run \`haive doctor\`).`);
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
4692
5415
|
|
|
4693
5416
|
// src/commands/bench.ts
|
|
4694
5417
|
import { performance } from "perf_hooks";
|
|
4695
5418
|
import "commander";
|
|
4696
5419
|
import {
|
|
4697
5420
|
estimateTokens,
|
|
4698
|
-
findProjectRoot as
|
|
4699
|
-
resolveHaivePaths as
|
|
5421
|
+
findProjectRoot as findProjectRoot34,
|
|
5422
|
+
resolveHaivePaths as resolveHaivePaths31
|
|
4700
5423
|
} from "@hiveai/core";
|
|
4701
5424
|
import {
|
|
4702
5425
|
antiPatternsCheck,
|
|
@@ -4708,8 +5431,8 @@ import {
|
|
|
4708
5431
|
} from "@hiveai/mcp";
|
|
4709
5432
|
function registerBench(program2) {
|
|
4710
5433
|
program2.command("bench").description("Self-test the local hAIve setup: runs core MCP tools against this project and reports latency + payload size.").option("-t, --task <task>", "task description for ranking-aware tools", "audit dependencies for security risks").option("--json", "emit JSON instead of a table", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4711
|
-
const root =
|
|
4712
|
-
const paths =
|
|
5434
|
+
const root = findProjectRoot34(opts.dir);
|
|
5435
|
+
const paths = resolveHaivePaths31(root);
|
|
4713
5436
|
const ctx = { paths };
|
|
4714
5437
|
const task = opts.task ?? "audit dependencies for security risks";
|
|
4715
5438
|
const scenarios = [
|
|
@@ -4828,19 +5551,19 @@ function summarize(name, t0, payload, notes) {
|
|
|
4828
5551
|
}
|
|
4829
5552
|
|
|
4830
5553
|
// src/commands/memory-suggest.ts
|
|
4831
|
-
import { mkdir as
|
|
4832
|
-
import { existsSync as
|
|
4833
|
-
import
|
|
5554
|
+
import { mkdir as mkdir14, writeFile as writeFile19 } from "fs/promises";
|
|
5555
|
+
import { existsSync as existsSync35 } from "fs";
|
|
5556
|
+
import path34 from "path";
|
|
4834
5557
|
import "commander";
|
|
4835
5558
|
import {
|
|
4836
5559
|
aggregateUsage as aggregateUsage2,
|
|
4837
5560
|
buildFrontmatter as buildFrontmatter7,
|
|
4838
|
-
findProjectRoot as
|
|
5561
|
+
findProjectRoot as findProjectRoot35,
|
|
4839
5562
|
loadMemoriesFromDir as loadMemoriesFromDir8,
|
|
4840
5563
|
memoryFilePath as memoryFilePath6,
|
|
4841
5564
|
parseSince as parseSince2,
|
|
4842
5565
|
readUsageEvents as readUsageEvents2,
|
|
4843
|
-
resolveHaivePaths as
|
|
5566
|
+
resolveHaivePaths as resolveHaivePaths32,
|
|
4844
5567
|
serializeMemory as serializeMemory14
|
|
4845
5568
|
} from "@hiveai/core";
|
|
4846
5569
|
var SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -4853,8 +5576,8 @@ function registerMemorySuggest(memory2) {
|
|
|
4853
5576
|
memory2.command("suggest").description(
|
|
4854
5577
|
"Suggest memories to create based on recurring search queries in the usage log.\n\n Use --auto-save to draft the top-N suggestions as draft memories. They land\n in personal scope by default with status=draft, ready for you to edit and promote."
|
|
4855
5578
|
).option("--since <window>", "ISO date or relative (e.g. '7d', '24h')", "30d").option("--min <count>", "minimum repeat count to surface a query", "2").option("--top-n <n>", "with --auto-save, draft this many top suggestions", "3").option("--scope <scope>", "with --auto-save, scope of drafted memories (personal | team)", "personal").option("--auto-save", "draft top-N suggestions as draft memories on disk", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
4856
|
-
const root =
|
|
4857
|
-
const paths =
|
|
5579
|
+
const root = findProjectRoot35(opts.dir);
|
|
5580
|
+
const paths = resolveHaivePaths32(root);
|
|
4858
5581
|
const events = await readUsageEvents2(paths);
|
|
4859
5582
|
if (events.length === 0) {
|
|
4860
5583
|
if (opts.json) {
|
|
@@ -4900,7 +5623,7 @@ function registerMemorySuggest(memory2) {
|
|
|
4900
5623
|
}
|
|
4901
5624
|
const created = [];
|
|
4902
5625
|
const skipped = [];
|
|
4903
|
-
const existing =
|
|
5626
|
+
const existing = existsSync35(paths.memoriesDir) ? await loadMemoriesFromDir8(paths.memoriesDir) : [];
|
|
4904
5627
|
for (const s of top) {
|
|
4905
5628
|
const slug = slugify(s.query);
|
|
4906
5629
|
if (!slug) {
|
|
@@ -4923,13 +5646,13 @@ function registerMemorySuggest(memory2) {
|
|
|
4923
5646
|
fm.status = "draft";
|
|
4924
5647
|
const body = renderTemplate(s);
|
|
4925
5648
|
const file = memoryFilePath6(paths, fm.scope, fm.id, fm.module);
|
|
4926
|
-
await
|
|
4927
|
-
if (
|
|
4928
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
5649
|
+
await mkdir14(path34.dirname(file), { recursive: true });
|
|
5650
|
+
if (existsSync35(file)) {
|
|
5651
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path34.relative(root, file)}` });
|
|
4929
5652
|
continue;
|
|
4930
5653
|
}
|
|
4931
|
-
await
|
|
4932
|
-
created.push({ id: fm.id, file:
|
|
5654
|
+
await writeFile19(file, serializeMemory14({ frontmatter: fm, body }), "utf8");
|
|
5655
|
+
created.push({ id: fm.id, file: path34.relative(root, file), query: s.query });
|
|
4933
5656
|
}
|
|
4934
5657
|
if (opts.json) {
|
|
4935
5658
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -4937,10 +5660,10 @@ function registerMemorySuggest(memory2) {
|
|
|
4937
5660
|
}
|
|
4938
5661
|
for (const c of created) {
|
|
4939
5662
|
ui.success(`Drafted ${c.id} \u2192 ${c.file}`);
|
|
4940
|
-
console.log(` ${ui.dim("from query:")} ${
|
|
5663
|
+
console.log(` ${ui.dim("from query:")} ${truncate2(c.query, 60)}`);
|
|
4941
5664
|
}
|
|
4942
5665
|
for (const s of skipped) {
|
|
4943
|
-
ui.warn(`Skipped: ${
|
|
5666
|
+
ui.warn(`Skipped: ${truncate2(s.query, 50)} \u2014 ${s.reason}`);
|
|
4944
5667
|
}
|
|
4945
5668
|
if (created.length > 0) {
|
|
4946
5669
|
console.log();
|
|
@@ -4964,7 +5687,7 @@ function registerMemorySuggest(memory2) {
|
|
|
4964
5687
|
}
|
|
4965
5688
|
for (const s of suggestions.slice(0, 30)) {
|
|
4966
5689
|
console.log(
|
|
4967
|
-
` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${
|
|
5690
|
+
` ${ui.bold(`\xD7${s.count}`)} ${ui.dim(`[${s.tools.join(",")}]`)} ${truncate2(s.query, 70)}`
|
|
4968
5691
|
);
|
|
4969
5692
|
console.log(` ${ui.dim("\u2192")} ${s.reason}`);
|
|
4970
5693
|
}
|
|
@@ -5010,28 +5733,28 @@ function renderTemplate(s) {
|
|
|
5010
5733
|
`- **Why** \u2014 the rationale or root cause`,
|
|
5011
5734
|
`- **How to apply** \u2014 what an agent should do when this comes up again`,
|
|
5012
5735
|
``,
|
|
5013
|
-
`Then run \`haive memory promote ${
|
|
5736
|
+
`Then run \`haive memory promote ${truncate2(s.query, 30)}\` to mark it validated.`
|
|
5014
5737
|
].join("\n");
|
|
5015
5738
|
}
|
|
5016
5739
|
function slugify(s) {
|
|
5017
5740
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
5018
5741
|
}
|
|
5019
|
-
function
|
|
5742
|
+
function truncate2(text, max) {
|
|
5020
5743
|
if (text.length <= max) return text;
|
|
5021
5744
|
return text.slice(0, max - 1) + "\u2026";
|
|
5022
5745
|
}
|
|
5023
5746
|
|
|
5024
5747
|
// src/commands/memory-archive.ts
|
|
5025
|
-
import { existsSync as
|
|
5026
|
-
import { writeFile as
|
|
5027
|
-
import
|
|
5748
|
+
import { existsSync as existsSync36 } from "fs";
|
|
5749
|
+
import { writeFile as writeFile20 } from "fs/promises";
|
|
5750
|
+
import path35 from "path";
|
|
5028
5751
|
import "commander";
|
|
5029
5752
|
import {
|
|
5030
|
-
findProjectRoot as
|
|
5753
|
+
findProjectRoot as findProjectRoot36,
|
|
5031
5754
|
getUsage as getUsage9,
|
|
5032
5755
|
loadMemoriesFromDir as loadMemoriesFromDir9,
|
|
5033
|
-
loadUsageIndex as
|
|
5034
|
-
resolveHaivePaths as
|
|
5756
|
+
loadUsageIndex as loadUsageIndex13,
|
|
5757
|
+
resolveHaivePaths as resolveHaivePaths33,
|
|
5035
5758
|
serializeMemory as serializeMemory15
|
|
5036
5759
|
} from "@hiveai/core";
|
|
5037
5760
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
@@ -5039,9 +5762,9 @@ function registerMemoryArchive(memory2) {
|
|
|
5039
5762
|
memory2.command("archive").description(
|
|
5040
5763
|
"Archive obsolete memories: marks status='deprecated' for memories not read in N days\n whose anchored paths have all disappeared (or have no anchor at all).\n\n Defaults to a DRY RUN \u2014 pass --apply to actually rewrite files.\n Targets `attempt` memories by default since they age the fastest.\n\n Recover later with `haive memory edit <id>` to set status back to validated."
|
|
5041
5764
|
).option("--since <window>", "minimum age since last read (e.g. '180d', '6m')", "180d").option("--type <type>", "limit to a memory type (default 'attempt'). Pass 'all' to scan all types.", "attempt").option("--apply", "actually rewrite files (default: dry run)", false).option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
5042
|
-
const root =
|
|
5043
|
-
const paths =
|
|
5044
|
-
if (!
|
|
5765
|
+
const root = findProjectRoot36(opts.dir);
|
|
5766
|
+
const paths = resolveHaivePaths33(root);
|
|
5767
|
+
if (!existsSync36(paths.memoriesDir)) {
|
|
5045
5768
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
5046
5769
|
process.exitCode = 1;
|
|
5047
5770
|
return;
|
|
@@ -5054,7 +5777,7 @@ function registerMemoryArchive(memory2) {
|
|
|
5054
5777
|
}
|
|
5055
5778
|
const cutoff = Date.now() - minDays * MS_PER_DAY;
|
|
5056
5779
|
const all = await loadMemoriesFromDir9(paths.memoriesDir);
|
|
5057
|
-
const usage = await
|
|
5780
|
+
const usage = await loadUsageIndex13(paths);
|
|
5058
5781
|
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
5059
5782
|
const candidates = [];
|
|
5060
5783
|
for (const { memory: mem, filePath } of all) {
|
|
@@ -5062,7 +5785,7 @@ function registerMemoryArchive(memory2) {
|
|
|
5062
5785
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
5063
5786
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
5064
5787
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
5065
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
5788
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync36(path35.join(paths.root, p)));
|
|
5066
5789
|
const isAnchorless = !hasAnyAnchor;
|
|
5067
5790
|
if (!isAnchorless && !allPathsGone) continue;
|
|
5068
5791
|
const u = getUsage9(usage, fm.id);
|
|
@@ -5110,7 +5833,7 @@ function registerMemoryArchive(memory2) {
|
|
|
5110
5833
|
if (!found) continue;
|
|
5111
5834
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
5112
5835
|
try {
|
|
5113
|
-
await
|
|
5836
|
+
await writeFile20(c.filePath, serializeMemory15({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
5114
5837
|
archived++;
|
|
5115
5838
|
} catch (err) {
|
|
5116
5839
|
if (!opts.json) {
|
|
@@ -5136,30 +5859,30 @@ function parseDays(input) {
|
|
|
5136
5859
|
}
|
|
5137
5860
|
|
|
5138
5861
|
// src/commands/doctor.ts
|
|
5139
|
-
import { existsSync as
|
|
5862
|
+
import { existsSync as existsSync37 } from "fs";
|
|
5140
5863
|
import { stat } from "fs/promises";
|
|
5141
5864
|
import "path";
|
|
5142
5865
|
import "commander";
|
|
5143
5866
|
import {
|
|
5144
5867
|
codeMapPath as codeMapPath2,
|
|
5145
|
-
findProjectRoot as
|
|
5868
|
+
findProjectRoot as findProjectRoot37,
|
|
5146
5869
|
getUsage as getUsage10,
|
|
5147
5870
|
loadCodeMap as loadCodeMap3,
|
|
5148
5871
|
loadConfig as loadConfig4,
|
|
5149
5872
|
loadMemoriesFromDir as loadMemoriesFromDir10,
|
|
5150
|
-
loadUsageIndex as
|
|
5873
|
+
loadUsageIndex as loadUsageIndex14,
|
|
5151
5874
|
readUsageEvents as readUsageEvents3,
|
|
5152
|
-
resolveHaivePaths as
|
|
5875
|
+
resolveHaivePaths as resolveHaivePaths34
|
|
5153
5876
|
} from "@hiveai/core";
|
|
5154
5877
|
var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
5155
5878
|
function registerDoctor(program2) {
|
|
5156
5879
|
program2.command("doctor").description(
|
|
5157
5880
|
"Analyze the local hAIve setup and emit actionable recommendations.\n\n Inspects: project-context status, memory health (stale/anchorless/decay/pending),\n code-map freshness, usage log signals (low-hit briefings, repeated empty searches).\n\n Read-only by default. Pass --fix to suggest commands you can copy-paste."
|
|
5158
5881
|
).option("--json", "emit JSON instead of human-readable output", false).option("--fix", "include suggested fix commands in human output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
5159
|
-
const root =
|
|
5160
|
-
const paths =
|
|
5882
|
+
const root = findProjectRoot37(opts.dir);
|
|
5883
|
+
const paths = resolveHaivePaths34(root);
|
|
5161
5884
|
const findings = [];
|
|
5162
|
-
if (!
|
|
5885
|
+
if (!existsSync37(paths.haiveDir)) {
|
|
5163
5886
|
findings.push({
|
|
5164
5887
|
severity: "error",
|
|
5165
5888
|
code: "not-initialized",
|
|
@@ -5168,7 +5891,7 @@ function registerDoctor(program2) {
|
|
|
5168
5891
|
});
|
|
5169
5892
|
return emit(findings, opts);
|
|
5170
5893
|
}
|
|
5171
|
-
if (!
|
|
5894
|
+
if (!existsSync37(paths.projectContext)) {
|
|
5172
5895
|
findings.push({
|
|
5173
5896
|
severity: "warn",
|
|
5174
5897
|
code: "no-project-context",
|
|
@@ -5176,8 +5899,8 @@ function registerDoctor(program2) {
|
|
|
5176
5899
|
fix: "haive init"
|
|
5177
5900
|
});
|
|
5178
5901
|
} else {
|
|
5179
|
-
const { readFile:
|
|
5180
|
-
const content = await
|
|
5902
|
+
const { readFile: readFile15 } = await import("fs/promises");
|
|
5903
|
+
const content = await readFile15(paths.projectContext, "utf8");
|
|
5181
5904
|
const isTemplate = content.includes("TODO \u2014 high-level overview") || content.includes("Generated by `haive init`");
|
|
5182
5905
|
if (isTemplate) {
|
|
5183
5906
|
findings.push({
|
|
@@ -5188,7 +5911,7 @@ function registerDoctor(program2) {
|
|
|
5188
5911
|
});
|
|
5189
5912
|
}
|
|
5190
5913
|
}
|
|
5191
|
-
const memories =
|
|
5914
|
+
const memories = existsSync37(paths.memoriesDir) ? await loadMemoriesFromDir10(paths.memoriesDir) : [];
|
|
5192
5915
|
const now = Date.now();
|
|
5193
5916
|
if (memories.length === 0) {
|
|
5194
5917
|
findings.push({
|
|
@@ -5197,7 +5920,7 @@ function registerDoctor(program2) {
|
|
|
5197
5920
|
message: "No memories yet. Capture knowledge as agents work via mem_save / mem_observe / mem_tried."
|
|
5198
5921
|
});
|
|
5199
5922
|
} else {
|
|
5200
|
-
const usage = await
|
|
5923
|
+
const usage = await loadUsageIndex14(paths);
|
|
5201
5924
|
const stale = memories.filter((m) => m.memory.frontmatter.status === "stale");
|
|
5202
5925
|
if (stale.length > 0) {
|
|
5203
5926
|
findings.push({
|
|
@@ -5342,22 +6065,22 @@ function isSearchTool(name) {
|
|
|
5342
6065
|
}
|
|
5343
6066
|
|
|
5344
6067
|
// src/commands/playback.ts
|
|
5345
|
-
import { existsSync as
|
|
6068
|
+
import { existsSync as existsSync38 } from "fs";
|
|
5346
6069
|
import "commander";
|
|
5347
6070
|
import {
|
|
5348
|
-
findProjectRoot as
|
|
6071
|
+
findProjectRoot as findProjectRoot38,
|
|
5349
6072
|
loadMemoriesFromDir as loadMemoriesFromDir11,
|
|
5350
6073
|
parseSince as parseSince3,
|
|
5351
6074
|
readUsageEvents as readUsageEvents4,
|
|
5352
|
-
resolveHaivePaths as
|
|
6075
|
+
resolveHaivePaths as resolveHaivePaths35
|
|
5353
6076
|
} from "@hiveai/core";
|
|
5354
6077
|
var MS_PER_MINUTE = 6e4;
|
|
5355
6078
|
function registerPlayback(program2) {
|
|
5356
6079
|
program2.command("playback").description(
|
|
5357
6080
|
"Replay past sessions from the usage log. For each session, show:\n - tool calls (what kind, how many)\n - briefing tasks asked\n - memories that have been created since then (that the session didn't have)\n\n Useful to ask 'would today's haive have helped past me on this task?'"
|
|
5358
6081
|
).option("--since <window>", "limit to events in this window (e.g. '7d')", "30d").option("--session-gap <minutes>", "minutes of inactivity that splits a session", "30").option("--limit <n>", "show at most this many sessions (newest first)", "10").option("--json", "emit JSON instead of human-readable output", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
5359
|
-
const root =
|
|
5360
|
-
const paths =
|
|
6082
|
+
const root = findProjectRoot38(opts.dir);
|
|
6083
|
+
const paths = resolveHaivePaths35(root);
|
|
5361
6084
|
const events = await readUsageEvents4(paths);
|
|
5362
6085
|
if (events.length === 0) {
|
|
5363
6086
|
if (opts.json) {
|
|
@@ -5372,7 +6095,7 @@ function registerPlayback(program2) {
|
|
|
5372
6095
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
5373
6096
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
5374
6097
|
const sessions = bucketSessions(filtered, gapMs);
|
|
5375
|
-
const all =
|
|
6098
|
+
const all = existsSync38(paths.memoriesDir) ? await loadMemoriesFromDir11(paths.memoriesDir) : [];
|
|
5376
6099
|
const memByCreatedAt = all.filter(({ memory: memory2 }) => memory2.frontmatter.type !== "session_recap").map(({ memory: memory2 }) => ({ id: memory2.frontmatter.id, at: Date.parse(memory2.frontmatter.created_at) })).sort((a, b) => a.at - b.at);
|
|
5377
6100
|
const enriched = sessions.map((s, i) => {
|
|
5378
6101
|
const startMs = Date.parse(s.start);
|
|
@@ -5412,7 +6135,7 @@ function registerPlayback(program2) {
|
|
|
5412
6135
|
if (s.briefing_tasks.length > 0) {
|
|
5413
6136
|
console.log(` ${ui.dim("briefings asked:")}`);
|
|
5414
6137
|
for (const t of s.briefing_tasks) {
|
|
5415
|
-
console.log(` \u2022 ${
|
|
6138
|
+
console.log(` \u2022 ${truncate3(t, 80)}`);
|
|
5416
6139
|
}
|
|
5417
6140
|
}
|
|
5418
6141
|
if (s.memories_created_since > 0) {
|
|
@@ -5453,7 +6176,7 @@ function countTools(events) {
|
|
|
5453
6176
|
for (const e of events) out[e.tool] = (out[e.tool] ?? 0) + 1;
|
|
5454
6177
|
return out;
|
|
5455
6178
|
}
|
|
5456
|
-
function
|
|
6179
|
+
function truncate3(text, max) {
|
|
5457
6180
|
if (text.length <= max) return text;
|
|
5458
6181
|
return text.slice(0, max - 1) + "\u2026";
|
|
5459
6182
|
}
|
|
@@ -5462,8 +6185,8 @@ function truncate2(text, max) {
|
|
|
5462
6185
|
import { spawn as spawn3 } from "child_process";
|
|
5463
6186
|
import "commander";
|
|
5464
6187
|
import {
|
|
5465
|
-
findProjectRoot as
|
|
5466
|
-
resolveHaivePaths as
|
|
6188
|
+
findProjectRoot as findProjectRoot39,
|
|
6189
|
+
resolveHaivePaths as resolveHaivePaths36
|
|
5467
6190
|
} from "@hiveai/core";
|
|
5468
6191
|
import { preCommitCheck } from "@hiveai/mcp";
|
|
5469
6192
|
function registerPrecommit(program2) {
|
|
@@ -5474,8 +6197,8 @@ function registerPrecommit(program2) {
|
|
|
5474
6197
|
"'any' | 'high-confidence' (default) | 'never' (report only)",
|
|
5475
6198
|
"high-confidence"
|
|
5476
6199
|
).option("--no-semantic", "disable semantic search in anti-patterns matching").option("--json", "emit JSON instead of human-readable output", false).option("--paths <paths...>", "explicit paths to check (skips git diff)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
5477
|
-
const root =
|
|
5478
|
-
const paths =
|
|
6200
|
+
const root = findProjectRoot39(opts.dir);
|
|
6201
|
+
const paths = resolveHaivePaths36(root);
|
|
5479
6202
|
const ctx = { paths };
|
|
5480
6203
|
let diff = "";
|
|
5481
6204
|
let touchedPaths = opts.paths ?? [];
|
|
@@ -5567,8 +6290,8 @@ function runCommand(cmd, args, cwd) {
|
|
|
5567
6290
|
}
|
|
5568
6291
|
|
|
5569
6292
|
// src/index.ts
|
|
5570
|
-
var program = new
|
|
5571
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.
|
|
6293
|
+
var program = new Command40();
|
|
6294
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.0");
|
|
5572
6295
|
registerInit(program);
|
|
5573
6296
|
registerMcp(program);
|
|
5574
6297
|
registerBriefing(program);
|
|
@@ -5576,6 +6299,7 @@ registerTui(program);
|
|
|
5576
6299
|
registerEmbeddings(program);
|
|
5577
6300
|
registerSync(program);
|
|
5578
6301
|
registerInstallHooks(program);
|
|
6302
|
+
registerObserve(program);
|
|
5579
6303
|
registerIndexCode(program);
|
|
5580
6304
|
var memory = program.command("memory").description("Manage memory entries");
|
|
5581
6305
|
registerMemoryAdd(memory);
|