@hiveai/cli 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +377 -51
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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 Command42 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/briefing.ts
|
|
7
7
|
import { existsSync } from "fs";
|
|
@@ -9,6 +9,7 @@ import { readFile } from "fs/promises";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import "commander";
|
|
11
11
|
import {
|
|
12
|
+
extractActionsBriefBody,
|
|
12
13
|
findProjectRoot,
|
|
13
14
|
literalMatchesAllTokens,
|
|
14
15
|
literalMatchesAnyToken,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
loadUsageIndex,
|
|
18
19
|
memoryMatchesAnchorPaths,
|
|
19
20
|
queryCodeMap,
|
|
21
|
+
resolveBriefingBudget,
|
|
20
22
|
resolveHaivePaths,
|
|
21
23
|
tokenizeQuery,
|
|
22
24
|
trackReads
|
|
@@ -195,7 +197,7 @@ async function getHotFiles(root, daysBack, maxHotFiles, filePaths) {
|
|
|
195
197
|
if (!f) continue;
|
|
196
198
|
counts.set(f, (counts.get(f) ?? 0) + 1);
|
|
197
199
|
}
|
|
198
|
-
let entries = [...counts.entries()].map(([
|
|
200
|
+
let entries = [...counts.entries()].map(([path40, changes]) => ({ path: path40, changes }));
|
|
199
201
|
const lowerPaths = filePaths.map((p) => p.toLowerCase());
|
|
200
202
|
if (lowerPaths.length > 0) {
|
|
201
203
|
entries = entries.filter((e) => lowerPaths.some((p) => e.path.toLowerCase().includes(p)));
|
|
@@ -284,8 +286,16 @@ var TokenBudgetWriter = class {
|
|
|
284
286
|
};
|
|
285
287
|
function registerBriefing(program2) {
|
|
286
288
|
program2.command("briefing").description(
|
|
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 --
|
|
289
|
+
'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 --budget quick --task "tiny fix"\n'
|
|
288
290
|
).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(
|
|
291
|
+
"--budget <preset>",
|
|
292
|
+
"align with MCP get_briefing budget_preset: quick | balanced | deep \u2014 sets cap + truncation budget (overrides --max-memories / replaces default open-ended output)",
|
|
293
|
+
void 0
|
|
294
|
+
).option(
|
|
295
|
+
"--memory-format <mode>",
|
|
296
|
+
"printed memory bodies: full (default) | actions (cheap bullet-focused excerpt)",
|
|
297
|
+
"full"
|
|
298
|
+
).option(
|
|
289
299
|
"--scope <scope>",
|
|
290
300
|
"personal | team | shared | all (default: all \u2014 includes team + shared cross-repo memories)",
|
|
291
301
|
"all"
|
|
@@ -297,8 +307,24 @@ function registerBriefing(program2) {
|
|
|
297
307
|
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
298
308
|
const root = findProjectRoot(opts.dir);
|
|
299
309
|
const paths = resolveHaivePaths(root);
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
let budgetPreset = null;
|
|
311
|
+
if (opts.budget) {
|
|
312
|
+
const b = opts.budget.trim().toLowerCase();
|
|
313
|
+
if (b === "quick" || b === "balanced" || b === "deep") budgetPreset = b;
|
|
314
|
+
else ui.warn(`Unknown --budget '${opts.budget}' \u2014 ignoring (use quick|balanced|deep).`);
|
|
315
|
+
}
|
|
316
|
+
let maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
|
|
317
|
+
let budgetTokensCap = opts.maxTokens ? Math.max(100, Number(opts.maxTokens)) : null;
|
|
318
|
+
if (budgetPreset !== null) {
|
|
319
|
+
const presetNums = resolveBriefingBudget(budgetPreset, {
|
|
320
|
+
max_tokens: 8e3,
|
|
321
|
+
max_memories: 8,
|
|
322
|
+
include_module_contexts: true
|
|
323
|
+
});
|
|
324
|
+
budgetTokensCap = presetNums.max_tokens;
|
|
325
|
+
maxMemories = presetNums.max_memories;
|
|
326
|
+
}
|
|
327
|
+
const writer = budgetTokensCap !== null ? new TokenBudgetWriter(budgetTokensCap * CHARS_PER_TOKEN) : null;
|
|
302
328
|
const out = (text) => {
|
|
303
329
|
if (writer) return writer.write(text);
|
|
304
330
|
console.log(text);
|
|
@@ -352,7 +378,6 @@ function registerBriefing(program2) {
|
|
|
352
378
|
const all = ownMemories;
|
|
353
379
|
const filePaths = parseCsv(opts.files);
|
|
354
380
|
const tokens = opts.task ? tokenizeQuery(opts.task) : null;
|
|
355
|
-
const maxMemories = Math.max(1, Number(opts.maxMemories ?? 10));
|
|
356
381
|
const scopeFilter = opts.scope ?? "all";
|
|
357
382
|
const recaps = all.filter(({ memory: mem }) => mem.frontmatter.type === "session_recap").sort(
|
|
358
383
|
(a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
|
|
@@ -454,7 +479,8 @@ function registerBriefing(program2) {
|
|
|
454
479
|
if (anchorSymbols.length > 0) parts.push(`symbols: ${anchorSymbols.join(", ")}`);
|
|
455
480
|
out(ui.dim(` [${parts.join(" \xB7 ")}]`));
|
|
456
481
|
}
|
|
457
|
-
|
|
482
|
+
const memBody = opts.memoryFormat?.toLowerCase() === "actions" ? extractActionsBriefBody(item.memory.body) : item.memory.body.trim();
|
|
483
|
+
out(memBody);
|
|
458
484
|
out("");
|
|
459
485
|
}
|
|
460
486
|
if (!stopped()) out(ui.dim(`${top.length} memor${top.length === 1 ? "y" : "ies"} surfaced`));
|
|
@@ -2715,6 +2741,7 @@ import {
|
|
|
2715
2741
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
2716
2742
|
deriveConfidence as deriveConfidence4,
|
|
2717
2743
|
estimateTokens,
|
|
2744
|
+
extractActionsBriefBody as extractActionsBriefBody2,
|
|
2718
2745
|
getUsage as getUsage5,
|
|
2719
2746
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
2720
2747
|
isAutoPromoteEligible,
|
|
@@ -2727,6 +2754,7 @@ import {
|
|
|
2727
2754
|
loadUsageIndex as loadUsageIndex7,
|
|
2728
2755
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
|
|
2729
2756
|
queryCodeMap as queryCodeMap2,
|
|
2757
|
+
resolveBriefingBudget as resolveBriefingBudget2,
|
|
2730
2758
|
serializeMemory as serializeMemory9,
|
|
2731
2759
|
tokenizeQuery as tokenizeQuery22,
|
|
2732
2760
|
trackReads as trackReads3,
|
|
@@ -2927,6 +2955,30 @@ var MemSaveInputSchema = {
|
|
|
2927
2955
|
function bodyHash(body) {
|
|
2928
2956
|
return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
2929
2957
|
}
|
|
2958
|
+
var WORD_RE = /\b[a-z0-9]{3,}\b/gi;
|
|
2959
|
+
function bodyTokenSet(body) {
|
|
2960
|
+
const raw = body.toLowerCase().match(WORD_RE) ?? [];
|
|
2961
|
+
return new Set(raw);
|
|
2962
|
+
}
|
|
2963
|
+
function maxBodySimilarity(incomingTokens, memories, scope, type, excludeIds) {
|
|
2964
|
+
if (incomingTokens.size < 6) return null;
|
|
2965
|
+
let best = null;
|
|
2966
|
+
const skip = excludeIds ?? /* @__PURE__ */ new Set();
|
|
2967
|
+
for (const { memory: memory2 } of memories) {
|
|
2968
|
+
const fm = memory2.frontmatter;
|
|
2969
|
+
if (skip.has(fm.id)) continue;
|
|
2970
|
+
if (fm.scope !== scope || fm.type !== type) continue;
|
|
2971
|
+
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
2972
|
+
const other = bodyTokenSet(memory2.body);
|
|
2973
|
+
if (other.size === 0) continue;
|
|
2974
|
+
let inter = 0;
|
|
2975
|
+
for (const t of incomingTokens) if (other.has(t)) inter++;
|
|
2976
|
+
const uni = incomingTokens.size + other.size - inter;
|
|
2977
|
+
const j = uni === 0 ? 0 : inter / uni;
|
|
2978
|
+
if (j >= 0.72 && (!best || j > best.score)) best = { score: j, id: fm.id };
|
|
2979
|
+
}
|
|
2980
|
+
return best;
|
|
2981
|
+
}
|
|
2930
2982
|
async function memSave(input, ctx) {
|
|
2931
2983
|
if (!existsSync42(ctx.paths.haiveDir)) {
|
|
2932
2984
|
throw new Error(
|
|
@@ -2948,12 +3000,26 @@ async function memSave(input, ctx) {
|
|
|
2948
3000
|
`Duplicate content detected \u2014 identical body already saved as "${hashDuplicate.memory.frontmatter.id}". Use mem_update to modify it, or change the body to add new information.`
|
|
2949
3001
|
);
|
|
2950
3002
|
}
|
|
3003
|
+
const incomingTokens = bodyTokenSet(input.body);
|
|
3004
|
+
function bodySimilarWarnings(excludeIds) {
|
|
3005
|
+
const dup = maxBodySimilarity(incomingTokens, existing, resolvedScope, input.type, excludeIds);
|
|
3006
|
+
if (!dup?.id) return {};
|
|
3007
|
+
const body_similar = {
|
|
3008
|
+
id: dup.id,
|
|
3009
|
+
score: Math.round(dup.score * 100) / 100
|
|
3010
|
+
};
|
|
3011
|
+
return {
|
|
3012
|
+
similarityWarning: `Body is ~${Math.round(dup.score * 100)}% similar (token overlap) to existing "${dup.id}" \u2014 consolidate if redundant.`,
|
|
3013
|
+
body_similar
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
2951
3016
|
if (input.topic) {
|
|
2952
3017
|
const topicMatch = existing.find(
|
|
2953
3018
|
({ memory: memory2 }) => memory2.frontmatter.topic === input.topic && memory2.frontmatter.scope === resolvedScope && (!input.module || memory2.frontmatter.module === input.module)
|
|
2954
3019
|
);
|
|
2955
3020
|
if (topicMatch) {
|
|
2956
3021
|
const fm = topicMatch.memory.frontmatter;
|
|
3022
|
+
const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
|
|
2957
3023
|
const newFrontmatter = {
|
|
2958
3024
|
...fm,
|
|
2959
3025
|
body: input.body,
|
|
@@ -2970,13 +3036,19 @@ async function memSave(input, ctx) {
|
|
|
2970
3036
|
serializeMemory2({ frontmatter: newFrontmatter, body: input.body }),
|
|
2971
3037
|
"utf8"
|
|
2972
3038
|
);
|
|
3039
|
+
const mergedTw = [
|
|
3040
|
+
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
|
|
3041
|
+
simW ?? null
|
|
3042
|
+
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
2973
3043
|
return {
|
|
2974
3044
|
id: fm.id,
|
|
2975
3045
|
scope: fm.scope,
|
|
2976
3046
|
file_path: topicMatch.filePath,
|
|
2977
3047
|
action: "updated",
|
|
2978
3048
|
revision_count: newFrontmatter.revision_count,
|
|
2979
|
-
...
|
|
3049
|
+
...mergedTw ? { warning: mergedTw } : {},
|
|
3050
|
+
...bs ? { body_similar: bs } : {},
|
|
3051
|
+
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
2980
3052
|
};
|
|
2981
3053
|
}
|
|
2982
3054
|
}
|
|
@@ -3018,9 +3090,11 @@ async function memSave(input, ctx) {
|
|
|
3018
3090
|
}
|
|
3019
3091
|
}
|
|
3020
3092
|
await writeFile22(file, serializeMemory2({ frontmatter, body: input.body }), "utf8");
|
|
3093
|
+
const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
|
|
3021
3094
|
const finalWarning = [
|
|
3022
3095
|
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
|
|
3023
|
-
warning ?? null
|
|
3096
|
+
warning ?? null,
|
|
3097
|
+
simWarnNew ?? null
|
|
3024
3098
|
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3025
3099
|
return {
|
|
3026
3100
|
id: frontmatter.id,
|
|
@@ -3029,6 +3103,7 @@ async function memSave(input, ctx) {
|
|
|
3029
3103
|
action: "created",
|
|
3030
3104
|
...finalWarning ? { warning: finalWarning } : {},
|
|
3031
3105
|
...similar_found ? { similar_found } : {},
|
|
3106
|
+
...bsNew ? { body_similar: bsNew } : {},
|
|
3032
3107
|
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
3033
3108
|
};
|
|
3034
3109
|
}
|
|
@@ -3930,17 +4005,29 @@ var GetBriefingInputSchema = {
|
|
|
3930
4005
|
),
|
|
3931
4006
|
include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
|
|
3932
4007
|
track: z17.boolean().default(true).describe("Increment read_count on returned memories"),
|
|
3933
|
-
format: z17.enum(["full", "compact"]).default("full").describe(
|
|
3934
|
-
"Output format: 'full' returns
|
|
4008
|
+
format: z17.enum(["full", "compact", "actions"]).default("full").describe(
|
|
4009
|
+
"Output format: 'full' returns memory bodies (honors token budget via truncation); 'compact' returns a 1-line summary per memory (call mem_get for detail); 'actions' squeezes bodies to actionable bullet lines \u2014 fewer tokens vs full."
|
|
3935
4010
|
),
|
|
3936
4011
|
symbols: z17.array(z17.string()).default([]).describe(
|
|
3937
4012
|
"Symbol names to look up in the code-map (e.g. ['PaymentService', 'TenantFilter']). Returns the file(s) exporting each symbol so agents don't need to grep. Requires `haive index code` to have been run."
|
|
3938
4013
|
),
|
|
3939
4014
|
min_semantic_score: z17.number().min(0).max(1).default(0).describe(
|
|
3940
4015
|
"Drop semantic-only memory hits whose cosine score is below this threshold. Useful to avoid weakly-related noise when the task is short or the corpus is broad. Has no effect on memories matched via anchor/module/literal \u2014 those are always kept. Try 0.25\u20130.4 for stricter matching."
|
|
4016
|
+
),
|
|
4017
|
+
budget_preset: z17.enum(["quick", "balanced", "deep"]).optional().describe(
|
|
4018
|
+
"Shortcut token budget: 'quick' minimizes tokens/skip module CONTEXT slices; 'balanced' mirrors historical defaults; 'deep' uses a larger briefing. When set, overrides max_tokens, max_memories, and include_module_contexts."
|
|
3941
4019
|
)
|
|
3942
4020
|
};
|
|
4021
|
+
var GetBriefingZod = z17.object(GetBriefingInputSchema);
|
|
3943
4022
|
async function getBriefing(input, ctx) {
|
|
4023
|
+
const resolvedBudget = resolveBriefingBudget2(input.budget_preset, {
|
|
4024
|
+
max_tokens: input.max_tokens,
|
|
4025
|
+
max_memories: input.max_memories,
|
|
4026
|
+
include_module_contexts: input.include_module_contexts
|
|
4027
|
+
});
|
|
4028
|
+
const briefingMaxTokens = resolvedBudget.max_tokens;
|
|
4029
|
+
const briefingMaxMemories = resolvedBudget.max_memories;
|
|
4030
|
+
const briefingIncludeModules = resolvedBudget.include_module_contexts;
|
|
3944
4031
|
const inferred = inferModulesFromPaths2(input.files);
|
|
3945
4032
|
const memories = [];
|
|
3946
4033
|
let searchMode = "literal";
|
|
@@ -4052,8 +4139,8 @@ async function getBriefing(input, ctx) {
|
|
|
4052
4139
|
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
4053
4140
|
return sb - sa;
|
|
4054
4141
|
});
|
|
4055
|
-
for (const mem of ranked.slice(0,
|
|
4056
|
-
if (seen.size >=
|
|
4142
|
+
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
4143
|
+
if (seen.size >= briefingMaxMemories * 2) break;
|
|
4057
4144
|
const loaded = byId.get(mem.id);
|
|
4058
4145
|
if (!loaded) continue;
|
|
4059
4146
|
for (const relId of loaded.memory.frontmatter.related_ids ?? []) {
|
|
@@ -4062,7 +4149,7 @@ async function getBriefing(input, ctx) {
|
|
|
4062
4149
|
if (related) addOrUpdate(related, "anchor", void 0, "partial");
|
|
4063
4150
|
}
|
|
4064
4151
|
}
|
|
4065
|
-
memories.push(...ranked.slice(0,
|
|
4152
|
+
memories.push(...ranked.slice(0, briefingMaxMemories));
|
|
4066
4153
|
if (input.track && memories.length > 0) {
|
|
4067
4154
|
await trackReads3(ctx.paths, memories.map((m) => m.id));
|
|
4068
4155
|
const freshUsage = await loadUsageIndex7(ctx.paths);
|
|
@@ -4142,7 +4229,7 @@ async function getBriefing(input, ctx) {
|
|
|
4142
4229
|
}
|
|
4143
4230
|
}
|
|
4144
4231
|
}
|
|
4145
|
-
const moduleContents =
|
|
4232
|
+
const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
|
|
4146
4233
|
const memoriesText = memories.map((m) => {
|
|
4147
4234
|
const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
|
|
4148
4235
|
return `### ${m.id} (${m.scope}/${m.type}, ${m.confidence})${unverified}
|
|
@@ -4160,7 +4247,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4160
4247
|
},
|
|
4161
4248
|
{ key: "memories", text: memoriesText, weight: 4, mode: "head" }
|
|
4162
4249
|
],
|
|
4163
|
-
|
|
4250
|
+
briefingMaxTokens
|
|
4164
4251
|
);
|
|
4165
4252
|
const projectSlice = slices.find((s) => s.key === "project");
|
|
4166
4253
|
const modulesSlice = slices.find((s) => s.key === "modules");
|
|
@@ -4202,7 +4289,10 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4202
4289
|
const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4203
4290
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
4204
4291
|
}
|
|
4205
|
-
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories
|
|
4292
|
+
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
|
|
4293
|
+
...m,
|
|
4294
|
+
body: extractActionsBriefBody2(m.body)
|
|
4295
|
+
})) : trimmedMemories;
|
|
4206
4296
|
let symbolLocations;
|
|
4207
4297
|
const symbolsToLookup = new Set(input.symbols);
|
|
4208
4298
|
for (const m of outputMemories) {
|
|
@@ -4324,6 +4414,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
4324
4414
|
"After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
|
|
4325
4415
|
);
|
|
4326
4416
|
}
|
|
4417
|
+
if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
|
|
4418
|
+
hints.push(
|
|
4419
|
+
"For tighter token budgets on small tasks pass budget_preset:'quick'; for refactor-sized work use budget_preset:'deep'."
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4327
4422
|
}
|
|
4328
4423
|
return {
|
|
4329
4424
|
...input.task ? { task: input.task } : {},
|
|
@@ -4346,7 +4441,8 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
4346
4441
|
...hints.length > 0 ? { hints } : {},
|
|
4347
4442
|
estimated_tokens: totalTokens,
|
|
4348
4443
|
budget: {
|
|
4349
|
-
max_tokens:
|
|
4444
|
+
max_tokens: briefingMaxTokens,
|
|
4445
|
+
...input.budget_preset ? { preset_applied: input.budget_preset } : {},
|
|
4350
4446
|
spent: {
|
|
4351
4447
|
project: projectSlice.estimatedTokens,
|
|
4352
4448
|
modules: modulesSlice.estimatedTokens,
|
|
@@ -4542,7 +4638,7 @@ var MemRelevantToInputSchema = {
|
|
|
4542
4638
|
files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
4543
4639
|
limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
4544
4640
|
min_semantic_score: z21.number().min(0).max(1).default(0.25).describe("Drop weakly-related semantic hits below this cosine threshold."),
|
|
4545
|
-
format: z21.enum(["full", "compact"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies.")
|
|
4641
|
+
format: z21.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
|
|
4546
4642
|
};
|
|
4547
4643
|
async function memRelevantTo(input, ctx) {
|
|
4548
4644
|
const briefingInput = {
|
|
@@ -5711,7 +5807,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
5711
5807
|
};
|
|
5712
5808
|
}
|
|
5713
5809
|
var SERVER_NAME = "haive";
|
|
5714
|
-
var SERVER_VERSION = "0.9.
|
|
5810
|
+
var SERVER_VERSION = "0.9.3";
|
|
5715
5811
|
function jsonResult(data) {
|
|
5716
5812
|
return {
|
|
5717
5813
|
content: [
|
|
@@ -5877,7 +5973,8 @@ function createHaiveServer(options = {}) {
|
|
|
5877
5973
|
" task \u2014 what you are about to do (1\u20132 sentences) \u2014 ALWAYS provide this",
|
|
5878
5974
|
" files \u2014 files you are about to edit \u2014 surfaces anchored memories",
|
|
5879
5975
|
" symbols \u2014 symbol names to look up in the code-map (e.g. ['PaymentService'])",
|
|
5880
|
-
" format \u2014 'full' (default) | 'compact' (1-line
|
|
5976
|
+
" format \u2014 'full' (default) | 'compact' (1-line) | 'actions' (bullet-first excerpts)",
|
|
5977
|
+
" budget_preset \u2014 'quick' | 'balanced' | 'deep' \u2014 scales max_tokens/memories/module contexts",
|
|
5881
5978
|
"",
|
|
5882
5979
|
"EXAMPLE USAGE:",
|
|
5883
5980
|
" get_briefing({ task: 'add a Stripe payment integration', files: ['src/payments/'], symbols: ['PaymentService'] })",
|
|
@@ -6183,6 +6280,7 @@ function createHaiveServer(options = {}) {
|
|
|
6183
6280
|
" files \u2014 files you'll edit (surfaces anchored memories)",
|
|
6184
6281
|
" limit \u2014 cap on returned memories (default 8)",
|
|
6185
6282
|
" min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
|
|
6283
|
+
" format \u2014 'full' | 'compact' | 'actions' (inherits get_briefing memory framing)",
|
|
6186
6284
|
"",
|
|
6187
6285
|
"RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
|
|
6188
6286
|
].join("\n"),
|
|
@@ -8703,6 +8801,7 @@ function registerSessionEnd(session2) {
|
|
|
8703
8801
|
if (!opts.quiet) {
|
|
8704
8802
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
8705
8803
|
ui.info(`id=${fm.id} file=${path33.relative(root, topicMatch.filePath)}`);
|
|
8804
|
+
ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
|
|
8706
8805
|
}
|
|
8707
8806
|
return;
|
|
8708
8807
|
}
|
|
@@ -8725,6 +8824,7 @@ function registerSessionEnd(session2) {
|
|
|
8725
8824
|
ui.success(`Session recap created`);
|
|
8726
8825
|
ui.info(`id=${frontmatter.id} scope=${scope} file=${path33.relative(root, file)}`);
|
|
8727
8826
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
8827
|
+
ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
|
|
8728
8828
|
}
|
|
8729
8829
|
});
|
|
8730
8830
|
}
|
|
@@ -9156,9 +9256,13 @@ Next steps:
|
|
|
9156
9256
|
|
|
9157
9257
|
// src/commands/stats.ts
|
|
9158
9258
|
import "commander";
|
|
9259
|
+
import { existsSync as existsSync54 } from "fs";
|
|
9260
|
+
import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
|
|
9261
|
+
import path36 from "path";
|
|
9159
9262
|
import {
|
|
9160
9263
|
aggregateUsage,
|
|
9161
9264
|
findProjectRoot as findProjectRoot34,
|
|
9265
|
+
loadMemoriesFromDir as loadMemoriesFromDir28,
|
|
9162
9266
|
loadUsageIndex as loadUsageIndex23,
|
|
9163
9267
|
parseSince,
|
|
9164
9268
|
readUsageEvents as readUsageEvents2,
|
|
@@ -9166,9 +9270,17 @@ import {
|
|
|
9166
9270
|
usageLogSize
|
|
9167
9271
|
} from "@hiveai/core";
|
|
9168
9272
|
function registerStats(program2) {
|
|
9169
|
-
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(
|
|
9273
|
+
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(
|
|
9274
|
+
"--export-report <path>",
|
|
9275
|
+
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
9276
|
+
void 0
|
|
9277
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9170
9278
|
const root = findProjectRoot34(opts.dir);
|
|
9171
9279
|
const paths = resolveHaivePaths31(root);
|
|
9280
|
+
if (opts.exportReport) {
|
|
9281
|
+
await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
|
|
9282
|
+
return;
|
|
9283
|
+
}
|
|
9172
9284
|
if (opts.memoryHits) {
|
|
9173
9285
|
await renderMemoryHits(paths, opts);
|
|
9174
9286
|
return;
|
|
@@ -9217,6 +9329,57 @@ function registerStats(program2) {
|
|
|
9217
9329
|
}
|
|
9218
9330
|
});
|
|
9219
9331
|
}
|
|
9332
|
+
async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
9333
|
+
const outAbs = path36.isAbsolute(outRelative) ? path36.resolve(outRelative) : path36.resolve(root, outRelative);
|
|
9334
|
+
const size = await usageLogSize(paths);
|
|
9335
|
+
let events = await readUsageEvents2(paths);
|
|
9336
|
+
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
9337
|
+
if (existsSync54(paths.memoriesDir)) {
|
|
9338
|
+
const mems = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9339
|
+
for (const { memory: memory2 } of mems) {
|
|
9340
|
+
const fm = memory2.frontmatter;
|
|
9341
|
+
if (fm.type === "session_recap") memoryCount.total_skipped_session++;
|
|
9342
|
+
else if (fm.scope === "team") memoryCount.team++;
|
|
9343
|
+
else if (fm.scope === "personal") memoryCount.personal++;
|
|
9344
|
+
}
|
|
9345
|
+
}
|
|
9346
|
+
const sinceDt = parseSince(sinceRaw) ?? void 0;
|
|
9347
|
+
const aggregate = aggregateUsage(events, sinceDt);
|
|
9348
|
+
const inWindow = (at) => sinceDt === void 0 || Date.parse(at) >= sinceDt.getTime();
|
|
9349
|
+
const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
|
|
9350
|
+
let memoryHitsLeader = null;
|
|
9351
|
+
try {
|
|
9352
|
+
const usageIdx = await loadUsageIndex23(paths);
|
|
9353
|
+
const tops = Object.entries(usageIdx.by_id).map(([id, v]) => ({ id, read_count: v.read_count })).filter((x) => x.read_count > 0).sort((a, b) => b.read_count - a.read_count);
|
|
9354
|
+
memoryHitsLeader = tops[0] ?? null;
|
|
9355
|
+
} catch {
|
|
9356
|
+
memoryHitsLeader = null;
|
|
9357
|
+
}
|
|
9358
|
+
const roiHints = [
|
|
9359
|
+
"Correlate get_briefing calls with skipped multi-file exploration \u2014 proxies available via `pnpm benchmark:roi` at repo root.",
|
|
9360
|
+
"Prefer get_briefing(format:'actions') or budget_preset:'quick' for low-risk edits to reduce token pressure.",
|
|
9361
|
+
"Run `haive memory lint` in CI to keep the corpus actionable.",
|
|
9362
|
+
"Install the haive VS Code extension (packages/vscode) for always-on memory surfacing beside the editor."
|
|
9363
|
+
];
|
|
9364
|
+
if (!size.exists || events.length === 0) {
|
|
9365
|
+
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
9366
|
+
events = [];
|
|
9367
|
+
}
|
|
9368
|
+
await mkdir15(path36.dirname(outAbs), { recursive: true });
|
|
9369
|
+
const payload = {
|
|
9370
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9371
|
+
project_root: root,
|
|
9372
|
+
window_since: sinceRaw,
|
|
9373
|
+
usage_log_meta: size,
|
|
9374
|
+
memory_inventory: memoryCount,
|
|
9375
|
+
aggregate,
|
|
9376
|
+
get_briefing_calls_in_window: briefingCalls,
|
|
9377
|
+
top_memory_reads: memoryHitsLeader,
|
|
9378
|
+
roi_hints: roiHints
|
|
9379
|
+
};
|
|
9380
|
+
await writeFile27(outAbs, JSON.stringify(payload, null, 2), "utf8");
|
|
9381
|
+
ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
|
|
9382
|
+
}
|
|
9220
9383
|
async function renderMemoryHits(paths, opts) {
|
|
9221
9384
|
const index = await loadUsageIndex23(paths);
|
|
9222
9385
|
const since = parseSince(opts.since ?? "30d");
|
|
@@ -9391,15 +9554,15 @@ function summarize(name, t0, payload, notes) {
|
|
|
9391
9554
|
}
|
|
9392
9555
|
|
|
9393
9556
|
// src/commands/memory-suggest.ts
|
|
9394
|
-
import { mkdir as
|
|
9395
|
-
import { existsSync as
|
|
9396
|
-
import
|
|
9557
|
+
import { mkdir as mkdir16, writeFile as writeFile28 } from "fs/promises";
|
|
9558
|
+
import { existsSync as existsSync55 } from "fs";
|
|
9559
|
+
import path37 from "path";
|
|
9397
9560
|
import "commander";
|
|
9398
9561
|
import {
|
|
9399
9562
|
aggregateUsage as aggregateUsage2,
|
|
9400
9563
|
buildFrontmatter as buildFrontmatter11,
|
|
9401
9564
|
findProjectRoot as findProjectRoot36,
|
|
9402
|
-
loadMemoriesFromDir as
|
|
9565
|
+
loadMemoriesFromDir as loadMemoriesFromDir29,
|
|
9403
9566
|
memoryFilePath as memoryFilePath10,
|
|
9404
9567
|
parseSince as parseSince2,
|
|
9405
9568
|
readUsageEvents as readUsageEvents3,
|
|
@@ -9463,7 +9626,7 @@ function registerMemorySuggest(memory2) {
|
|
|
9463
9626
|
}
|
|
9464
9627
|
const created = [];
|
|
9465
9628
|
const skipped = [];
|
|
9466
|
-
const existing =
|
|
9629
|
+
const existing = existsSync55(paths.memoriesDir) ? await loadMemoriesFromDir29(paths.memoriesDir) : [];
|
|
9467
9630
|
for (const s of top) {
|
|
9468
9631
|
const slug = slugify(s.query);
|
|
9469
9632
|
if (!slug) {
|
|
@@ -9486,13 +9649,13 @@ function registerMemorySuggest(memory2) {
|
|
|
9486
9649
|
fm.status = "draft";
|
|
9487
9650
|
const body = renderTemplate(s);
|
|
9488
9651
|
const file = memoryFilePath10(paths, fm.scope, fm.id, fm.module);
|
|
9489
|
-
await
|
|
9490
|
-
if (
|
|
9491
|
-
skipped.push({ query: s.query, reason: `file already exists at ${
|
|
9652
|
+
await mkdir16(path37.dirname(file), { recursive: true });
|
|
9653
|
+
if (existsSync55(file)) {
|
|
9654
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
|
|
9492
9655
|
continue;
|
|
9493
9656
|
}
|
|
9494
|
-
await
|
|
9495
|
-
created.push({ id: fm.id, file:
|
|
9657
|
+
await writeFile28(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
|
|
9658
|
+
created.push({ id: fm.id, file: path37.relative(root, file), query: s.query });
|
|
9496
9659
|
}
|
|
9497
9660
|
if (opts.json) {
|
|
9498
9661
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -9585,14 +9748,14 @@ function truncate2(text, max) {
|
|
|
9585
9748
|
}
|
|
9586
9749
|
|
|
9587
9750
|
// src/commands/memory-archive.ts
|
|
9588
|
-
import { existsSync as
|
|
9589
|
-
import { writeFile as
|
|
9590
|
-
import
|
|
9751
|
+
import { existsSync as existsSync56 } from "fs";
|
|
9752
|
+
import { writeFile as writeFile29 } from "fs/promises";
|
|
9753
|
+
import path38 from "path";
|
|
9591
9754
|
import "commander";
|
|
9592
9755
|
import {
|
|
9593
9756
|
findProjectRoot as findProjectRoot37,
|
|
9594
9757
|
getUsage as getUsage18,
|
|
9595
|
-
loadMemoriesFromDir as
|
|
9758
|
+
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
9596
9759
|
loadUsageIndex as loadUsageIndex24,
|
|
9597
9760
|
resolveHaivePaths as resolveHaivePaths34,
|
|
9598
9761
|
serializeMemory as serializeMemory25
|
|
@@ -9604,7 +9767,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9604
9767
|
).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) => {
|
|
9605
9768
|
const root = findProjectRoot37(opts.dir);
|
|
9606
9769
|
const paths = resolveHaivePaths34(root);
|
|
9607
|
-
if (!
|
|
9770
|
+
if (!existsSync56(paths.memoriesDir)) {
|
|
9608
9771
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
9609
9772
|
process.exitCode = 1;
|
|
9610
9773
|
return;
|
|
@@ -9616,7 +9779,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9616
9779
|
return;
|
|
9617
9780
|
}
|
|
9618
9781
|
const cutoff = Date.now() - minDays * MS_PER_DAY2;
|
|
9619
|
-
const all = await
|
|
9782
|
+
const all = await loadMemoriesFromDir30(paths.memoriesDir);
|
|
9620
9783
|
const usage = await loadUsageIndex24(paths);
|
|
9621
9784
|
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
9622
9785
|
const candidates = [];
|
|
@@ -9625,7 +9788,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9625
9788
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
9626
9789
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
9627
9790
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
9628
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
9791
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync56(path38.join(paths.root, p)));
|
|
9629
9792
|
const isAnchorless = !hasAnyAnchor;
|
|
9630
9793
|
if (!isAnchorless && !allPathsGone) continue;
|
|
9631
9794
|
const u = getUsage18(usage, fm.id);
|
|
@@ -9673,7 +9836,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9673
9836
|
if (!found) continue;
|
|
9674
9837
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
9675
9838
|
try {
|
|
9676
|
-
await
|
|
9839
|
+
await writeFile29(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
9677
9840
|
archived++;
|
|
9678
9841
|
} catch (err) {
|
|
9679
9842
|
if (!opts.json) {
|
|
@@ -9699,7 +9862,7 @@ function parseDays(input) {
|
|
|
9699
9862
|
}
|
|
9700
9863
|
|
|
9701
9864
|
// src/commands/doctor.ts
|
|
9702
|
-
import { existsSync as
|
|
9865
|
+
import { existsSync as existsSync57 } from "fs";
|
|
9703
9866
|
import { stat } from "fs/promises";
|
|
9704
9867
|
import "path";
|
|
9705
9868
|
import { execSync as execSync3 } from "child_process";
|
|
@@ -9710,7 +9873,7 @@ import {
|
|
|
9710
9873
|
getUsage as getUsage19,
|
|
9711
9874
|
loadCodeMap as loadCodeMap5,
|
|
9712
9875
|
loadConfig as loadConfig7,
|
|
9713
|
-
loadMemoriesFromDir as
|
|
9876
|
+
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
9714
9877
|
loadUsageIndex as loadUsageIndex25,
|
|
9715
9878
|
readUsageEvents as readUsageEvents4,
|
|
9716
9879
|
resolveHaivePaths as resolveHaivePaths35
|
|
@@ -9723,7 +9886,7 @@ function registerDoctor(program2) {
|
|
|
9723
9886
|
const root = findProjectRoot38(opts.dir);
|
|
9724
9887
|
const paths = resolveHaivePaths35(root);
|
|
9725
9888
|
const findings = [];
|
|
9726
|
-
if (!
|
|
9889
|
+
if (!existsSync57(paths.haiveDir)) {
|
|
9727
9890
|
findings.push({
|
|
9728
9891
|
severity: "error",
|
|
9729
9892
|
code: "not-initialized",
|
|
@@ -9732,7 +9895,7 @@ function registerDoctor(program2) {
|
|
|
9732
9895
|
});
|
|
9733
9896
|
return emit(findings, opts);
|
|
9734
9897
|
}
|
|
9735
|
-
if (!
|
|
9898
|
+
if (!existsSync57(paths.projectContext)) {
|
|
9736
9899
|
findings.push({
|
|
9737
9900
|
severity: "warn",
|
|
9738
9901
|
code: "no-project-context",
|
|
@@ -9752,7 +9915,7 @@ function registerDoctor(program2) {
|
|
|
9752
9915
|
});
|
|
9753
9916
|
}
|
|
9754
9917
|
}
|
|
9755
|
-
const memories =
|
|
9918
|
+
const memories = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir31(paths.memoriesDir) : [];
|
|
9756
9919
|
const now = Date.now();
|
|
9757
9920
|
if (memories.length === 0) {
|
|
9758
9921
|
findings.push({
|
|
@@ -9876,7 +10039,7 @@ function registerDoctor(program2) {
|
|
|
9876
10039
|
timeout: 3e3,
|
|
9877
10040
|
stdio: ["ignore", "pipe", "ignore"]
|
|
9878
10041
|
}).trim();
|
|
9879
|
-
const cliVersion = "0.9.
|
|
10042
|
+
const cliVersion = "0.9.3";
|
|
9880
10043
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
9881
10044
|
findings.push({
|
|
9882
10045
|
severity: "warn",
|
|
@@ -9925,11 +10088,11 @@ function isSearchTool(name) {
|
|
|
9925
10088
|
}
|
|
9926
10089
|
|
|
9927
10090
|
// src/commands/playback.ts
|
|
9928
|
-
import { existsSync as
|
|
10091
|
+
import { existsSync as existsSync58 } from "fs";
|
|
9929
10092
|
import "commander";
|
|
9930
10093
|
import {
|
|
9931
10094
|
findProjectRoot as findProjectRoot39,
|
|
9932
|
-
loadMemoriesFromDir as
|
|
10095
|
+
loadMemoriesFromDir as loadMemoriesFromDir32,
|
|
9933
10096
|
parseSince as parseSince3,
|
|
9934
10097
|
readUsageEvents as readUsageEvents5,
|
|
9935
10098
|
resolveHaivePaths as resolveHaivePaths36
|
|
@@ -9955,7 +10118,7 @@ function registerPlayback(program2) {
|
|
|
9955
10118
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
9956
10119
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
9957
10120
|
const sessions = bucketSessions(filtered, gapMs);
|
|
9958
|
-
const all =
|
|
10121
|
+
const all = existsSync58(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
|
|
9959
10122
|
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);
|
|
9960
10123
|
const enriched = sessions.map((s, i) => {
|
|
9961
10124
|
const startMs = Date.parse(s.start);
|
|
@@ -10148,10 +10311,172 @@ function runCommand3(cmd, args, cwd) {
|
|
|
10148
10311
|
});
|
|
10149
10312
|
}
|
|
10150
10313
|
|
|
10314
|
+
// src/commands/welcome.ts
|
|
10315
|
+
import { existsSync as existsSync59 } from "fs";
|
|
10316
|
+
import "commander";
|
|
10317
|
+
import {
|
|
10318
|
+
findProjectRoot as findProjectRoot41,
|
|
10319
|
+
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
10320
|
+
resolveHaivePaths as resolveHaivePaths38
|
|
10321
|
+
} from "@hiveai/core";
|
|
10322
|
+
var TYPE_RANK = {
|
|
10323
|
+
decision: 0,
|
|
10324
|
+
architecture: 1,
|
|
10325
|
+
convention: 2,
|
|
10326
|
+
glossary: 3,
|
|
10327
|
+
gotcha: 4,
|
|
10328
|
+
attempt: 5
|
|
10329
|
+
};
|
|
10330
|
+
function registerWelcome(program2) {
|
|
10331
|
+
program2.command("welcome").description(
|
|
10332
|
+
"Onboarding checklist: ranks validated/proposed **team** memories by type.\nUse after `haive init` so new devs skim institutional knowledge quickly.\n\n haive welcome\n haive welcome --limit 15\n"
|
|
10333
|
+
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10334
|
+
const root = findProjectRoot41(opts.dir);
|
|
10335
|
+
const paths = resolveHaivePaths38(root);
|
|
10336
|
+
if (!existsSync59(paths.memoriesDir)) {
|
|
10337
|
+
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
10338
|
+
process.exitCode = 1;
|
|
10339
|
+
return;
|
|
10340
|
+
}
|
|
10341
|
+
const all = await loadMemoriesFromDir33(paths.memoriesDir);
|
|
10342
|
+
const team = all.filter(
|
|
10343
|
+
({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
|
|
10344
|
+
);
|
|
10345
|
+
team.sort((a, b) => {
|
|
10346
|
+
const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
|
|
10347
|
+
const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
|
|
10348
|
+
if (ta !== tb) return ta - tb;
|
|
10349
|
+
const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
10350
|
+
const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
10351
|
+
if (sta !== stb) return sta - stb;
|
|
10352
|
+
return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
|
|
10353
|
+
});
|
|
10354
|
+
const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
|
|
10355
|
+
const pick = team.slice(0, cap);
|
|
10356
|
+
console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
|
|
10357
|
+
console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
|
|
10358
|
+
if (pick.length === 0) {
|
|
10359
|
+
ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
|
|
10360
|
+
return;
|
|
10361
|
+
}
|
|
10362
|
+
let i = 1;
|
|
10363
|
+
for (const { memory: memory2 } of pick) {
|
|
10364
|
+
const fm = memory2.frontmatter;
|
|
10365
|
+
const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
|
|
10366
|
+
const line = head ?? fm.id;
|
|
10367
|
+
console.log(
|
|
10368
|
+
`${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
|
|
10369
|
+
${line}`
|
|
10370
|
+
);
|
|
10371
|
+
i++;
|
|
10372
|
+
}
|
|
10373
|
+
});
|
|
10374
|
+
}
|
|
10375
|
+
|
|
10376
|
+
// src/commands/memory-lint.ts
|
|
10377
|
+
import { existsSync as existsSync60 } from "fs";
|
|
10378
|
+
import "commander";
|
|
10379
|
+
import {
|
|
10380
|
+
findProjectRoot as findProjectRoot42,
|
|
10381
|
+
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
10382
|
+
resolveHaivePaths as resolveHaivePaths39
|
|
10383
|
+
} from "@hiveai/core";
|
|
10384
|
+
async function lintMemoriesAsync(root) {
|
|
10385
|
+
const paths = resolveHaivePaths39(root);
|
|
10386
|
+
const out = [];
|
|
10387
|
+
if (!existsSync60(paths.memoriesDir)) return out;
|
|
10388
|
+
const loaded = await loadMemoriesFromDir34(paths.memoriesDir);
|
|
10389
|
+
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
10390
|
+
for (const { filePath, memory: memory2 } of loaded) {
|
|
10391
|
+
const fm = memory2.frontmatter;
|
|
10392
|
+
if (fm.type === "session_recap") continue;
|
|
10393
|
+
const body = memory2.body.trim();
|
|
10394
|
+
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
10395
|
+
if (naked.length < 40 && fm.status !== "rejected") {
|
|
10396
|
+
out.push({
|
|
10397
|
+
file: filePath,
|
|
10398
|
+
id: fm.id,
|
|
10399
|
+
severity: "warn",
|
|
10400
|
+
code: "SHORT_BODY",
|
|
10401
|
+
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
10402
|
+
});
|
|
10403
|
+
}
|
|
10404
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
10405
|
+
out.push({
|
|
10406
|
+
file: filePath,
|
|
10407
|
+
id: fm.id,
|
|
10408
|
+
severity: "warn",
|
|
10409
|
+
code: "MISSING_ANCHOR",
|
|
10410
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
|
|
10411
|
+
});
|
|
10412
|
+
}
|
|
10413
|
+
if (fm.status === "stale" && !fm.stale_reason) {
|
|
10414
|
+
out.push({
|
|
10415
|
+
file: filePath,
|
|
10416
|
+
id: fm.id,
|
|
10417
|
+
severity: "info",
|
|
10418
|
+
code: "STALE_NO_REASON",
|
|
10419
|
+
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
10420
|
+
});
|
|
10421
|
+
}
|
|
10422
|
+
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
10423
|
+
out.push({
|
|
10424
|
+
file: filePath,
|
|
10425
|
+
id: fm.id,
|
|
10426
|
+
severity: "info",
|
|
10427
|
+
code: "LONG_GLOSSARY",
|
|
10428
|
+
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
10429
|
+
});
|
|
10430
|
+
}
|
|
10431
|
+
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
10432
|
+
if (!hasMarkdownHeading) {
|
|
10433
|
+
out.push({
|
|
10434
|
+
file: filePath,
|
|
10435
|
+
id: fm.id,
|
|
10436
|
+
severity: "warn",
|
|
10437
|
+
code: "NO_MD_HEADING",
|
|
10438
|
+
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
10439
|
+
});
|
|
10440
|
+
}
|
|
10441
|
+
}
|
|
10442
|
+
return out;
|
|
10443
|
+
}
|
|
10444
|
+
function registerMemoryLint(parent) {
|
|
10445
|
+
parent.command("lint").description(
|
|
10446
|
+
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
10447
|
+
).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10448
|
+
const root = findProjectRoot42(opts.dir);
|
|
10449
|
+
const findings = await lintMemoriesAsync(root);
|
|
10450
|
+
if (opts.json) {
|
|
10451
|
+
console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
|
|
10452
|
+
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
10453
|
+
return;
|
|
10454
|
+
}
|
|
10455
|
+
if (findings.length === 0) {
|
|
10456
|
+
ui.success(`memory lint OK \u2014 ${root}`);
|
|
10457
|
+
return;
|
|
10458
|
+
}
|
|
10459
|
+
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
10460
|
+
`);
|
|
10461
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
10462
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
10463
|
+
for (const f of findings) {
|
|
10464
|
+
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
10465
|
+
console.log(
|
|
10466
|
+
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
10467
|
+
);
|
|
10468
|
+
console.log(` ${f.message}`);
|
|
10469
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
10470
|
+
}
|
|
10471
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
10472
|
+
});
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10151
10475
|
// src/index.ts
|
|
10152
|
-
var program = new
|
|
10153
|
-
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.
|
|
10476
|
+
var program = new Command42();
|
|
10477
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.3");
|
|
10154
10478
|
registerInit(program);
|
|
10479
|
+
registerWelcome(program);
|
|
10155
10480
|
registerMcp(program);
|
|
10156
10481
|
registerBriefing(program);
|
|
10157
10482
|
registerTui(program);
|
|
@@ -10183,6 +10508,7 @@ registerMemoryImportChangelog(memory);
|
|
|
10183
10508
|
registerMemoryDigest(memory);
|
|
10184
10509
|
registerMemorySuggest(memory);
|
|
10185
10510
|
registerMemoryArchive(memory);
|
|
10511
|
+
registerMemoryLint(memory);
|
|
10186
10512
|
var session = program.command("session").description("Manage session lifecycle");
|
|
10187
10513
|
registerSessionEnd(session);
|
|
10188
10514
|
registerSnapshot(program);
|