@hiveai/cli 0.9.2 → 0.9.5
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 +769 -165
- 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 Command44 } 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(([path41, changes]) => ({ path: path41, 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`));
|
|
@@ -1856,6 +1882,7 @@ var PROJECT_CONTEXT_TEMPLATE = `# Project context
|
|
|
1856
1882
|
|
|
1857
1883
|
> Generated by \`haive init\`. Run \`haive init --bootstrap\` to auto-fill from your codebase,
|
|
1858
1884
|
> or invoke the MCP prompt \`bootstrap_project\` in your AI client for a richer AI-generated version.
|
|
1885
|
+
> **AI agents**: use the **haive** MCP (\`get_briefing\` / \`mem_relevant_to\`) before substantive edits \u2014 see bridge files and \`.cursor/rules/haive-mcp-required.mdc\`.
|
|
1859
1886
|
|
|
1860
1887
|
## Architecture
|
|
1861
1888
|
TODO \u2014 high-level overview of the codebase.
|
|
@@ -1880,16 +1907,47 @@ Memories live under \`.ai/memories/\` (personal/team/module).
|
|
|
1880
1907
|
|
|
1881
1908
|
## hAIve \u2014 mandatory rules for AI agents
|
|
1882
1909
|
|
|
1910
|
+
Installing or initializing hAIve in this repository means **the team expects AI agents to drive work through hAIve**, not bypass it.
|
|
1911
|
+
|
|
1912
|
+
### MCP first (default \u2014 never optional unless the developer says so)
|
|
1913
|
+
The **haive** MCP server must be enabled in your client. Until it is unavailable (misconfiguration report to the developer), you **must** use it for substantive work:
|
|
1914
|
+
|
|
1915
|
+
- **Before** you edit project source files, configs (\`package.json\`, tooling), or workflows for the current goal, invoke \`get_briefing\` with a short **task** string (and **files** / **symbols** when you know them). If project context was already refreshed in this session and you only need ranked memories, you may call \`mem_relevant_to\` instead\u2014but **still** consult hAIve before trusting ungrounded guesses.
|
|
1916
|
+
- **Before** declaring a coding task finished, invoke the MCP prompt \`post_task\` when the session produced learnings or substantive changes worth capturing.
|
|
1917
|
+
|
|
1918
|
+
Do **not** treat these as nice-to-have: they are **default-required** for coding agents here.
|
|
1919
|
+
|
|
1883
1920
|
### Session rules
|
|
1884
|
-
- **Before** marking a task as done ("Done", "Finished", "C'est fait"), call the MCP prompt \`post_task\`.
|
|
1885
1921
|
- **Immediately** when an approach fails (wrong API, wrong pattern, wrong assumption), call \`mem_tried\` \u2014 do not wait until the end of the session.
|
|
1886
|
-
- When starting a new task, call \`get_briefing\` with the task description to load relevant memories and avoid repeating past mistakes.
|
|
1887
1922
|
|
|
1888
1923
|
### Safety rules \u2014 NEVER violate these
|
|
1889
1924
|
- If \`get_briefing\` returns an \`action_required\` list, **stop and show each item to the developer** before doing anything. Use the exact \`developer_message\` provided. Wait for explicit confirmation.
|
|
1890
1925
|
- **Never modify code autonomously** because of a breaking change detected in another project (dependency version bump, API contract change, removed field). Always ask first.
|
|
1891
1926
|
- When in doubt about a cross-repo change: ask, don't act.
|
|
1892
1927
|
`;
|
|
1928
|
+
var CURSOR_HAIVE_RULE_MDC = `---
|
|
1929
|
+
description: Require hAIve MCP (get_briefing / mem_relevant_to) before substantive repo edits
|
|
1930
|
+
alwaysApply: true
|
|
1931
|
+
---
|
|
1932
|
+
|
|
1933
|
+
# hAIve \u2014 required for agents
|
|
1934
|
+
|
|
1935
|
+
This repository uses **hAIve**. Running \`haive init\` means the team expects agents to **use the haive MCP server by default**, not skip it.
|
|
1936
|
+
|
|
1937
|
+
## Before you change the repo
|
|
1938
|
+
|
|
1939
|
+
1. Call **\`get_briefing\`** with \`task\` (and \`files\` / \`symbols\` when known), **or** \`mem_relevant_to\` if full project context is already loaded and you only need ranked memories.
|
|
1940
|
+
2. Then read/search the codebase as needed. Do not invert this order for non-trivial work.
|
|
1941
|
+
|
|
1942
|
+
## During / after the task
|
|
1943
|
+
|
|
1944
|
+
- On failure: **\`mem_tried\`** immediately.
|
|
1945
|
+
- Before closing a substantive session: MCP prompt **\`post_task\`** when there is something worth capturing.
|
|
1946
|
+
|
|
1947
|
+
## If haive MCP is missing
|
|
1948
|
+
|
|
1949
|
+
Tell the developer to enable the **haive** server (e.g. \`haive mcp --stdio\` in client config) and restart the client. Do not silently ignore hAIve.
|
|
1950
|
+
`;
|
|
1893
1951
|
var CI_WORKFLOW = `name: haive-sync
|
|
1894
1952
|
|
|
1895
1953
|
on:
|
|
@@ -2010,7 +2068,7 @@ jobs:
|
|
|
2010
2068
|
function registerInit(program2) {
|
|
2011
2069
|
program2.command("init").description(
|
|
2012
2070
|
"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."
|
|
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(
|
|
2071
|
+
).option("-d, --dir <dir>", "project root", process.cwd()).option("--no-bridges", "do not generate CLAUDE.md / .cursorrules / copilot-instructions.md / .cursor/rules/haive-mcp-required.mdc").option("--with-ci", "write a GitHub Actions workflow (.github/workflows/haive-sync.yml) \u2014 included automatically in autopilot mode").option(
|
|
2014
2072
|
"--manual",
|
|
2015
2073
|
"opt out of autopilot: memories require manual approval, no auto-session recap, no auto-context"
|
|
2016
2074
|
).option(
|
|
@@ -2040,6 +2098,7 @@ function registerInit(program2) {
|
|
|
2040
2098
|
await mkdir3(paths.teamDir, { recursive: true });
|
|
2041
2099
|
await mkdir3(paths.moduleDir, { recursive: true });
|
|
2042
2100
|
await mkdir3(paths.modulesContextDir, { recursive: true });
|
|
2101
|
+
await ensureAiRuntimeLayout(paths.runtimeDir);
|
|
2043
2102
|
if (!existsSync6(paths.projectContext)) {
|
|
2044
2103
|
if (wantBootstrap) {
|
|
2045
2104
|
ui.info("Bootstrapping project context from local files\u2026");
|
|
@@ -2069,6 +2128,7 @@ function registerInit(program2) {
|
|
|
2069
2128
|
await writeBridge(root, "CLAUDE.md");
|
|
2070
2129
|
await writeBridge(root, ".cursorrules");
|
|
2071
2130
|
await writeBridge(root, path7.join(".github", "copilot-instructions.md"));
|
|
2131
|
+
await writeCursorHaiveRule(root);
|
|
2072
2132
|
}
|
|
2073
2133
|
const stacksToSeed = await resolveStacksToSeed(root, wantStack);
|
|
2074
2134
|
if (stacksToSeed.length > 0) {
|
|
@@ -2211,6 +2271,17 @@ async function resolveStacksToSeed(root, stackOpt) {
|
|
|
2211
2271
|
}
|
|
2212
2272
|
return stackOpt.split(",").map((s) => s.trim().toLowerCase()).filter(isValidStack);
|
|
2213
2273
|
}
|
|
2274
|
+
async function writeCursorHaiveRule(root) {
|
|
2275
|
+
const relPath = ".cursor/rules/haive-mcp-required.mdc";
|
|
2276
|
+
const target = path7.join(root, relPath);
|
|
2277
|
+
if (existsSync6(target)) {
|
|
2278
|
+
ui.info(`Cursor rule ${relPath} already exists \u2014 skipped`);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
await mkdir3(path7.dirname(target), { recursive: true });
|
|
2282
|
+
await writeFile3(target, CURSOR_HAIVE_RULE_MDC, "utf8");
|
|
2283
|
+
ui.success(`Created Cursor rule ${relPath}`);
|
|
2284
|
+
}
|
|
2214
2285
|
async function writeBridge(root, relPath) {
|
|
2215
2286
|
const target = path7.join(root, relPath);
|
|
2216
2287
|
if (existsSync6(target)) {
|
|
@@ -2221,6 +2292,27 @@ async function writeBridge(root, relPath) {
|
|
|
2221
2292
|
await writeFile3(target, BRIDGE_BODY, "utf8");
|
|
2222
2293
|
ui.success(`Created bridge ${relPath}`);
|
|
2223
2294
|
}
|
|
2295
|
+
var RUNTIME_README_BODY = `# .ai/.runtime \u2014 disposable local layer
|
|
2296
|
+
|
|
2297
|
+
Not team truth. Use for machine-local session notes or tooling scratch files.
|
|
2298
|
+
Official memories belong in .ai/memories/ (versioned in Git).
|
|
2299
|
+
Only .gitignore and this README are meant to commit; everything else stays untracked.
|
|
2300
|
+
`;
|
|
2301
|
+
var RUNTIME_GITIGNORE_BODY = `*
|
|
2302
|
+
!.gitignore
|
|
2303
|
+
!README.md
|
|
2304
|
+
`;
|
|
2305
|
+
async function ensureAiRuntimeLayout(runtimeDir) {
|
|
2306
|
+
await mkdir3(runtimeDir, { recursive: true });
|
|
2307
|
+
const gi = path7.join(runtimeDir, ".gitignore");
|
|
2308
|
+
if (!existsSync6(gi)) {
|
|
2309
|
+
await writeFile3(gi, RUNTIME_GITIGNORE_BODY, "utf8");
|
|
2310
|
+
}
|
|
2311
|
+
const readme = path7.join(runtimeDir, "README.md");
|
|
2312
|
+
if (!existsSync6(readme)) {
|
|
2313
|
+
await writeFile3(readme, RUNTIME_README_BODY, "utf8");
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2224
2316
|
async function ensureGitignoreEntries(root, patterns) {
|
|
2225
2317
|
try {
|
|
2226
2318
|
const gitignorePath = path7.join(root, ".gitignore");
|
|
@@ -2605,6 +2697,7 @@ import {
|
|
|
2605
2697
|
loadMemoriesFromDir as loadMemoriesFromDir3,
|
|
2606
2698
|
loadUsageIndex as loadUsageIndex2,
|
|
2607
2699
|
pickSnippetNeedle,
|
|
2700
|
+
rankMemoriesLexical,
|
|
2608
2701
|
tokenizeQuery as tokenizeQuery2,
|
|
2609
2702
|
trackReads as trackReads2
|
|
2610
2703
|
} from "@hiveai/core";
|
|
@@ -2715,6 +2808,7 @@ import {
|
|
|
2715
2808
|
DEFAULT_AUTO_PROMOTE_RULE,
|
|
2716
2809
|
deriveConfidence as deriveConfidence4,
|
|
2717
2810
|
estimateTokens,
|
|
2811
|
+
extractActionsBriefBody as extractActionsBriefBody2,
|
|
2718
2812
|
getUsage as getUsage5,
|
|
2719
2813
|
inferModulesFromPaths as inferModulesFromPaths2,
|
|
2720
2814
|
isAutoPromoteEligible,
|
|
@@ -2727,6 +2821,7 @@ import {
|
|
|
2727
2821
|
loadUsageIndex as loadUsageIndex7,
|
|
2728
2822
|
memoryMatchesAnchorPaths as memoryMatchesAnchorPaths22,
|
|
2729
2823
|
queryCodeMap as queryCodeMap2,
|
|
2824
|
+
resolveBriefingBudget as resolveBriefingBudget2,
|
|
2730
2825
|
serializeMemory as serializeMemory9,
|
|
2731
2826
|
tokenizeQuery as tokenizeQuery22,
|
|
2732
2827
|
trackReads as trackReads3,
|
|
@@ -2804,9 +2899,19 @@ import {
|
|
|
2804
2899
|
serializeMemory as serializeMemory10
|
|
2805
2900
|
} from "@hiveai/core";
|
|
2806
2901
|
import { z as z29 } from "zod";
|
|
2902
|
+
import { existsSync as existsSync27 } from "fs";
|
|
2903
|
+
import { findLexicalConflictPairs, loadMemoriesFromDir as loadMemoriesFromDir21 } from "@hiveai/core";
|
|
2807
2904
|
import { z as z30 } from "zod";
|
|
2905
|
+
import { resolveProjectInfo } from "@hiveai/core";
|
|
2808
2906
|
import { z as z31 } from "zod";
|
|
2907
|
+
import { MemoryTypeSchema, suggestTopicKey } from "@hiveai/core";
|
|
2809
2908
|
import { z as z32 } from "zod";
|
|
2909
|
+
import { existsSync as existsSync28 } from "fs";
|
|
2910
|
+
import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir222 } from "@hiveai/core";
|
|
2911
|
+
import { z as z33 } from "zod";
|
|
2912
|
+
import { z as z34 } from "zod";
|
|
2913
|
+
import { z as z35 } from "zod";
|
|
2914
|
+
import { z as z36 } from "zod";
|
|
2810
2915
|
function createContext(options = {}) {
|
|
2811
2916
|
const env = options.env ?? process.env;
|
|
2812
2917
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -2927,6 +3032,30 @@ var MemSaveInputSchema = {
|
|
|
2927
3032
|
function bodyHash(body) {
|
|
2928
3033
|
return createHash("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
2929
3034
|
}
|
|
3035
|
+
var WORD_RE = /\b[a-z0-9]{3,}\b/gi;
|
|
3036
|
+
function bodyTokenSet(body) {
|
|
3037
|
+
const raw = body.toLowerCase().match(WORD_RE) ?? [];
|
|
3038
|
+
return new Set(raw);
|
|
3039
|
+
}
|
|
3040
|
+
function maxBodySimilarity(incomingTokens, memories, scope, type, excludeIds) {
|
|
3041
|
+
if (incomingTokens.size < 6) return null;
|
|
3042
|
+
let best = null;
|
|
3043
|
+
const skip = excludeIds ?? /* @__PURE__ */ new Set();
|
|
3044
|
+
for (const { memory: memory2 } of memories) {
|
|
3045
|
+
const fm = memory2.frontmatter;
|
|
3046
|
+
if (skip.has(fm.id)) continue;
|
|
3047
|
+
if (fm.scope !== scope || fm.type !== type) continue;
|
|
3048
|
+
if (fm.status === "rejected" || fm.status === "deprecated") continue;
|
|
3049
|
+
const other = bodyTokenSet(memory2.body);
|
|
3050
|
+
if (other.size === 0) continue;
|
|
3051
|
+
let inter = 0;
|
|
3052
|
+
for (const t of incomingTokens) if (other.has(t)) inter++;
|
|
3053
|
+
const uni = incomingTokens.size + other.size - inter;
|
|
3054
|
+
const j = uni === 0 ? 0 : inter / uni;
|
|
3055
|
+
if (j >= 0.72 && (!best || j > best.score)) best = { score: j, id: fm.id };
|
|
3056
|
+
}
|
|
3057
|
+
return best;
|
|
3058
|
+
}
|
|
2930
3059
|
async function memSave(input, ctx) {
|
|
2931
3060
|
if (!existsSync42(ctx.paths.haiveDir)) {
|
|
2932
3061
|
throw new Error(
|
|
@@ -2948,12 +3077,26 @@ async function memSave(input, ctx) {
|
|
|
2948
3077
|
`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
3078
|
);
|
|
2950
3079
|
}
|
|
3080
|
+
const incomingTokens = bodyTokenSet(input.body);
|
|
3081
|
+
function bodySimilarWarnings(excludeIds) {
|
|
3082
|
+
const dup = maxBodySimilarity(incomingTokens, existing, resolvedScope, input.type, excludeIds);
|
|
3083
|
+
if (!dup?.id) return {};
|
|
3084
|
+
const body_similar = {
|
|
3085
|
+
id: dup.id,
|
|
3086
|
+
score: Math.round(dup.score * 100) / 100
|
|
3087
|
+
};
|
|
3088
|
+
return {
|
|
3089
|
+
similarityWarning: `Body is ~${Math.round(dup.score * 100)}% similar (token overlap) to existing "${dup.id}" \u2014 consolidate if redundant.`,
|
|
3090
|
+
body_similar
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
2951
3093
|
if (input.topic) {
|
|
2952
3094
|
const topicMatch = existing.find(
|
|
2953
3095
|
({ memory: memory2 }) => memory2.frontmatter.topic === input.topic && memory2.frontmatter.scope === resolvedScope && (!input.module || memory2.frontmatter.module === input.module)
|
|
2954
3096
|
);
|
|
2955
3097
|
if (topicMatch) {
|
|
2956
3098
|
const fm = topicMatch.memory.frontmatter;
|
|
3099
|
+
const { similarityWarning: simW, body_similar: bs } = bodySimilarWarnings(/* @__PURE__ */ new Set([fm.id]));
|
|
2957
3100
|
const newFrontmatter = {
|
|
2958
3101
|
...fm,
|
|
2959
3102
|
body: input.body,
|
|
@@ -2970,13 +3113,19 @@ async function memSave(input, ctx) {
|
|
|
2970
3113
|
serializeMemory2({ frontmatter: newFrontmatter, body: input.body }),
|
|
2971
3114
|
"utf8"
|
|
2972
3115
|
);
|
|
3116
|
+
const mergedTw = [
|
|
3117
|
+
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by haive sync.` : null,
|
|
3118
|
+
simW ?? null
|
|
3119
|
+
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
2973
3120
|
return {
|
|
2974
3121
|
id: fm.id,
|
|
2975
3122
|
scope: fm.scope,
|
|
2976
3123
|
file_path: topicMatch.filePath,
|
|
2977
3124
|
action: "updated",
|
|
2978
3125
|
revision_count: newFrontmatter.revision_count,
|
|
2979
|
-
...
|
|
3126
|
+
...mergedTw ? { warning: mergedTw } : {},
|
|
3127
|
+
...bs ? { body_similar: bs } : {},
|
|
3128
|
+
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
2980
3129
|
};
|
|
2981
3130
|
}
|
|
2982
3131
|
}
|
|
@@ -3018,9 +3167,11 @@ async function memSave(input, ctx) {
|
|
|
3018
3167
|
}
|
|
3019
3168
|
}
|
|
3020
3169
|
await writeFile22(file, serializeMemory2({ frontmatter, body: input.body }), "utf8");
|
|
3170
|
+
const { similarityWarning: simWarnNew, body_similar: bsNew } = bodySimilarWarnings();
|
|
3021
3171
|
const finalWarning = [
|
|
3022
3172
|
invalidPaths.length > 0 ? `Anchor path(s) not found in project: ${invalidPaths.join(", ")}. They will be marked stale by \`haive sync\`.` : null,
|
|
3023
|
-
warning ?? null
|
|
3173
|
+
warning ?? null,
|
|
3174
|
+
simWarnNew ?? null
|
|
3024
3175
|
].filter(Boolean).join(" \u2014 ") || void 0;
|
|
3025
3176
|
return {
|
|
3026
3177
|
id: frontmatter.id,
|
|
@@ -3029,6 +3180,7 @@ async function memSave(input, ctx) {
|
|
|
3029
3180
|
action: "created",
|
|
3030
3181
|
...finalWarning ? { warning: finalWarning } : {},
|
|
3031
3182
|
...similar_found ? { similar_found } : {},
|
|
3183
|
+
...bsNew ? { body_similar: bsNew } : {},
|
|
3032
3184
|
...invalidPaths.length > 0 ? { invalid_paths: invalidPaths } : {}
|
|
3033
3185
|
};
|
|
3034
3186
|
}
|
|
@@ -3044,6 +3196,9 @@ var MemSearchInputSchema = {
|
|
|
3044
3196
|
semantic: z5.boolean().default(false).describe(
|
|
3045
3197
|
"Use semantic similarity from the embeddings index (requires `haive embeddings index`)."
|
|
3046
3198
|
),
|
|
3199
|
+
lexical_rank: z5.boolean().default(false).describe(
|
|
3200
|
+
"When true (and semantic is false), rank the filtered corpus with Okapi-BM25-style lexical scoring instead of literal AND/OR. Helps phrase-like queries without embeddings."
|
|
3201
|
+
),
|
|
3047
3202
|
min_score: z5.number().min(0).max(1).default(0).describe("Minimum cosine similarity (semantic mode only)"),
|
|
3048
3203
|
track: z5.boolean().default(true).describe("Increment read_count on returned memories (used for passive validation)")
|
|
3049
3204
|
};
|
|
@@ -3069,6 +3224,27 @@ async function memSearch(input, ctx) {
|
|
|
3069
3224
|
notice: "Semantic search unavailable (embeddings index missing or @hiveai/embeddings not installed). Falling back to literal search."
|
|
3070
3225
|
};
|
|
3071
3226
|
}
|
|
3227
|
+
} else if (input.lexical_rank && input.query.trim()) {
|
|
3228
|
+
const { ranked, scores } = rankMemoriesLexical(
|
|
3229
|
+
filtered,
|
|
3230
|
+
input.query,
|
|
3231
|
+
input.limit
|
|
3232
|
+
);
|
|
3233
|
+
if (ranked.length > 0) {
|
|
3234
|
+
const snippetNeedle = pickSnippetNeedle(input.query);
|
|
3235
|
+
result = {
|
|
3236
|
+
matches: ranked.map(
|
|
3237
|
+
(loaded, i) => lexicalHit(loaded, snippetNeedle, usage, scores[i])
|
|
3238
|
+
),
|
|
3239
|
+
total: ranked.length,
|
|
3240
|
+
mode: "lexical_ranked"
|
|
3241
|
+
};
|
|
3242
|
+
} else {
|
|
3243
|
+
result = {
|
|
3244
|
+
...buildLiteralResult(input, filtered, usage),
|
|
3245
|
+
notice: "Lexical ranking found no BM25-positive hits \u2014 showing literal matches instead."
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3072
3248
|
} else {
|
|
3073
3249
|
result = buildLiteralResult(input, filtered, usage);
|
|
3074
3250
|
}
|
|
@@ -3166,6 +3342,9 @@ function toHit(loaded, needle, usage) {
|
|
|
3166
3342
|
file_path: loaded.filePath
|
|
3167
3343
|
};
|
|
3168
3344
|
}
|
|
3345
|
+
function lexicalHit(loaded, needle, usage, lexicalScore) {
|
|
3346
|
+
return { ...toHit(loaded, needle, usage), score: lexicalScore };
|
|
3347
|
+
}
|
|
3169
3348
|
var MemVerifyInputSchema = {
|
|
3170
3349
|
id: z6.string().optional().describe("If set, verify only this memory id"),
|
|
3171
3350
|
update: z6.boolean().default(false).describe("Write the resulting status back to disk (status=stale or validated)")
|
|
@@ -3930,17 +4109,29 @@ var GetBriefingInputSchema = {
|
|
|
3930
4109
|
),
|
|
3931
4110
|
include_stale: z17.boolean().default(false).describe("Include stale memories (excluded by default \u2014 they may be outdated)"),
|
|
3932
4111
|
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
|
|
4112
|
+
format: z17.enum(["full", "compact", "actions"]).default("full").describe(
|
|
4113
|
+
"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
4114
|
),
|
|
3936
4115
|
symbols: z17.array(z17.string()).default([]).describe(
|
|
3937
4116
|
"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
4117
|
),
|
|
3939
4118
|
min_semantic_score: z17.number().min(0).max(1).default(0).describe(
|
|
3940
4119
|
"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."
|
|
4120
|
+
),
|
|
4121
|
+
budget_preset: z17.enum(["quick", "balanced", "deep"]).optional().describe(
|
|
4122
|
+
"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
4123
|
)
|
|
3942
4124
|
};
|
|
4125
|
+
var GetBriefingZod = z17.object(GetBriefingInputSchema);
|
|
3943
4126
|
async function getBriefing(input, ctx) {
|
|
4127
|
+
const resolvedBudget = resolveBriefingBudget2(input.budget_preset, {
|
|
4128
|
+
max_tokens: input.max_tokens,
|
|
4129
|
+
max_memories: input.max_memories,
|
|
4130
|
+
include_module_contexts: input.include_module_contexts
|
|
4131
|
+
});
|
|
4132
|
+
const briefingMaxTokens = resolvedBudget.max_tokens;
|
|
4133
|
+
const briefingMaxMemories = resolvedBudget.max_memories;
|
|
4134
|
+
const briefingIncludeModules = resolvedBudget.include_module_contexts;
|
|
3944
4135
|
const inferred = inferModulesFromPaths2(input.files);
|
|
3945
4136
|
const memories = [];
|
|
3946
4137
|
let searchMode = "literal";
|
|
@@ -4052,8 +4243,8 @@ async function getBriefing(input, ctx) {
|
|
|
4052
4243
|
const sb = reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
|
|
4053
4244
|
return sb - sa;
|
|
4054
4245
|
});
|
|
4055
|
-
for (const mem of ranked.slice(0,
|
|
4056
|
-
if (seen.size >=
|
|
4246
|
+
for (const mem of ranked.slice(0, briefingMaxMemories)) {
|
|
4247
|
+
if (seen.size >= briefingMaxMemories * 2) break;
|
|
4057
4248
|
const loaded = byId.get(mem.id);
|
|
4058
4249
|
if (!loaded) continue;
|
|
4059
4250
|
for (const relId of loaded.memory.frontmatter.related_ids ?? []) {
|
|
@@ -4062,7 +4253,7 @@ async function getBriefing(input, ctx) {
|
|
|
4062
4253
|
if (related) addOrUpdate(related, "anchor", void 0, "partial");
|
|
4063
4254
|
}
|
|
4064
4255
|
}
|
|
4065
|
-
memories.push(...ranked.slice(0,
|
|
4256
|
+
memories.push(...ranked.slice(0, briefingMaxMemories));
|
|
4066
4257
|
if (input.track && memories.length > 0) {
|
|
4067
4258
|
await trackReads3(ctx.paths, memories.map((m) => m.id));
|
|
4068
4259
|
const freshUsage = await loadUsageIndex7(ctx.paths);
|
|
@@ -4142,7 +4333,7 @@ async function getBriefing(input, ctx) {
|
|
|
4142
4333
|
}
|
|
4143
4334
|
}
|
|
4144
4335
|
}
|
|
4145
|
-
const moduleContents =
|
|
4336
|
+
const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
|
|
4146
4337
|
const memoriesText = memories.map((m) => {
|
|
4147
4338
|
const unverified = m.status === "proposed" ? " [UNVERIFIED \u2014 not yet validated]" : "";
|
|
4148
4339
|
return `### ${m.id} (${m.scope}/${m.type}, ${m.confidence})${unverified}
|
|
@@ -4160,7 +4351,7 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4160
4351
|
},
|
|
4161
4352
|
{ key: "memories", text: memoriesText, weight: 4, mode: "head" }
|
|
4162
4353
|
],
|
|
4163
|
-
|
|
4354
|
+
briefingMaxTokens
|
|
4164
4355
|
);
|
|
4165
4356
|
const projectSlice = slices.find((s) => s.key === "project");
|
|
4166
4357
|
const modulesSlice = slices.find((s) => s.key === "modules");
|
|
@@ -4202,7 +4393,10 @@ ${m.content}`).join("\n\n---\n\n"),
|
|
|
4202
4393
|
const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4203
4394
|
if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
|
|
4204
4395
|
}
|
|
4205
|
-
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : trimmedMemories
|
|
4396
|
+
const outputMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
|
|
4397
|
+
...m,
|
|
4398
|
+
body: extractActionsBriefBody2(m.body)
|
|
4399
|
+
})) : trimmedMemories;
|
|
4206
4400
|
let symbolLocations;
|
|
4207
4401
|
const symbolsToLookup = new Set(input.symbols);
|
|
4208
4402
|
for (const m of outputMemories) {
|
|
@@ -4324,6 +4518,11 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
4324
4518
|
"After completing the task: capture new gotchas with mem_observe, failed approaches with mem_tried, validated patterns with mem_save."
|
|
4325
4519
|
);
|
|
4326
4520
|
}
|
|
4521
|
+
if (outputMemories.length > 2 && !input.budget_preset && input.task && !hints.some((h) => h.includes("budget_preset"))) {
|
|
4522
|
+
hints.push(
|
|
4523
|
+
"For tighter token budgets on small tasks pass budget_preset:'quick'; for refactor-sized work use budget_preset:'deep'."
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4327
4526
|
}
|
|
4328
4527
|
return {
|
|
4329
4528
|
...input.task ? { task: input.task } : {},
|
|
@@ -4346,7 +4545,8 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
|
|
|
4346
4545
|
...hints.length > 0 ? { hints } : {},
|
|
4347
4546
|
estimated_tokens: totalTokens,
|
|
4348
4547
|
budget: {
|
|
4349
|
-
max_tokens:
|
|
4548
|
+
max_tokens: briefingMaxTokens,
|
|
4549
|
+
...input.budget_preset ? { preset_applied: input.budget_preset } : {},
|
|
4350
4550
|
spent: {
|
|
4351
4551
|
project: projectSlice.estimatedTokens,
|
|
4352
4552
|
modules: modulesSlice.estimatedTokens,
|
|
@@ -4542,7 +4742,7 @@ var MemRelevantToInputSchema = {
|
|
|
4542
4742
|
files: z21.array(z21.string()).default([]).describe("Optional: files you are about to edit \u2014 surfaces anchored memories."),
|
|
4543
4743
|
limit: z21.number().int().positive().max(30).default(8).describe("Cap on returned memories."),
|
|
4544
4744
|
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.")
|
|
4745
|
+
format: z21.enum(["full", "compact", "actions"]).default("full").describe("'compact' = id + 1-line summary; 'full' = complete bodies; 'actions' = bullet-first excerpts.")
|
|
4546
4746
|
};
|
|
4547
4747
|
async function memRelevantTo(input, ctx) {
|
|
4548
4748
|
const briefingInput = {
|
|
@@ -5476,11 +5676,76 @@ function gitFileDiff(root, file, sinceDays) {
|
|
|
5476
5676
|
return null;
|
|
5477
5677
|
}
|
|
5478
5678
|
}
|
|
5679
|
+
var MemConflictCandidatesInputSchema = {
|
|
5680
|
+
since_days: z30.number().int().positive().max(3650).default(365).describe("Only memories created since N days ago"),
|
|
5681
|
+
types: z30.array(z30.enum(["decision", "architecture", "convention", "gotcha"])).default(["decision", "architecture"]).describe("Memory types scanned for pairwise lexical overlap"),
|
|
5682
|
+
min_jaccard: z30.number().min(0).max(1).default(0.45).describe("Minimum Jaccard token similarity to surface as a candidate pair"),
|
|
5683
|
+
max_pairs: z30.number().int().positive().max(100).default(20).describe("Cap pairs returned"),
|
|
5684
|
+
max_scan: z30.number().int().positive().max(2e3).default(500).describe("Maximum memories sampled for O(n\xB2) scan \u2014 excess dropped after chronological sort.")
|
|
5685
|
+
};
|
|
5686
|
+
async function memConflictCandidates(input, ctx) {
|
|
5687
|
+
if (!existsSync27(ctx.paths.memoriesDir)) {
|
|
5688
|
+
return {
|
|
5689
|
+
pairs: [],
|
|
5690
|
+
scanned: 0,
|
|
5691
|
+
truncated: false,
|
|
5692
|
+
notice: "No .ai/memories directory."
|
|
5693
|
+
};
|
|
5694
|
+
}
|
|
5695
|
+
const all = await loadMemoriesFromDir21(ctx.paths.memoriesDir);
|
|
5696
|
+
const { pairs, scanned, truncated } = findLexicalConflictPairs(all, {
|
|
5697
|
+
sinceDays: input.since_days,
|
|
5698
|
+
types: input.types,
|
|
5699
|
+
minJaccard: input.min_jaccard,
|
|
5700
|
+
maxPairs: input.max_pairs,
|
|
5701
|
+
maxScan: input.max_scan
|
|
5702
|
+
});
|
|
5703
|
+
const notice = pairs.length === 0 ? "No lexical candidate pairs \u2265 threshold \u2014 try lowering min_jaccard or widen since_days/types." : void 0;
|
|
5704
|
+
return { pairs, scanned, truncated, notice };
|
|
5705
|
+
}
|
|
5706
|
+
var MemResolveProjectInputSchema = {
|
|
5707
|
+
cwd: z31.string().optional().describe("Directory used for root discovery when HAIVE_PROJECT_ROOT is unset.")
|
|
5708
|
+
};
|
|
5709
|
+
async function memResolveProject(input, _ctx) {
|
|
5710
|
+
void _ctx;
|
|
5711
|
+
return {
|
|
5712
|
+
ok: true,
|
|
5713
|
+
info: resolveProjectInfo({
|
|
5714
|
+
cwd: input.cwd
|
|
5715
|
+
})
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
var MemSuggestTopicInputSchema = {
|
|
5719
|
+
type: MemoryTypeSchema.describe("Memory kind \u2014 drives the suggested topic family."),
|
|
5720
|
+
title: z32.string().min(1).describe("Short title or phrase (headers, headings) \u2014 turned into slug")
|
|
5721
|
+
};
|
|
5722
|
+
async function memSuggestTopic(input, _ctx) {
|
|
5723
|
+
void _ctx;
|
|
5724
|
+
const suggestion = suggestTopicKey(input.type, input.title);
|
|
5725
|
+
return { topic_key: suggestion.topic_key, family: suggestion.family, type: input.type };
|
|
5726
|
+
}
|
|
5727
|
+
var MemTimelineInputSchema = {
|
|
5728
|
+
memory_id: z33.string().optional().describe("Seed id \u2014 expands via related_ids, topic, anchors"),
|
|
5729
|
+
topic: z33.string().optional().describe("Frontmatter.topic value \u2014 chronological list when memory_id omitted"),
|
|
5730
|
+
limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
|
|
5731
|
+
};
|
|
5732
|
+
async function memTimeline(input, ctx) {
|
|
5733
|
+
if (!existsSync28(ctx.paths.memoriesDir)) {
|
|
5734
|
+
return { entries: [], total: 0, notice: "No .ai/memories directory." };
|
|
5735
|
+
}
|
|
5736
|
+
const all = await loadMemoriesFromDir222(ctx.paths.memoriesDir);
|
|
5737
|
+
const { entries, notice } = collectTimelineEntries(all, {
|
|
5738
|
+
memoryId: input.memory_id,
|
|
5739
|
+
topic: input.topic,
|
|
5740
|
+
limit: input.limit
|
|
5741
|
+
});
|
|
5742
|
+
return { entries, total: entries.length, notice };
|
|
5743
|
+
}
|
|
5479
5744
|
var BootstrapProjectArgsSchema = {
|
|
5480
|
-
module:
|
|
5745
|
+
module: z34.string().optional().describe(
|
|
5481
5746
|
"Optional module name to scope the analysis to (writes to .ai/modules/<module>/context.md)"
|
|
5482
5747
|
),
|
|
5483
|
-
focus:
|
|
5748
|
+
focus: z34.string().optional().describe("Optional area to emphasize (e.g. 'data layer', 'API surface')")
|
|
5484
5749
|
};
|
|
5485
5750
|
var ROOT_TEMPLATE = `# Project context
|
|
5486
5751
|
|
|
@@ -5561,8 +5826,8 @@ ${template}\`\`\`
|
|
|
5561
5826
|
};
|
|
5562
5827
|
}
|
|
5563
5828
|
var PostTaskArgsSchema = {
|
|
5564
|
-
task_summary:
|
|
5565
|
-
files_touched:
|
|
5829
|
+
task_summary: z35.string().optional().describe("One sentence describing what you just did"),
|
|
5830
|
+
files_touched: z35.array(z35.string()).optional().describe("Files you created or modified during the task")
|
|
5566
5831
|
};
|
|
5567
5832
|
function postTaskPrompt(args, ctx) {
|
|
5568
5833
|
const taskLine = args.task_summary ? `
|
|
@@ -5645,10 +5910,10 @@ When done, respond with a brief summary: "Saved N memories: [list of IDs]. Sessi
|
|
|
5645
5910
|
};
|
|
5646
5911
|
}
|
|
5647
5912
|
var ImportDocsArgsSchema = {
|
|
5648
|
-
content:
|
|
5649
|
-
source:
|
|
5650
|
-
scope:
|
|
5651
|
-
dry_run:
|
|
5913
|
+
content: z36.string().describe("The documentation content to analyze and import as memories (Markdown, README, ADR, etc.)"),
|
|
5914
|
+
source: z36.string().optional().describe("Origin of the content (file path, URL, or document title) \u2014 used to anchor memories"),
|
|
5915
|
+
scope: z36.enum(["personal", "team"]).default("team").describe("Scope to assign to created memories"),
|
|
5916
|
+
dry_run: z36.boolean().default(false).describe("If true, describe what would be saved without actually calling mem_save")
|
|
5652
5917
|
};
|
|
5653
5918
|
function importDocsPrompt(args, ctx) {
|
|
5654
5919
|
const sourceLine = args.source ? `
|
|
@@ -5711,7 +5976,7 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
|
|
|
5711
5976
|
};
|
|
5712
5977
|
}
|
|
5713
5978
|
var SERVER_NAME = "haive";
|
|
5714
|
-
var SERVER_VERSION = "0.9.
|
|
5979
|
+
var SERVER_VERSION = "0.9.5";
|
|
5715
5980
|
function jsonResult(data) {
|
|
5716
5981
|
return {
|
|
5717
5982
|
content: [
|
|
@@ -5762,6 +6027,23 @@ function createHaiveServer(options = {}) {
|
|
|
5762
6027
|
return jsonResult(await memSave(input, context));
|
|
5763
6028
|
}
|
|
5764
6029
|
);
|
|
6030
|
+
server.tool(
|
|
6031
|
+
"mem_suggest_topic",
|
|
6032
|
+
[
|
|
6033
|
+
"Propose a stable `topic` key (topic-upsert) from type + short title.",
|
|
6034
|
+
"",
|
|
6035
|
+
"USE BEFORE mem_save when you want deterministic updates to the same memory over time;",
|
|
6036
|
+
"families mimic Engram-style grouping (architecture/*, bug/*, decision/*, \u2026).",
|
|
6037
|
+
"",
|
|
6038
|
+
"PARAMETERS:",
|
|
6039
|
+
" type \u2014 convention | decision | gotcha | architecture | glossary | attempt | session_recap",
|
|
6040
|
+
" title \u2014 phrase to slugify under the suggested family prefix",
|
|
6041
|
+
"",
|
|
6042
|
+
"RETURNS: { topic_key, family, type }"
|
|
6043
|
+
].join("\n"),
|
|
6044
|
+
MemSuggestTopicInputSchema,
|
|
6045
|
+
async (input) => jsonResult(await memSuggestTopic(input, context))
|
|
6046
|
+
);
|
|
5765
6047
|
server.tool(
|
|
5766
6048
|
"mem_tried",
|
|
5767
6049
|
[
|
|
@@ -5853,8 +6135,12 @@ function createHaiveServer(options = {}) {
|
|
|
5853
6135
|
server.tool(
|
|
5854
6136
|
"get_briefing",
|
|
5855
6137
|
[
|
|
5856
|
-
"\u2B50
|
|
5857
|
-
"
|
|
6138
|
+
"\u2B50 DEFAULT-FIRST for coding agents on any repo where `haive init` ran: call this BEFORE",
|
|
6139
|
+
"changing source or project config for the current goal (unless the developer explicitly opts out).",
|
|
6140
|
+
"One-shot onboarding: everything relevant in a single call under a token budget.",
|
|
6141
|
+
"",
|
|
6142
|
+
"PROGRESSIVE DISCLOSURE \u2014 after this, drill down only if needed:",
|
|
6143
|
+
" mem_relevant_to / mem_search (compact lists) \u2192 mem_get (full body + anchors).",
|
|
5858
6144
|
"",
|
|
5859
6145
|
"RETURNS (in order of priority):",
|
|
5860
6146
|
" 0. action_required \u2014 \u26A0\uFE0F HANDLE THIS FIRST if non-empty (see protocol below)",
|
|
@@ -5877,7 +6163,8 @@ function createHaiveServer(options = {}) {
|
|
|
5877
6163
|
" task \u2014 what you are about to do (1\u20132 sentences) \u2014 ALWAYS provide this",
|
|
5878
6164
|
" files \u2014 files you are about to edit \u2014 surfaces anchored memories",
|
|
5879
6165
|
" symbols \u2014 symbol names to look up in the code-map (e.g. ['PaymentService'])",
|
|
5880
|
-
" format \u2014 'full' (default) | 'compact' (1-line
|
|
6166
|
+
" format \u2014 'full' (default) | 'compact' (1-line) | 'actions' (bullet-first excerpts)",
|
|
6167
|
+
" budget_preset \u2014 'quick' | 'balanced' | 'deep' \u2014 scales max_tokens/memories/module contexts",
|
|
5881
6168
|
"",
|
|
5882
6169
|
"EXAMPLE USAGE:",
|
|
5883
6170
|
" get_briefing({ task: 'add a Stripe payment integration', files: ['src/payments/'], symbols: ['PaymentService'] })",
|
|
@@ -5888,7 +6175,7 @@ function createHaiveServer(options = {}) {
|
|
|
5888
6175
|
" low \u2014 proposed, few reads (take with caution)",
|
|
5889
6176
|
" unverified \u2014 draft (unverified: true flag set)",
|
|
5890
6177
|
"",
|
|
5891
|
-
"Replaces 4\u20135 separate tool calls.
|
|
6178
|
+
"Replaces 4\u20135 separate tool calls. Prefer this first; use mem_search / mem_get only for follow-up."
|
|
5892
6179
|
].join("\n"),
|
|
5893
6180
|
GetBriefingInputSchema,
|
|
5894
6181
|
async (input) => {
|
|
@@ -5907,6 +6194,8 @@ function createHaiveServer(options = {}) {
|
|
|
5907
6194
|
"SEARCH MODES:",
|
|
5908
6195
|
" Literal (default): AND search across id, tags, and body \u2014 all tokens must match.",
|
|
5909
6196
|
" Falls back to OR automatically if no AND results (partial match).",
|
|
6197
|
+
" Lexical rank (lexical_rank: true, semantic: false): Okapi-BM25-style scoring on the",
|
|
6198
|
+
" filtered corpus \u2014 good for phrase-like queries without embeddings.",
|
|
5910
6199
|
" Semantic (semantic: true): embedding-based similarity \u2014 finds related memories",
|
|
5911
6200
|
" even with different wording. Requires haive embeddings index to be built.",
|
|
5912
6201
|
"",
|
|
@@ -5915,6 +6204,7 @@ function createHaiveServer(options = {}) {
|
|
|
5915
6204
|
" scope \u2014 filter by personal | team | module",
|
|
5916
6205
|
" type \u2014 filter by convention | decision | gotcha | architecture | glossary",
|
|
5917
6206
|
" semantic \u2014 true for embedding-based search (requires @hiveai/embeddings)",
|
|
6207
|
+
" lexical_rank \u2014 BM25-style ranking (ignored when semantic is true)",
|
|
5918
6208
|
" limit \u2014 max results (default 10)",
|
|
5919
6209
|
"",
|
|
5920
6210
|
"RETURNS: array of { id, type, scope, status, confidence, body, match_quality }"
|
|
@@ -5925,6 +6215,22 @@ function createHaiveServer(options = {}) {
|
|
|
5925
6215
|
return jsonResult(await memSearch(input, context));
|
|
5926
6216
|
}
|
|
5927
6217
|
);
|
|
6218
|
+
server.tool(
|
|
6219
|
+
"mem_timeline",
|
|
6220
|
+
[
|
|
6221
|
+
"Chronological view of related memories: by shared frontmatter.topic OR expanded from a seed id",
|
|
6222
|
+
"(related_ids, same topic, overlapping anchor paths \u2014 one extra hop on related_ids).",
|
|
6223
|
+
"",
|
|
6224
|
+
"PARAMETERS:",
|
|
6225
|
+
" memory_id \u2014 optional seed memory id",
|
|
6226
|
+
" topic \u2014 optional topic key (required if memory_id omitted)",
|
|
6227
|
+
" limit \u2014 max entries (default 30)",
|
|
6228
|
+
"",
|
|
6229
|
+
"RETURNS: { entries: [{ id, type, scope, created_at, one_line, topic? }], total, notice? }"
|
|
6230
|
+
].join("\n"),
|
|
6231
|
+
MemTimelineInputSchema,
|
|
6232
|
+
async (input) => jsonResult(await memTimeline(input, context))
|
|
6233
|
+
);
|
|
5928
6234
|
server.tool(
|
|
5929
6235
|
"mem_for_files",
|
|
5930
6236
|
[
|
|
@@ -5954,7 +6260,7 @@ function createHaiveServer(options = {}) {
|
|
|
5954
6260
|
[
|
|
5955
6261
|
"Fetch a single memory by its full id with all details.",
|
|
5956
6262
|
"",
|
|
5957
|
-
"USE WHEN get_briefing returned a
|
|
6263
|
+
"USE WHEN get_briefing / mem_relevant_to / mem_search returned a compact hit and you need",
|
|
5958
6264
|
"the full body, or when you know the exact id of a memory.",
|
|
5959
6265
|
"",
|
|
5960
6266
|
"PARAMETERS:",
|
|
@@ -6042,6 +6348,22 @@ function createHaiveServer(options = {}) {
|
|
|
6042
6348
|
CodeMapInputSchema,
|
|
6043
6349
|
async (input) => jsonResult(await codeMapTool(input, context))
|
|
6044
6350
|
);
|
|
6351
|
+
server.tool(
|
|
6352
|
+
"mem_resolve_project",
|
|
6353
|
+
[
|
|
6354
|
+
"Diagnostics: resolve which project root hAIve is using (never throws).",
|
|
6355
|
+
"",
|
|
6356
|
+
"USE IN multi-root workspaces or when the agent CWD may not be the repo root \u2014",
|
|
6357
|
+
"mirrors HAIVE_PROJECT_ROOT, findProjectRoot markers, and presence of .ai/memories.",
|
|
6358
|
+
"",
|
|
6359
|
+
"PARAMETERS:",
|
|
6360
|
+
" cwd \u2014 optional directory used for discovery when HAIVE_PROJECT_ROOT is unset",
|
|
6361
|
+
"",
|
|
6362
|
+
"RETURNS: { ok: true, info: { cwd, resolved_root, haive_project_root_env, \u2026 } }"
|
|
6363
|
+
].join("\n"),
|
|
6364
|
+
MemResolveProjectInputSchema,
|
|
6365
|
+
async (input) => jsonResult(await memResolveProject(input, context))
|
|
6366
|
+
);
|
|
6045
6367
|
server.tool(
|
|
6046
6368
|
"mem_update",
|
|
6047
6369
|
[
|
|
@@ -6175,6 +6497,8 @@ function createHaiveServer(options = {}) {
|
|
|
6175
6497
|
"One-shot ranked memories for a task \u2014 use instead of get_briefing when",
|
|
6176
6498
|
"project context is already loaded and you only want the relevant memory layer.",
|
|
6177
6499
|
"",
|
|
6500
|
+
"Second step in progressive disclosure (after get_briefing): narrow here, then mem_get for full text.",
|
|
6501
|
+
"",
|
|
6178
6502
|
"Reuses the same ranking pipeline (anchor / module / literal / semantic) but",
|
|
6179
6503
|
"skips project_context, modules, action_required, etc.",
|
|
6180
6504
|
"",
|
|
@@ -6183,6 +6507,7 @@ function createHaiveServer(options = {}) {
|
|
|
6183
6507
|
" files \u2014 files you'll edit (surfaces anchored memories)",
|
|
6184
6508
|
" limit \u2014 cap on returned memories (default 8)",
|
|
6185
6509
|
" min_semantic_score \u2014 drop weak semantic hits below this cosine (default 0.25)",
|
|
6510
|
+
" format \u2014 'full' | 'compact' | 'actions' (inherits get_briefing memory framing)",
|
|
6186
6511
|
"",
|
|
6187
6512
|
"RETURNS: { task, search_mode, memories: [...], hints?: [...], empty?: true }"
|
|
6188
6513
|
].join("\n"),
|
|
@@ -6331,6 +6656,24 @@ function createHaiveServer(options = {}) {
|
|
|
6331
6656
|
return jsonResult(await memConflicts(input, context));
|
|
6332
6657
|
}
|
|
6333
6658
|
);
|
|
6659
|
+
server.tool(
|
|
6660
|
+
"mem_conflict_candidates",
|
|
6661
|
+
[
|
|
6662
|
+
"Bulk lexical scan for decision/architecture-like pairs that look similar (Jaccard on tokens).",
|
|
6663
|
+
"",
|
|
6664
|
+
"Advisory only \u2014 follow with mem_conflicts_with on specific ids for real contradiction checks.",
|
|
6665
|
+
"",
|
|
6666
|
+
"PARAMETERS:",
|
|
6667
|
+
" since_days, types, min_jaccard, max_pairs, max_scan",
|
|
6668
|
+
"",
|
|
6669
|
+
"RETURNS: { pairs: [{ id_a, id_b, jaccard }], scanned, truncated, notice? }"
|
|
6670
|
+
].join("\n"),
|
|
6671
|
+
MemConflictCandidatesInputSchema,
|
|
6672
|
+
async (input) => {
|
|
6673
|
+
tracker.record("mem_conflict_candidates", `${input.since_days}d`);
|
|
6674
|
+
return jsonResult(await memConflictCandidates(input, context));
|
|
6675
|
+
}
|
|
6676
|
+
);
|
|
6334
6677
|
server.tool(
|
|
6335
6678
|
"pre_commit_check",
|
|
6336
6679
|
[
|
|
@@ -6468,7 +6811,7 @@ function registerMcp(program2) {
|
|
|
6468
6811
|
// src/commands/sync.ts
|
|
6469
6812
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6470
6813
|
import { readFile as readFile8, writeFile as writeFile13, mkdir as mkdir8 } from "fs/promises";
|
|
6471
|
-
import { existsSync as
|
|
6814
|
+
import { existsSync as existsSync29 } from "fs";
|
|
6472
6815
|
import path12 from "path";
|
|
6473
6816
|
import "commander";
|
|
6474
6817
|
import {
|
|
@@ -6480,7 +6823,7 @@ import {
|
|
|
6480
6823
|
isDecaying as isDecaying2,
|
|
6481
6824
|
loadCodeMap as loadCodeMap4,
|
|
6482
6825
|
loadConfig as loadConfig4,
|
|
6483
|
-
loadMemoriesFromDir as
|
|
6826
|
+
loadMemoriesFromDir as loadMemoriesFromDir23,
|
|
6484
6827
|
loadUsageIndex as loadUsageIndex12,
|
|
6485
6828
|
pullCrossRepoSources,
|
|
6486
6829
|
resolveHaivePaths as resolveHaivePaths7,
|
|
@@ -6504,7 +6847,7 @@ function registerSync(program2) {
|
|
|
6504
6847
|
).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) => {
|
|
6505
6848
|
const root = findProjectRoot10(opts.dir);
|
|
6506
6849
|
const paths = resolveHaivePaths7(root);
|
|
6507
|
-
if (!
|
|
6850
|
+
if (!existsSync29(paths.memoriesDir)) {
|
|
6508
6851
|
if (!opts.quiet) ui.warn(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
6509
6852
|
process.exitCode = 1;
|
|
6510
6853
|
return;
|
|
@@ -6520,7 +6863,7 @@ function registerSync(program2) {
|
|
|
6520
6863
|
let promoted = 0;
|
|
6521
6864
|
let autoApproved = 0;
|
|
6522
6865
|
if (opts.verify !== false) {
|
|
6523
|
-
const memories = await
|
|
6866
|
+
const memories = await loadMemoriesFromDir23(paths.memoriesDir);
|
|
6524
6867
|
for (const { memory: memory2, filePath } of memories) {
|
|
6525
6868
|
if (memory2.frontmatter.type === "session_recap") {
|
|
6526
6869
|
if (memory2.frontmatter.status === "stale") {
|
|
@@ -6581,7 +6924,7 @@ function registerSync(program2) {
|
|
|
6581
6924
|
}
|
|
6582
6925
|
}
|
|
6583
6926
|
if (opts.promote !== false) {
|
|
6584
|
-
const memories = await
|
|
6927
|
+
const memories = await loadMemoriesFromDir23(paths.memoriesDir);
|
|
6585
6928
|
const usage = await loadUsageIndex12(paths);
|
|
6586
6929
|
const nowMs = Date.now();
|
|
6587
6930
|
for (const { memory: memory2, filePath } of memories) {
|
|
@@ -6620,7 +6963,7 @@ function registerSync(program2) {
|
|
|
6620
6963
|
}
|
|
6621
6964
|
}
|
|
6622
6965
|
const sinceReport = opts.since ? collectSinceChanges(root, opts.since) : null;
|
|
6623
|
-
const draftMemories = (await
|
|
6966
|
+
const draftMemories = (await loadMemoriesFromDir23(paths.memoriesDir)).filter(
|
|
6624
6967
|
(m) => m.memory.frontmatter.status === "draft"
|
|
6625
6968
|
);
|
|
6626
6969
|
const draftCount = draftMemories.length;
|
|
@@ -6655,7 +6998,7 @@ function registerSync(program2) {
|
|
|
6655
6998
|
}
|
|
6656
6999
|
}
|
|
6657
7000
|
if (!opts.quiet) {
|
|
6658
|
-
const allForDecay = await
|
|
7001
|
+
const allForDecay = await loadMemoriesFromDir23(paths.memoriesDir);
|
|
6659
7002
|
const usageForDecay = await loadUsageIndex12(paths);
|
|
6660
7003
|
const decaying = allForDecay.filter(({ memory: memory2 }) => {
|
|
6661
7004
|
const fm = memory2.frontmatter;
|
|
@@ -6873,8 +7216,8 @@ Attends une **confirmation explicite** avant d'agir.
|
|
|
6873
7216
|
});
|
|
6874
7217
|
}
|
|
6875
7218
|
async function injectBridge(bridgeFile, memoriesDir, maxMemories, root, quiet) {
|
|
6876
|
-
if (!
|
|
6877
|
-
const all = await
|
|
7219
|
+
if (!existsSync29(memoriesDir)) return;
|
|
7220
|
+
const all = await loadMemoriesFromDir23(memoriesDir);
|
|
6878
7221
|
const top = all.filter(({ memory: memory2 }) => {
|
|
6879
7222
|
const s = memory2.frontmatter.status;
|
|
6880
7223
|
if (memory2.frontmatter.type === "session_recap") return false;
|
|
@@ -6898,7 +7241,7 @@ ${m.memory.body.trim()}`;
|
|
|
6898
7241
|
` + block + `
|
|
6899
7242
|
|
|
6900
7243
|
${BRIDGE_END}`;
|
|
6901
|
-
const fileExists =
|
|
7244
|
+
const fileExists = existsSync29(bridgeFile);
|
|
6902
7245
|
let existing = fileExists ? await readFile8(bridgeFile, "utf8") : "";
|
|
6903
7246
|
existing = existing.replace(/\r\n/g, "\n");
|
|
6904
7247
|
const startIdx = existing.indexOf(BRIDGE_START);
|
|
@@ -6949,14 +7292,14 @@ function collectSinceChanges(root, ref) {
|
|
|
6949
7292
|
// src/commands/memory-add.ts
|
|
6950
7293
|
import { createHash as createHash2 } from "crypto";
|
|
6951
7294
|
import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile14 } from "fs/promises";
|
|
6952
|
-
import { existsSync as
|
|
7295
|
+
import { existsSync as existsSync30 } from "fs";
|
|
6953
7296
|
import path13 from "path";
|
|
6954
7297
|
import "commander";
|
|
6955
7298
|
import {
|
|
6956
7299
|
buildFrontmatter as buildFrontmatter7,
|
|
6957
7300
|
findProjectRoot as findProjectRoot11,
|
|
6958
7301
|
inferModulesFromPaths as inferModulesFromPaths3,
|
|
6959
|
-
loadMemoriesFromDir as
|
|
7302
|
+
loadMemoriesFromDir as loadMemoriesFromDir24,
|
|
6960
7303
|
memoryFilePath as memoryFilePath6,
|
|
6961
7304
|
resolveHaivePaths as resolveHaivePaths8,
|
|
6962
7305
|
serializeMemory as serializeMemory12
|
|
@@ -6988,7 +7331,7 @@ function registerMemoryAdd(memory2) {
|
|
|
6988
7331
|
).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) => {
|
|
6989
7332
|
const root = findProjectRoot11(opts.dir);
|
|
6990
7333
|
const paths = resolveHaivePaths8(root);
|
|
6991
|
-
if (!
|
|
7334
|
+
if (!existsSync30(paths.haiveDir)) {
|
|
6992
7335
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
6993
7336
|
process.exitCode = 1;
|
|
6994
7337
|
return;
|
|
@@ -6999,7 +7342,7 @@ function registerMemoryAdd(memory2) {
|
|
|
6999
7342
|
const inferredTags = autoTagsEnabled ? inferModulesFromPaths3(anchorPaths) : [];
|
|
7000
7343
|
const mergedTags = Array.from(/* @__PURE__ */ new Set([...userTags, ...inferredTags]));
|
|
7001
7344
|
if (anchorPaths.length > 0) {
|
|
7002
|
-
const missing = anchorPaths.filter((p) => !
|
|
7345
|
+
const missing = anchorPaths.filter((p) => !existsSync30(path13.resolve(root, p)));
|
|
7003
7346
|
if (missing.length > 0) {
|
|
7004
7347
|
ui.warn(`Anchor path${missing.length > 1 ? "s" : ""} not found in project:`);
|
|
7005
7348
|
for (const p of missing) ui.warn(` \u2717 ${p}`);
|
|
@@ -7011,7 +7354,7 @@ function registerMemoryAdd(memory2) {
|
|
|
7011
7354
|
const title = opts.title ?? opts.slug;
|
|
7012
7355
|
let body;
|
|
7013
7356
|
if (opts.bodyFile !== void 0) {
|
|
7014
|
-
if (!
|
|
7357
|
+
if (!existsSync30(opts.bodyFile)) {
|
|
7015
7358
|
ui.error(`--body-file not found: ${opts.bodyFile}`);
|
|
7016
7359
|
process.exitCode = 1;
|
|
7017
7360
|
return;
|
|
@@ -7032,9 +7375,9 @@ TODO \u2014 write the memory body.
|
|
|
7032
7375
|
`;
|
|
7033
7376
|
}
|
|
7034
7377
|
const scope = opts.scope ?? "personal";
|
|
7035
|
-
if (
|
|
7378
|
+
if (existsSync30(paths.memoriesDir)) {
|
|
7036
7379
|
const incomingHash = createHash2("sha256").update(body.trim()).digest("hex").slice(0, 12);
|
|
7037
|
-
const allForHash = await
|
|
7380
|
+
const allForHash = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
7038
7381
|
const hashDup = allForHash.find(
|
|
7039
7382
|
({ memory: memory3 }) => createHash2("sha256").update(memory3.body.trim()).digest("hex").slice(0, 12) === incomingHash && memory3.frontmatter.scope === scope
|
|
7040
7383
|
);
|
|
@@ -7045,8 +7388,8 @@ TODO \u2014 write the memory body.
|
|
|
7045
7388
|
return;
|
|
7046
7389
|
}
|
|
7047
7390
|
}
|
|
7048
|
-
if (opts.topic &&
|
|
7049
|
-
const existing = await
|
|
7391
|
+
if (opts.topic && existsSync30(paths.memoriesDir)) {
|
|
7392
|
+
const existing = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
7050
7393
|
const topicMatch = existing.find(
|
|
7051
7394
|
({ memory: memory3 }) => memory3.frontmatter.topic === opts.topic && memory3.frontmatter.scope === scope && (!opts.module || memory3.frontmatter.module === opts.module)
|
|
7052
7395
|
);
|
|
@@ -7084,13 +7427,13 @@ TODO \u2014 write the memory body.
|
|
|
7084
7427
|
});
|
|
7085
7428
|
const file = memoryFilePath6(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7086
7429
|
await mkdir9(path13.dirname(file), { recursive: true });
|
|
7087
|
-
if (
|
|
7430
|
+
if (existsSync30(file)) {
|
|
7088
7431
|
ui.error(`Memory already exists at ${file}`);
|
|
7089
7432
|
process.exitCode = 1;
|
|
7090
7433
|
return;
|
|
7091
7434
|
}
|
|
7092
|
-
if (
|
|
7093
|
-
const existing = await
|
|
7435
|
+
if (existsSync30(paths.memoriesDir)) {
|
|
7436
|
+
const existing = await loadMemoriesFromDir24(paths.memoriesDir);
|
|
7094
7437
|
const slugTokens = opts.slug.toLowerCase().split(/[-_\s]+/).filter(Boolean);
|
|
7095
7438
|
const similar = existing.filter(({ memory: memory3 }) => {
|
|
7096
7439
|
const id = memory3.frontmatter.id.toLowerCase();
|
|
@@ -7132,14 +7475,14 @@ function parseCsv2(value) {
|
|
|
7132
7475
|
}
|
|
7133
7476
|
|
|
7134
7477
|
// src/commands/memory-list.ts
|
|
7135
|
-
import { existsSync as
|
|
7478
|
+
import { existsSync as existsSync31 } from "fs";
|
|
7136
7479
|
import path14 from "path";
|
|
7137
7480
|
import "commander";
|
|
7138
7481
|
import { findProjectRoot as findProjectRoot12, resolveHaivePaths as resolveHaivePaths9 } from "@hiveai/core";
|
|
7139
7482
|
|
|
7140
7483
|
// src/utils/fs.ts
|
|
7141
7484
|
import {
|
|
7142
|
-
loadMemoriesFromDir as
|
|
7485
|
+
loadMemoriesFromDir as loadMemoriesFromDir25,
|
|
7143
7486
|
loadMemory,
|
|
7144
7487
|
listMarkdownFilesRecursive
|
|
7145
7488
|
} from "@hiveai/core";
|
|
@@ -7149,12 +7492,12 @@ function registerMemoryList(memory2) {
|
|
|
7149
7492
|
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) => {
|
|
7150
7493
|
const root = findProjectRoot12(opts.dir);
|
|
7151
7494
|
const paths = resolveHaivePaths9(root);
|
|
7152
|
-
if (!
|
|
7495
|
+
if (!existsSync31(paths.memoriesDir)) {
|
|
7153
7496
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
7154
7497
|
process.exitCode = 1;
|
|
7155
7498
|
return;
|
|
7156
7499
|
}
|
|
7157
|
-
const all = await
|
|
7500
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7158
7501
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
7159
7502
|
const filtered = all.filter((m) => {
|
|
7160
7503
|
if (!matchesFilters(m, opts)) return false;
|
|
@@ -7217,7 +7560,7 @@ function matchesFilters(loaded, opts) {
|
|
|
7217
7560
|
|
|
7218
7561
|
// src/commands/memory-promote.ts
|
|
7219
7562
|
import { mkdir as mkdir10, unlink as unlink2, writeFile as writeFile15 } from "fs/promises";
|
|
7220
|
-
import { existsSync as
|
|
7563
|
+
import { existsSync as existsSync33 } from "fs";
|
|
7221
7564
|
import path15 from "path";
|
|
7222
7565
|
import "commander";
|
|
7223
7566
|
import {
|
|
@@ -7230,12 +7573,12 @@ function registerMemoryPromote(memory2) {
|
|
|
7230
7573
|
memory2.command("promote <id>").description("Promote a personal memory to team scope (status -> proposed)").option("-d, --dir <dir>", "project root").action(async (id, opts) => {
|
|
7231
7574
|
const root = findProjectRoot13(opts.dir);
|
|
7232
7575
|
const paths = resolveHaivePaths10(root);
|
|
7233
|
-
if (!
|
|
7576
|
+
if (!existsSync33(paths.memoriesDir)) {
|
|
7234
7577
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
7235
7578
|
process.exitCode = 1;
|
|
7236
7579
|
return;
|
|
7237
7580
|
}
|
|
7238
|
-
const teamAndModule = await
|
|
7581
|
+
const teamAndModule = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7239
7582
|
const alreadyShared = teamAndModule.find(
|
|
7240
7583
|
(m) => m.memory.frontmatter.id === id && (m.memory.frontmatter.scope === "team" || m.memory.frontmatter.scope === "module")
|
|
7241
7584
|
);
|
|
@@ -7249,7 +7592,7 @@ function registerMemoryPromote(memory2) {
|
|
|
7249
7592
|
}
|
|
7250
7593
|
return;
|
|
7251
7594
|
}
|
|
7252
|
-
const all = await
|
|
7595
|
+
const all = await loadMemoriesFromDir25(paths.personalDir);
|
|
7253
7596
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
7254
7597
|
if (!found) {
|
|
7255
7598
|
ui.error(`No personal memory with id "${id}". (Promotion only applies to personal scope.)`);
|
|
@@ -7275,7 +7618,7 @@ function registerMemoryPromote(memory2) {
|
|
|
7275
7618
|
}
|
|
7276
7619
|
|
|
7277
7620
|
// src/commands/memory-approve.ts
|
|
7278
|
-
import { existsSync as
|
|
7621
|
+
import { existsSync as existsSync34 } from "fs";
|
|
7279
7622
|
import { writeFile as writeFile16 } from "fs/promises";
|
|
7280
7623
|
import path16 from "path";
|
|
7281
7624
|
import "commander";
|
|
@@ -7288,12 +7631,12 @@ function registerMemoryApprove(memory2) {
|
|
|
7288
7631
|
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) => {
|
|
7289
7632
|
const root = findProjectRoot14(opts.dir);
|
|
7290
7633
|
const paths = resolveHaivePaths11(root);
|
|
7291
|
-
if (!
|
|
7634
|
+
if (!existsSync34(paths.memoriesDir)) {
|
|
7292
7635
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7293
7636
|
process.exitCode = 1;
|
|
7294
7637
|
return;
|
|
7295
7638
|
}
|
|
7296
|
-
const all = await
|
|
7639
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7297
7640
|
if (opts.all || opts.pending) {
|
|
7298
7641
|
const candidates = all.filter((m) => {
|
|
7299
7642
|
const s = m.memory.frontmatter.status;
|
|
@@ -7347,7 +7690,7 @@ function registerMemoryApprove(memory2) {
|
|
|
7347
7690
|
|
|
7348
7691
|
// src/commands/memory-update.ts
|
|
7349
7692
|
import { writeFile as writeFile17 } from "fs/promises";
|
|
7350
|
-
import { existsSync as
|
|
7693
|
+
import { existsSync as existsSync35 } from "fs";
|
|
7351
7694
|
import path17 from "path";
|
|
7352
7695
|
import "commander";
|
|
7353
7696
|
import {
|
|
@@ -7359,12 +7702,12 @@ function registerMemoryUpdate(memory2) {
|
|
|
7359
7702
|
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) => {
|
|
7360
7703
|
const root = findProjectRoot15(opts.dir);
|
|
7361
7704
|
const paths = resolveHaivePaths12(root);
|
|
7362
|
-
if (!
|
|
7705
|
+
if (!existsSync35(paths.memoriesDir)) {
|
|
7363
7706
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
7364
7707
|
process.exitCode = 1;
|
|
7365
7708
|
return;
|
|
7366
7709
|
}
|
|
7367
|
-
const memories = await
|
|
7710
|
+
const memories = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7368
7711
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
7369
7712
|
if (!loaded) {
|
|
7370
7713
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -7431,7 +7774,7 @@ function parseCsv3(value) {
|
|
|
7431
7774
|
|
|
7432
7775
|
// src/commands/memory-auto-promote.ts
|
|
7433
7776
|
import { writeFile as writeFile18 } from "fs/promises";
|
|
7434
|
-
import { existsSync as
|
|
7777
|
+
import { existsSync as existsSync36 } from "fs";
|
|
7435
7778
|
import path18 from "path";
|
|
7436
7779
|
import "commander";
|
|
7437
7780
|
import {
|
|
@@ -7451,7 +7794,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
7451
7794
|
).option("--apply", "actually write status=validated to disk (default: dry-run)").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
7452
7795
|
const root = findProjectRoot16(opts.dir);
|
|
7453
7796
|
const paths = resolveHaivePaths13(root);
|
|
7454
|
-
if (!
|
|
7797
|
+
if (!existsSync36(paths.memoriesDir)) {
|
|
7455
7798
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7456
7799
|
process.exitCode = 1;
|
|
7457
7800
|
return;
|
|
@@ -7460,7 +7803,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
7460
7803
|
minReads: Number(opts.minReads ?? DEFAULT_AUTO_PROMOTE_RULE3.minReads),
|
|
7461
7804
|
maxRejections: Number(opts.maxRejections ?? DEFAULT_AUTO_PROMOTE_RULE3.maxRejections)
|
|
7462
7805
|
};
|
|
7463
|
-
const memories = await
|
|
7806
|
+
const memories = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7464
7807
|
const usage = await loadUsageIndex13(paths);
|
|
7465
7808
|
const eligible = memories.filter(
|
|
7466
7809
|
({ memory: memory3 }) => isAutoPromoteEligible3(memory3.frontmatter, getUsage11(usage, memory3.frontmatter.id), rule)
|
|
@@ -7494,7 +7837,7 @@ function registerMemoryAutoPromote(memory2) {
|
|
|
7494
7837
|
|
|
7495
7838
|
// src/commands/memory-edit.ts
|
|
7496
7839
|
import { spawn as spawn3 } from "child_process";
|
|
7497
|
-
import { existsSync as
|
|
7840
|
+
import { existsSync as existsSync37 } from "fs";
|
|
7498
7841
|
import { readFile as readFile10 } from "fs/promises";
|
|
7499
7842
|
import path19 from "path";
|
|
7500
7843
|
import "commander";
|
|
@@ -7507,12 +7850,12 @@ function registerMemoryEdit(memory2) {
|
|
|
7507
7850
|
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) => {
|
|
7508
7851
|
const root = findProjectRoot17(opts.dir);
|
|
7509
7852
|
const paths = resolveHaivePaths14(root);
|
|
7510
|
-
if (!
|
|
7853
|
+
if (!existsSync37(paths.memoriesDir)) {
|
|
7511
7854
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7512
7855
|
process.exitCode = 1;
|
|
7513
7856
|
return;
|
|
7514
7857
|
}
|
|
7515
|
-
const all = await
|
|
7858
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7516
7859
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
7517
7860
|
if (!found) {
|
|
7518
7861
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -7547,7 +7890,7 @@ function runEditor(editor, file) {
|
|
|
7547
7890
|
}
|
|
7548
7891
|
|
|
7549
7892
|
// src/commands/memory-for-files.ts
|
|
7550
|
-
import { existsSync as
|
|
7893
|
+
import { existsSync as existsSync38 } from "fs";
|
|
7551
7894
|
import path20 from "path";
|
|
7552
7895
|
import "commander";
|
|
7553
7896
|
import {
|
|
@@ -7563,12 +7906,12 @@ function registerMemoryForFiles(memory2) {
|
|
|
7563
7906
|
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) => {
|
|
7564
7907
|
const root = findProjectRoot18(opts.dir);
|
|
7565
7908
|
const paths = resolveHaivePaths15(root);
|
|
7566
|
-
if (!
|
|
7909
|
+
if (!existsSync38(paths.memoriesDir)) {
|
|
7567
7910
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7568
7911
|
process.exitCode = 1;
|
|
7569
7912
|
return;
|
|
7570
7913
|
}
|
|
7571
|
-
const all = await
|
|
7914
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7572
7915
|
const usage = await loadUsageIndex14(paths);
|
|
7573
7916
|
const inferred = inferModulesFromPaths4(files);
|
|
7574
7917
|
const byAnchor = [];
|
|
@@ -7675,7 +8018,7 @@ function printGroup(root, label, loaded, usage) {
|
|
|
7675
8018
|
}
|
|
7676
8019
|
|
|
7677
8020
|
// src/commands/memory-hot.ts
|
|
7678
|
-
import { existsSync as
|
|
8021
|
+
import { existsSync as existsSync39 } from "fs";
|
|
7679
8022
|
import path21 from "path";
|
|
7680
8023
|
import "commander";
|
|
7681
8024
|
import {
|
|
@@ -7688,13 +8031,13 @@ function registerMemoryHot(memory2) {
|
|
|
7688
8031
|
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) => {
|
|
7689
8032
|
const root = findProjectRoot19(opts.dir);
|
|
7690
8033
|
const paths = resolveHaivePaths16(root);
|
|
7691
|
-
if (!
|
|
8034
|
+
if (!existsSync39(paths.memoriesDir)) {
|
|
7692
8035
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7693
8036
|
process.exitCode = 1;
|
|
7694
8037
|
return;
|
|
7695
8038
|
}
|
|
7696
8039
|
const threshold = Math.max(1, Number(opts.threshold ?? 3));
|
|
7697
|
-
const all = await
|
|
8040
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7698
8041
|
const usage = await loadUsageIndex15(paths);
|
|
7699
8042
|
const candidates = all.filter(({ memory: mem }) => {
|
|
7700
8043
|
const fm = mem.frontmatter;
|
|
@@ -7726,7 +8069,7 @@ function registerMemoryHot(memory2) {
|
|
|
7726
8069
|
|
|
7727
8070
|
// src/commands/memory-tried.ts
|
|
7728
8071
|
import { mkdir as mkdir11, writeFile as writeFile19 } from "fs/promises";
|
|
7729
|
-
import { existsSync as
|
|
8072
|
+
import { existsSync as existsSync40 } from "fs";
|
|
7730
8073
|
import path23 from "path";
|
|
7731
8074
|
import "commander";
|
|
7732
8075
|
import {
|
|
@@ -7755,7 +8098,7 @@ function registerMemoryTried(memory2) {
|
|
|
7755
8098
|
).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) => {
|
|
7756
8099
|
const root = findProjectRoot20(opts.dir);
|
|
7757
8100
|
const paths = resolveHaivePaths17(root);
|
|
7758
|
-
if (!
|
|
8101
|
+
if (!existsSync40(paths.haiveDir)) {
|
|
7759
8102
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
7760
8103
|
process.exitCode = 1;
|
|
7761
8104
|
return;
|
|
@@ -7779,7 +8122,7 @@ function registerMemoryTried(memory2) {
|
|
|
7779
8122
|
const body = lines.join("\n") + "\n";
|
|
7780
8123
|
const file = memoryFilePath8(paths, frontmatter.scope, frontmatter.id, frontmatter.module);
|
|
7781
8124
|
await mkdir11(path23.dirname(file), { recursive: true });
|
|
7782
|
-
if (
|
|
8125
|
+
if (existsSync40(file)) {
|
|
7783
8126
|
ui.error(`Memory already exists at ${file}`);
|
|
7784
8127
|
process.exitCode = 1;
|
|
7785
8128
|
return;
|
|
@@ -7795,7 +8138,7 @@ function parseCsv4(value) {
|
|
|
7795
8138
|
}
|
|
7796
8139
|
|
|
7797
8140
|
// src/commands/memory-pending.ts
|
|
7798
|
-
import { existsSync as
|
|
8141
|
+
import { existsSync as existsSync41 } from "fs";
|
|
7799
8142
|
import path24 from "path";
|
|
7800
8143
|
import "commander";
|
|
7801
8144
|
import {
|
|
@@ -7808,12 +8151,12 @@ function registerMemoryPending(memory2) {
|
|
|
7808
8151
|
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) => {
|
|
7809
8152
|
const root = findProjectRoot21(opts.dir);
|
|
7810
8153
|
const paths = resolveHaivePaths18(root);
|
|
7811
|
-
if (!
|
|
8154
|
+
if (!existsSync41(paths.memoriesDir)) {
|
|
7812
8155
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7813
8156
|
process.exitCode = 1;
|
|
7814
8157
|
return;
|
|
7815
8158
|
}
|
|
7816
|
-
const all = await
|
|
8159
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7817
8160
|
const usage = await loadUsageIndex16(paths);
|
|
7818
8161
|
const proposed = all.filter(({ memory: mem }) => {
|
|
7819
8162
|
if (mem.frontmatter.status !== "proposed") return false;
|
|
@@ -7843,7 +8186,7 @@ function registerMemoryPending(memory2) {
|
|
|
7843
8186
|
}
|
|
7844
8187
|
|
|
7845
8188
|
// src/commands/memory-query.ts
|
|
7846
|
-
import { existsSync as
|
|
8189
|
+
import { existsSync as existsSync43 } from "fs";
|
|
7847
8190
|
import path25 from "path";
|
|
7848
8191
|
import "commander";
|
|
7849
8192
|
import {
|
|
@@ -7860,7 +8203,7 @@ function registerMemoryQuery(memory2) {
|
|
|
7860
8203
|
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) => {
|
|
7861
8204
|
const root = findProjectRoot22(opts.dir);
|
|
7862
8205
|
const paths = resolveHaivePaths19(root);
|
|
7863
|
-
if (!
|
|
8206
|
+
if (!existsSync43(paths.memoriesDir)) {
|
|
7864
8207
|
ui.error(`No memories directory at ${paths.memoriesDir}. Run \`haive init\` first.`);
|
|
7865
8208
|
process.exitCode = 1;
|
|
7866
8209
|
return;
|
|
@@ -7871,7 +8214,7 @@ function registerMemoryQuery(memory2) {
|
|
|
7871
8214
|
return;
|
|
7872
8215
|
}
|
|
7873
8216
|
const statusFilter = opts.status ? opts.status.split(",").map((s) => s.trim()) : null;
|
|
7874
|
-
const all = await
|
|
8217
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7875
8218
|
const passesFilters2 = (mem) => {
|
|
7876
8219
|
const fm = mem.frontmatter;
|
|
7877
8220
|
if (opts.scope && fm.scope !== opts.scope) return false;
|
|
@@ -7919,7 +8262,7 @@ ${top.length} of ${matches.length} match${matches.length === 1 ? "" : "es"}`)
|
|
|
7919
8262
|
|
|
7920
8263
|
// src/commands/memory-reject.ts
|
|
7921
8264
|
import { writeFile as writeFile20 } from "fs/promises";
|
|
7922
|
-
import { existsSync as
|
|
8265
|
+
import { existsSync as existsSync44 } from "fs";
|
|
7923
8266
|
import "commander";
|
|
7924
8267
|
import {
|
|
7925
8268
|
findProjectRoot as findProjectRoot23,
|
|
@@ -7933,12 +8276,12 @@ function registerMemoryReject(memory2) {
|
|
|
7933
8276
|
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) => {
|
|
7934
8277
|
const root = findProjectRoot23(opts.dir);
|
|
7935
8278
|
const paths = resolveHaivePaths20(root);
|
|
7936
|
-
if (!
|
|
8279
|
+
if (!existsSync44(paths.memoriesDir)) {
|
|
7937
8280
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7938
8281
|
process.exitCode = 1;
|
|
7939
8282
|
return;
|
|
7940
8283
|
}
|
|
7941
|
-
const memories = await
|
|
8284
|
+
const memories = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7942
8285
|
const loaded = memories.find((m) => m.memory.frontmatter.id === id);
|
|
7943
8286
|
if (!loaded) {
|
|
7944
8287
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -7969,7 +8312,7 @@ function registerMemoryReject(memory2) {
|
|
|
7969
8312
|
}
|
|
7970
8313
|
|
|
7971
8314
|
// src/commands/memory-rm.ts
|
|
7972
|
-
import { existsSync as
|
|
8315
|
+
import { existsSync as existsSync45 } from "fs";
|
|
7973
8316
|
import { unlink as unlink3 } from "fs/promises";
|
|
7974
8317
|
import path26 from "path";
|
|
7975
8318
|
import { createInterface } from "readline/promises";
|
|
@@ -7984,12 +8327,12 @@ function registerMemoryRm(memory2) {
|
|
|
7984
8327
|
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) => {
|
|
7985
8328
|
const root = findProjectRoot24(opts.dir);
|
|
7986
8329
|
const paths = resolveHaivePaths21(root);
|
|
7987
|
-
if (!
|
|
8330
|
+
if (!existsSync45(paths.memoriesDir)) {
|
|
7988
8331
|
ui.error(`No .ai/memories at ${root}.`);
|
|
7989
8332
|
process.exitCode = 1;
|
|
7990
8333
|
return;
|
|
7991
8334
|
}
|
|
7992
|
-
const all = await
|
|
8335
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
7993
8336
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
7994
8337
|
if (!found) {
|
|
7995
8338
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8020,7 +8363,7 @@ function registerMemoryRm(memory2) {
|
|
|
8020
8363
|
}
|
|
8021
8364
|
|
|
8022
8365
|
// src/commands/memory-show.ts
|
|
8023
|
-
import { existsSync as
|
|
8366
|
+
import { existsSync as existsSync46 } from "fs";
|
|
8024
8367
|
import { readFile as readFile11 } from "fs/promises";
|
|
8025
8368
|
import path27 from "path";
|
|
8026
8369
|
import "commander";
|
|
@@ -8035,12 +8378,12 @@ function registerMemoryShow(memory2) {
|
|
|
8035
8378
|
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) => {
|
|
8036
8379
|
const root = findProjectRoot25(opts.dir);
|
|
8037
8380
|
const paths = resolveHaivePaths22(root);
|
|
8038
|
-
if (!
|
|
8381
|
+
if (!existsSync46(paths.memoriesDir)) {
|
|
8039
8382
|
ui.error(`No .ai/memories at ${root}.`);
|
|
8040
8383
|
process.exitCode = 1;
|
|
8041
8384
|
return;
|
|
8042
8385
|
}
|
|
8043
|
-
const all = await
|
|
8386
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8044
8387
|
const found = all.find((m) => m.memory.frontmatter.id === id);
|
|
8045
8388
|
if (!found) {
|
|
8046
8389
|
ui.error(`No memory with id "${id}".`);
|
|
@@ -8079,7 +8422,7 @@ function registerMemoryShow(memory2) {
|
|
|
8079
8422
|
}
|
|
8080
8423
|
|
|
8081
8424
|
// src/commands/memory-stats.ts
|
|
8082
|
-
import { existsSync as
|
|
8425
|
+
import { existsSync as existsSync47 } from "fs";
|
|
8083
8426
|
import path28 from "path";
|
|
8084
8427
|
import "commander";
|
|
8085
8428
|
import {
|
|
@@ -8093,12 +8436,12 @@ function registerMemoryStats(memory2) {
|
|
|
8093
8436
|
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) => {
|
|
8094
8437
|
const root = findProjectRoot26(opts.dir);
|
|
8095
8438
|
const paths = resolveHaivePaths23(root);
|
|
8096
|
-
if (!
|
|
8439
|
+
if (!existsSync47(paths.memoriesDir)) {
|
|
8097
8440
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8098
8441
|
process.exitCode = 1;
|
|
8099
8442
|
return;
|
|
8100
8443
|
}
|
|
8101
|
-
const all = await
|
|
8444
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8102
8445
|
const usage = await loadUsageIndex20(paths);
|
|
8103
8446
|
const target = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
8104
8447
|
if (target.length === 0) {
|
|
@@ -8125,7 +8468,7 @@ function registerMemoryStats(memory2) {
|
|
|
8125
8468
|
|
|
8126
8469
|
// src/commands/memory-verify.ts
|
|
8127
8470
|
import { writeFile as writeFile21 } from "fs/promises";
|
|
8128
|
-
import { existsSync as
|
|
8471
|
+
import { existsSync as existsSync48 } from "fs";
|
|
8129
8472
|
import path29 from "path";
|
|
8130
8473
|
import "commander";
|
|
8131
8474
|
import {
|
|
@@ -8140,12 +8483,12 @@ function registerMemoryVerify(memory2) {
|
|
|
8140
8483
|
).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) => {
|
|
8141
8484
|
const root = findProjectRoot27(opts.dir);
|
|
8142
8485
|
const paths = resolveHaivePaths24(root);
|
|
8143
|
-
if (!
|
|
8486
|
+
if (!existsSync48(paths.memoriesDir)) {
|
|
8144
8487
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
8145
8488
|
process.exitCode = 1;
|
|
8146
8489
|
return;
|
|
8147
8490
|
}
|
|
8148
|
-
const all = await
|
|
8491
|
+
const all = await loadMemoriesFromDir25(paths.memoriesDir);
|
|
8149
8492
|
const targets = opts.id ? all.filter((m) => m.memory.frontmatter.id === opts.id) : all;
|
|
8150
8493
|
if (opts.id && targets.length === 0) {
|
|
8151
8494
|
ui.error(`No memory with id "${opts.id}".`);
|
|
@@ -8227,7 +8570,7 @@ function applyVerification2(mem, result) {
|
|
|
8227
8570
|
|
|
8228
8571
|
// src/commands/memory-import.ts
|
|
8229
8572
|
import { readFile as readFile12 } from "fs/promises";
|
|
8230
|
-
import { existsSync as
|
|
8573
|
+
import { existsSync as existsSync49 } from "fs";
|
|
8231
8574
|
import "commander";
|
|
8232
8575
|
import {
|
|
8233
8576
|
findProjectRoot as findProjectRoot28,
|
|
@@ -8239,12 +8582,12 @@ function registerMemoryImport(memory2) {
|
|
|
8239
8582
|
).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) => {
|
|
8240
8583
|
const root = findProjectRoot28(opts.dir);
|
|
8241
8584
|
const paths = resolveHaivePaths25(root);
|
|
8242
|
-
if (!
|
|
8585
|
+
if (!existsSync49(paths.haiveDir)) {
|
|
8243
8586
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8244
8587
|
process.exitCode = 1;
|
|
8245
8588
|
return;
|
|
8246
8589
|
}
|
|
8247
|
-
if (!
|
|
8590
|
+
if (!existsSync49(opts.from)) {
|
|
8248
8591
|
ui.error(`File not found: ${opts.from}`);
|
|
8249
8592
|
process.exitCode = 1;
|
|
8250
8593
|
return;
|
|
@@ -8277,7 +8620,7 @@ function registerMemoryImport(memory2) {
|
|
|
8277
8620
|
}
|
|
8278
8621
|
|
|
8279
8622
|
// src/commands/memory-import-changelog.ts
|
|
8280
|
-
import { existsSync as
|
|
8623
|
+
import { existsSync as existsSync50 } from "fs";
|
|
8281
8624
|
import { readFile as readFile13, mkdir as mkdir12, writeFile as writeFile23 } from "fs/promises";
|
|
8282
8625
|
import path30 from "path";
|
|
8283
8626
|
import "commander";
|
|
@@ -8351,7 +8694,7 @@ function registerMemoryImportChangelog(memory2) {
|
|
|
8351
8694
|
const root = findProjectRoot29(opts.dir);
|
|
8352
8695
|
const paths = resolveHaivePaths26(root);
|
|
8353
8696
|
const changelogPath = path30.resolve(root, opts.fromChangelog);
|
|
8354
|
-
if (!
|
|
8697
|
+
if (!existsSync50(changelogPath)) {
|
|
8355
8698
|
ui.error(`CHANGELOG not found: ${changelogPath}`);
|
|
8356
8699
|
process.exitCode = 1;
|
|
8357
8700
|
return;
|
|
@@ -8437,7 +8780,7 @@ ${ui.bold(`Imported ${saved} changelog entr${saved === 1 ? "y" : "ies"} from ${p
|
|
|
8437
8780
|
}
|
|
8438
8781
|
|
|
8439
8782
|
// src/commands/memory-digest.ts
|
|
8440
|
-
import { existsSync as
|
|
8783
|
+
import { existsSync as existsSync51 } from "fs";
|
|
8441
8784
|
import { writeFile as writeFile24 } from "fs/promises";
|
|
8442
8785
|
import path31 from "path";
|
|
8443
8786
|
import "commander";
|
|
@@ -8445,7 +8788,7 @@ import {
|
|
|
8445
8788
|
deriveConfidence as deriveConfidence12,
|
|
8446
8789
|
findProjectRoot as findProjectRoot30,
|
|
8447
8790
|
getUsage as getUsage17,
|
|
8448
|
-
loadMemoriesFromDir as
|
|
8791
|
+
loadMemoriesFromDir as loadMemoriesFromDir26,
|
|
8449
8792
|
loadUsageIndex as loadUsageIndex21,
|
|
8450
8793
|
resolveHaivePaths as resolveHaivePaths27
|
|
8451
8794
|
} from "@hiveai/core";
|
|
@@ -8462,7 +8805,7 @@ function registerMemoryDigest(program2) {
|
|
|
8462
8805
|
).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) => {
|
|
8463
8806
|
const root = findProjectRoot30(opts.dir);
|
|
8464
8807
|
const paths = resolveHaivePaths27(root);
|
|
8465
|
-
if (!
|
|
8808
|
+
if (!existsSync51(paths.memoriesDir)) {
|
|
8466
8809
|
ui.error("No .ai/memories found. Run `haive init` first.");
|
|
8467
8810
|
process.exitCode = 1;
|
|
8468
8811
|
return;
|
|
@@ -8470,7 +8813,7 @@ function registerMemoryDigest(program2) {
|
|
|
8470
8813
|
const days = Math.max(1, Number(opts.days ?? 7));
|
|
8471
8814
|
const scopeFilter = opts.scope ?? "team";
|
|
8472
8815
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
8473
|
-
const all = await
|
|
8816
|
+
const all = await loadMemoriesFromDir26(paths.memoriesDir);
|
|
8474
8817
|
const usage = await loadUsageIndex21(paths);
|
|
8475
8818
|
const recent = all.filter(({ memory: mem }) => {
|
|
8476
8819
|
const fm = mem.frontmatter;
|
|
@@ -8545,20 +8888,20 @@ function registerMemoryDigest(program2) {
|
|
|
8545
8888
|
|
|
8546
8889
|
// src/commands/session-end.ts
|
|
8547
8890
|
import { writeFile as writeFile25, mkdir as mkdir13, readFile as readFile14, rm as rm2 } from "fs/promises";
|
|
8548
|
-
import { existsSync as
|
|
8891
|
+
import { existsSync as existsSync53 } from "fs";
|
|
8549
8892
|
import path33 from "path";
|
|
8550
8893
|
import "commander";
|
|
8551
8894
|
import {
|
|
8552
8895
|
buildFrontmatter as buildFrontmatter10,
|
|
8553
8896
|
findProjectRoot as findProjectRoot31,
|
|
8554
|
-
loadMemoriesFromDir as
|
|
8897
|
+
loadMemoriesFromDir as loadMemoriesFromDir27,
|
|
8555
8898
|
memoryFilePath as memoryFilePath9,
|
|
8556
8899
|
resolveHaivePaths as resolveHaivePaths28,
|
|
8557
8900
|
serializeMemory as serializeMemory21
|
|
8558
8901
|
} from "@hiveai/core";
|
|
8559
8902
|
async function buildAutoRecap(paths) {
|
|
8560
8903
|
const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
8561
|
-
if (!
|
|
8904
|
+
if (!existsSync53(obsFile)) return null;
|
|
8562
8905
|
const raw = await readFile14(obsFile, "utf8").catch(() => "");
|
|
8563
8906
|
if (!raw.trim()) return null;
|
|
8564
8907
|
const lines = raw.split("\n").filter(Boolean);
|
|
@@ -8639,7 +8982,7 @@ function registerSessionEnd(session2) {
|
|
|
8639
8982
|
).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) => {
|
|
8640
8983
|
const root = findProjectRoot31(opts.dir);
|
|
8641
8984
|
const paths = resolveHaivePaths28(root);
|
|
8642
|
-
if (!
|
|
8985
|
+
if (!existsSync53(paths.haiveDir)) {
|
|
8643
8986
|
if (opts.auto || opts.quiet) return;
|
|
8644
8987
|
ui.error(`No .ai/ found at ${root}. Run \`haive init\` first.`);
|
|
8645
8988
|
process.exitCode = 1;
|
|
@@ -8671,7 +9014,7 @@ function registerSessionEnd(session2) {
|
|
|
8671
9014
|
});
|
|
8672
9015
|
const topic = recapTopic2(scope, opts.module);
|
|
8673
9016
|
const filesTouched = parseCsv5(resolvedFiles);
|
|
8674
|
-
const missingPaths = filesTouched.filter((p) => !
|
|
9017
|
+
const missingPaths = filesTouched.filter((p) => !existsSync53(path33.resolve(root, p)));
|
|
8675
9018
|
if (missingPaths.length > 0 && !opts.quiet) {
|
|
8676
9019
|
ui.warn(`Anchor path${missingPaths.length > 1 ? "s" : ""} not found in project (will be stale):`);
|
|
8677
9020
|
for (const p of missingPaths) ui.warn(` \u2717 ${p}`);
|
|
@@ -8679,11 +9022,11 @@ function registerSessionEnd(session2) {
|
|
|
8679
9022
|
const cleanupObservations = async () => {
|
|
8680
9023
|
if (!opts.auto) return;
|
|
8681
9024
|
const obsFile = path33.join(paths.haiveDir, ".cache", "observations.jsonl");
|
|
8682
|
-
if (
|
|
9025
|
+
if (existsSync53(obsFile)) await rm2(obsFile).catch(() => {
|
|
8683
9026
|
});
|
|
8684
9027
|
};
|
|
8685
|
-
if (
|
|
8686
|
-
const existing = await
|
|
9028
|
+
if (existsSync53(paths.memoriesDir)) {
|
|
9029
|
+
const existing = await loadMemoriesFromDir27(paths.memoriesDir);
|
|
8687
9030
|
const topicMatch = existing.find(
|
|
8688
9031
|
({ memory: memory2 }) => memory2.frontmatter.topic === topic && memory2.frontmatter.scope === scope && (!opts.module || memory2.frontmatter.module === opts.module)
|
|
8689
9032
|
);
|
|
@@ -8703,6 +9046,7 @@ function registerSessionEnd(session2) {
|
|
|
8703
9046
|
if (!opts.quiet) {
|
|
8704
9047
|
ui.success(`Session recap updated (revision #${revisionCount})`);
|
|
8705
9048
|
ui.info(`id=${fm.id} file=${path33.relative(root, topicMatch.filePath)}`);
|
|
9049
|
+
ui.info("Tip: `haive stats --export-report` generates a usage JSON suitable for dashboards.");
|
|
8706
9050
|
}
|
|
8707
9051
|
return;
|
|
8708
9052
|
}
|
|
@@ -8725,6 +9069,7 @@ function registerSessionEnd(session2) {
|
|
|
8725
9069
|
ui.success(`Session recap created`);
|
|
8726
9070
|
ui.info(`id=${frontmatter.id} scope=${scope} file=${path33.relative(root, file)}`);
|
|
8727
9071
|
ui.info("Next session: call `get_briefing` \u2014 the recap will be surfaced automatically.");
|
|
9072
|
+
ui.info("Tip: export a local MCP usage rollup with `haive stats --export-report .ai/tool-usage-roi-report.json`.");
|
|
8728
9073
|
}
|
|
8729
9074
|
});
|
|
8730
9075
|
}
|
|
@@ -8734,7 +9079,7 @@ function parseCsv5(value) {
|
|
|
8734
9079
|
}
|
|
8735
9080
|
|
|
8736
9081
|
// src/commands/snapshot.ts
|
|
8737
|
-
import { existsSync as
|
|
9082
|
+
import { existsSync as existsSync54 } from "fs";
|
|
8738
9083
|
import { readdir as readdir4 } from "fs/promises";
|
|
8739
9084
|
import path34 from "path";
|
|
8740
9085
|
import "commander";
|
|
@@ -8769,14 +9114,14 @@ function registerSnapshot(program2) {
|
|
|
8769
9114
|
).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) => {
|
|
8770
9115
|
const root = findProjectRoot32(opts.dir);
|
|
8771
9116
|
const paths = resolveHaivePaths29(root);
|
|
8772
|
-
if (!
|
|
9117
|
+
if (!existsSync54(paths.haiveDir)) {
|
|
8773
9118
|
ui.error("No .ai/ found. Run `haive init` first.");
|
|
8774
9119
|
process.exitCode = 1;
|
|
8775
9120
|
return;
|
|
8776
9121
|
}
|
|
8777
9122
|
if (opts.list) {
|
|
8778
9123
|
const contractsDir = path34.join(paths.haiveDir, "contracts");
|
|
8779
|
-
if (!
|
|
9124
|
+
if (!existsSync54(contractsDir)) {
|
|
8780
9125
|
console.log(ui.dim("No contract snapshots found."));
|
|
8781
9126
|
return;
|
|
8782
9127
|
}
|
|
@@ -8900,7 +9245,7 @@ function detectFormat(filePath) {
|
|
|
8900
9245
|
}
|
|
8901
9246
|
|
|
8902
9247
|
// src/commands/hub.ts
|
|
8903
|
-
import { existsSync as
|
|
9248
|
+
import { existsSync as existsSync55 } from "fs";
|
|
8904
9249
|
import { mkdir as mkdir14, readFile as readFile15, writeFile as writeFile26, copyFile } from "fs/promises";
|
|
8905
9250
|
import path35 from "path";
|
|
8906
9251
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -8908,7 +9253,7 @@ import "commander";
|
|
|
8908
9253
|
import {
|
|
8909
9254
|
findProjectRoot as findProjectRoot33,
|
|
8910
9255
|
loadConfig as loadConfig6,
|
|
8911
|
-
loadMemoriesFromDir as
|
|
9256
|
+
loadMemoriesFromDir as loadMemoriesFromDir28,
|
|
8912
9257
|
resolveHaivePaths as resolveHaivePaths30,
|
|
8913
9258
|
saveConfig as saveConfig2,
|
|
8914
9259
|
serializeMemory as serializeMemory23
|
|
@@ -9002,7 +9347,7 @@ Next steps:
|
|
|
9002
9347
|
return;
|
|
9003
9348
|
}
|
|
9004
9349
|
const hubRoot = path35.resolve(root, config.hubPath);
|
|
9005
|
-
if (!
|
|
9350
|
+
if (!existsSync55(hubRoot)) {
|
|
9006
9351
|
ui.error(`Hub not found at ${hubRoot}. Run \`haive hub init ${config.hubPath}\` first.`);
|
|
9007
9352
|
process.exitCode = 1;
|
|
9008
9353
|
return;
|
|
@@ -9010,7 +9355,7 @@ Next steps:
|
|
|
9010
9355
|
const projectName = path35.basename(root);
|
|
9011
9356
|
const destDir = path35.join(hubRoot, ".ai", "memories", "shared", projectName);
|
|
9012
9357
|
await mkdir14(destDir, { recursive: true });
|
|
9013
|
-
const all = await
|
|
9358
|
+
const all = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9014
9359
|
const shared = all.filter(
|
|
9015
9360
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && // Don't push imported memories (avoid echo loops)
|
|
9016
9361
|
!memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
@@ -9072,7 +9417,7 @@ Next steps:
|
|
|
9072
9417
|
}
|
|
9073
9418
|
const hubRoot = path35.resolve(root, config.hubPath);
|
|
9074
9419
|
const hubSharedDir = path35.join(hubRoot, ".ai", "memories", "shared");
|
|
9075
|
-
if (!
|
|
9420
|
+
if (!existsSync55(hubSharedDir)) {
|
|
9076
9421
|
ui.warn("Hub has no shared memories yet. Run `haive hub push` from other projects first.");
|
|
9077
9422
|
return;
|
|
9078
9423
|
}
|
|
@@ -9127,7 +9472,7 @@ Next steps:
|
|
|
9127
9472
|
` hubPath: ${config.hubPath ? ui.green(config.hubPath) : ui.dim("not configured")}`
|
|
9128
9473
|
);
|
|
9129
9474
|
const sharedDir = path35.join(paths.memoriesDir, "shared");
|
|
9130
|
-
if (
|
|
9475
|
+
if (existsSync55(sharedDir)) {
|
|
9131
9476
|
const { readdir: readdir5 } = await import("fs/promises");
|
|
9132
9477
|
const sources = (await readdir5(sharedDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
9133
9478
|
console.log(`
|
|
@@ -9139,7 +9484,7 @@ Next steps:
|
|
|
9139
9484
|
} else {
|
|
9140
9485
|
console.log(ui.dim(" No imported shared memories yet."));
|
|
9141
9486
|
}
|
|
9142
|
-
const all = await
|
|
9487
|
+
const all = await loadMemoriesFromDir28(paths.memoriesDir);
|
|
9143
9488
|
const outgoing = all.filter(
|
|
9144
9489
|
({ memory: memory2 }) => memory2.frontmatter.scope === "shared" && !memory2.frontmatter.tags.some((t) => t.startsWith("cross-repo:"))
|
|
9145
9490
|
);
|
|
@@ -9156,9 +9501,13 @@ Next steps:
|
|
|
9156
9501
|
|
|
9157
9502
|
// src/commands/stats.ts
|
|
9158
9503
|
import "commander";
|
|
9504
|
+
import { existsSync as existsSync56 } from "fs";
|
|
9505
|
+
import { mkdir as mkdir15, writeFile as writeFile27 } from "fs/promises";
|
|
9506
|
+
import path36 from "path";
|
|
9159
9507
|
import {
|
|
9160
9508
|
aggregateUsage,
|
|
9161
9509
|
findProjectRoot as findProjectRoot34,
|
|
9510
|
+
loadMemoriesFromDir as loadMemoriesFromDir29,
|
|
9162
9511
|
loadUsageIndex as loadUsageIndex23,
|
|
9163
9512
|
parseSince,
|
|
9164
9513
|
readUsageEvents as readUsageEvents2,
|
|
@@ -9166,9 +9515,17 @@ import {
|
|
|
9166
9515
|
usageLogSize
|
|
9167
9516
|
} from "@hiveai/core";
|
|
9168
9517
|
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(
|
|
9518
|
+
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(
|
|
9519
|
+
"--export-report <path>",
|
|
9520
|
+
"write a JSON rollup (tools + briefing counts + heuristic ROI hints). Parent dirs are created if needed.",
|
|
9521
|
+
void 0
|
|
9522
|
+
).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
9170
9523
|
const root = findProjectRoot34(opts.dir);
|
|
9171
9524
|
const paths = resolveHaivePaths31(root);
|
|
9525
|
+
if (opts.exportReport) {
|
|
9526
|
+
await writeRoiReport(paths, root, opts.since ?? "30d", opts.exportReport);
|
|
9527
|
+
return;
|
|
9528
|
+
}
|
|
9172
9529
|
if (opts.memoryHits) {
|
|
9173
9530
|
await renderMemoryHits(paths, opts);
|
|
9174
9531
|
return;
|
|
@@ -9217,6 +9574,57 @@ function registerStats(program2) {
|
|
|
9217
9574
|
}
|
|
9218
9575
|
});
|
|
9219
9576
|
}
|
|
9577
|
+
async function writeRoiReport(paths, root, sinceRaw, outRelative) {
|
|
9578
|
+
const outAbs = path36.isAbsolute(outRelative) ? path36.resolve(outRelative) : path36.resolve(root, outRelative);
|
|
9579
|
+
const size = await usageLogSize(paths);
|
|
9580
|
+
let events = await readUsageEvents2(paths);
|
|
9581
|
+
let memoryCount = { team: 0, personal: 0, total_skipped_session: 0 };
|
|
9582
|
+
if (existsSync56(paths.memoriesDir)) {
|
|
9583
|
+
const mems = await loadMemoriesFromDir29(paths.memoriesDir);
|
|
9584
|
+
for (const { memory: memory2 } of mems) {
|
|
9585
|
+
const fm = memory2.frontmatter;
|
|
9586
|
+
if (fm.type === "session_recap") memoryCount.total_skipped_session++;
|
|
9587
|
+
else if (fm.scope === "team") memoryCount.team++;
|
|
9588
|
+
else if (fm.scope === "personal") memoryCount.personal++;
|
|
9589
|
+
}
|
|
9590
|
+
}
|
|
9591
|
+
const sinceDt = parseSince(sinceRaw) ?? void 0;
|
|
9592
|
+
const aggregate = aggregateUsage(events, sinceDt);
|
|
9593
|
+
const inWindow = (at) => sinceDt === void 0 || Date.parse(at) >= sinceDt.getTime();
|
|
9594
|
+
const briefingCalls = events.filter((e) => inWindow(e.at) && e.tool === "get_briefing").length;
|
|
9595
|
+
let memoryHitsLeader = null;
|
|
9596
|
+
try {
|
|
9597
|
+
const usageIdx = await loadUsageIndex23(paths);
|
|
9598
|
+
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);
|
|
9599
|
+
memoryHitsLeader = tops[0] ?? null;
|
|
9600
|
+
} catch {
|
|
9601
|
+
memoryHitsLeader = null;
|
|
9602
|
+
}
|
|
9603
|
+
const roiHints = [
|
|
9604
|
+
"Correlate get_briefing calls with skipped multi-file exploration \u2014 proxies available via `pnpm benchmark:roi` at repo root.",
|
|
9605
|
+
"Prefer get_briefing(format:'actions') or budget_preset:'quick' for low-risk edits to reduce token pressure.",
|
|
9606
|
+
"Run `haive memory lint` in CI to keep the corpus actionable.",
|
|
9607
|
+
"Install the haive VS Code extension (packages/vscode) for always-on memory surfacing beside the editor."
|
|
9608
|
+
];
|
|
9609
|
+
if (!size.exists || events.length === 0) {
|
|
9610
|
+
ui.warn("Usage log missing or empty \u2014 report still written with partial data.");
|
|
9611
|
+
events = [];
|
|
9612
|
+
}
|
|
9613
|
+
await mkdir15(path36.dirname(outAbs), { recursive: true });
|
|
9614
|
+
const payload = {
|
|
9615
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9616
|
+
project_root: root,
|
|
9617
|
+
window_since: sinceRaw,
|
|
9618
|
+
usage_log_meta: size,
|
|
9619
|
+
memory_inventory: memoryCount,
|
|
9620
|
+
aggregate,
|
|
9621
|
+
get_briefing_calls_in_window: briefingCalls,
|
|
9622
|
+
top_memory_reads: memoryHitsLeader,
|
|
9623
|
+
roi_hints: roiHints
|
|
9624
|
+
};
|
|
9625
|
+
await writeFile27(outAbs, JSON.stringify(payload, null, 2), "utf8");
|
|
9626
|
+
ui.success(`Wrote ROI / usage rollup \u2192 ${outAbs}`);
|
|
9627
|
+
}
|
|
9220
9628
|
async function renderMemoryHits(paths, opts) {
|
|
9221
9629
|
const index = await loadUsageIndex23(paths);
|
|
9222
9630
|
const since = parseSince(opts.since ?? "30d");
|
|
@@ -9391,15 +9799,15 @@ function summarize(name, t0, payload, notes) {
|
|
|
9391
9799
|
}
|
|
9392
9800
|
|
|
9393
9801
|
// src/commands/memory-suggest.ts
|
|
9394
|
-
import { mkdir as
|
|
9395
|
-
import { existsSync as
|
|
9396
|
-
import
|
|
9802
|
+
import { mkdir as mkdir16, writeFile as writeFile28 } from "fs/promises";
|
|
9803
|
+
import { existsSync as existsSync57 } from "fs";
|
|
9804
|
+
import path37 from "path";
|
|
9397
9805
|
import "commander";
|
|
9398
9806
|
import {
|
|
9399
9807
|
aggregateUsage as aggregateUsage2,
|
|
9400
9808
|
buildFrontmatter as buildFrontmatter11,
|
|
9401
9809
|
findProjectRoot as findProjectRoot36,
|
|
9402
|
-
loadMemoriesFromDir as
|
|
9810
|
+
loadMemoriesFromDir as loadMemoriesFromDir30,
|
|
9403
9811
|
memoryFilePath as memoryFilePath10,
|
|
9404
9812
|
parseSince as parseSince2,
|
|
9405
9813
|
readUsageEvents as readUsageEvents3,
|
|
@@ -9463,7 +9871,7 @@ function registerMemorySuggest(memory2) {
|
|
|
9463
9871
|
}
|
|
9464
9872
|
const created = [];
|
|
9465
9873
|
const skipped = [];
|
|
9466
|
-
const existing =
|
|
9874
|
+
const existing = existsSync57(paths.memoriesDir) ? await loadMemoriesFromDir30(paths.memoriesDir) : [];
|
|
9467
9875
|
for (const s of top) {
|
|
9468
9876
|
const slug = slugify(s.query);
|
|
9469
9877
|
if (!slug) {
|
|
@@ -9486,13 +9894,13 @@ function registerMemorySuggest(memory2) {
|
|
|
9486
9894
|
fm.status = "draft";
|
|
9487
9895
|
const body = renderTemplate(s);
|
|
9488
9896
|
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 ${
|
|
9897
|
+
await mkdir16(path37.dirname(file), { recursive: true });
|
|
9898
|
+
if (existsSync57(file)) {
|
|
9899
|
+
skipped.push({ query: s.query, reason: `file already exists at ${path37.relative(root, file)}` });
|
|
9492
9900
|
continue;
|
|
9493
9901
|
}
|
|
9494
|
-
await
|
|
9495
|
-
created.push({ id: fm.id, file:
|
|
9902
|
+
await writeFile28(file, serializeMemory24({ frontmatter: fm, body }), "utf8");
|
|
9903
|
+
created.push({ id: fm.id, file: path37.relative(root, file), query: s.query });
|
|
9496
9904
|
}
|
|
9497
9905
|
if (opts.json) {
|
|
9498
9906
|
console.log(JSON.stringify({ created, skipped }, null, 2));
|
|
@@ -9585,14 +9993,14 @@ function truncate2(text, max) {
|
|
|
9585
9993
|
}
|
|
9586
9994
|
|
|
9587
9995
|
// src/commands/memory-archive.ts
|
|
9588
|
-
import { existsSync as
|
|
9589
|
-
import { writeFile as
|
|
9590
|
-
import
|
|
9996
|
+
import { existsSync as existsSync58 } from "fs";
|
|
9997
|
+
import { writeFile as writeFile29 } from "fs/promises";
|
|
9998
|
+
import path38 from "path";
|
|
9591
9999
|
import "commander";
|
|
9592
10000
|
import {
|
|
9593
10001
|
findProjectRoot as findProjectRoot37,
|
|
9594
10002
|
getUsage as getUsage18,
|
|
9595
|
-
loadMemoriesFromDir as
|
|
10003
|
+
loadMemoriesFromDir as loadMemoriesFromDir31,
|
|
9596
10004
|
loadUsageIndex as loadUsageIndex24,
|
|
9597
10005
|
resolveHaivePaths as resolveHaivePaths34,
|
|
9598
10006
|
serializeMemory as serializeMemory25
|
|
@@ -9604,7 +10012,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9604
10012
|
).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
10013
|
const root = findProjectRoot37(opts.dir);
|
|
9606
10014
|
const paths = resolveHaivePaths34(root);
|
|
9607
|
-
if (!
|
|
10015
|
+
if (!existsSync58(paths.memoriesDir)) {
|
|
9608
10016
|
ui.error(`No .ai/memories at ${root}. Run \`haive init\` first.`);
|
|
9609
10017
|
process.exitCode = 1;
|
|
9610
10018
|
return;
|
|
@@ -9616,7 +10024,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9616
10024
|
return;
|
|
9617
10025
|
}
|
|
9618
10026
|
const cutoff = Date.now() - minDays * MS_PER_DAY2;
|
|
9619
|
-
const all = await
|
|
10027
|
+
const all = await loadMemoriesFromDir31(paths.memoriesDir);
|
|
9620
10028
|
const usage = await loadUsageIndex24(paths);
|
|
9621
10029
|
const typeFilter = opts.type === "all" ? null : opts.type ?? "attempt";
|
|
9622
10030
|
const candidates = [];
|
|
@@ -9625,7 +10033,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9625
10033
|
if (typeFilter && fm.type !== typeFilter) continue;
|
|
9626
10034
|
if (fm.status === "deprecated" || fm.status === "rejected") continue;
|
|
9627
10035
|
const hasAnyAnchor = fm.anchor.paths.length + fm.anchor.symbols.length > 0;
|
|
9628
|
-
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !
|
|
10036
|
+
const allPathsGone = fm.anchor.paths.length > 0 && fm.anchor.paths.every((p) => !existsSync58(path38.join(paths.root, p)));
|
|
9629
10037
|
const isAnchorless = !hasAnyAnchor;
|
|
9630
10038
|
if (!isAnchorless && !allPathsGone) continue;
|
|
9631
10039
|
const u = getUsage18(usage, fm.id);
|
|
@@ -9673,7 +10081,7 @@ function registerMemoryArchive(memory2) {
|
|
|
9673
10081
|
if (!found) continue;
|
|
9674
10082
|
const fm = { ...found.memory.frontmatter, status: "deprecated" };
|
|
9675
10083
|
try {
|
|
9676
|
-
await
|
|
10084
|
+
await writeFile29(c.filePath, serializeMemory25({ frontmatter: fm, body: found.memory.body }), "utf8");
|
|
9677
10085
|
archived++;
|
|
9678
10086
|
} catch (err) {
|
|
9679
10087
|
if (!opts.json) {
|
|
@@ -9699,7 +10107,7 @@ function parseDays(input) {
|
|
|
9699
10107
|
}
|
|
9700
10108
|
|
|
9701
10109
|
// src/commands/doctor.ts
|
|
9702
|
-
import { existsSync as
|
|
10110
|
+
import { existsSync as existsSync59 } from "fs";
|
|
9703
10111
|
import { stat } from "fs/promises";
|
|
9704
10112
|
import "path";
|
|
9705
10113
|
import { execSync as execSync3 } from "child_process";
|
|
@@ -9710,7 +10118,7 @@ import {
|
|
|
9710
10118
|
getUsage as getUsage19,
|
|
9711
10119
|
loadCodeMap as loadCodeMap5,
|
|
9712
10120
|
loadConfig as loadConfig7,
|
|
9713
|
-
loadMemoriesFromDir as
|
|
10121
|
+
loadMemoriesFromDir as loadMemoriesFromDir32,
|
|
9714
10122
|
loadUsageIndex as loadUsageIndex25,
|
|
9715
10123
|
readUsageEvents as readUsageEvents4,
|
|
9716
10124
|
resolveHaivePaths as resolveHaivePaths35
|
|
@@ -9723,7 +10131,7 @@ function registerDoctor(program2) {
|
|
|
9723
10131
|
const root = findProjectRoot38(opts.dir);
|
|
9724
10132
|
const paths = resolveHaivePaths35(root);
|
|
9725
10133
|
const findings = [];
|
|
9726
|
-
if (!
|
|
10134
|
+
if (!existsSync59(paths.haiveDir)) {
|
|
9727
10135
|
findings.push({
|
|
9728
10136
|
severity: "error",
|
|
9729
10137
|
code: "not-initialized",
|
|
@@ -9732,7 +10140,7 @@ function registerDoctor(program2) {
|
|
|
9732
10140
|
});
|
|
9733
10141
|
return emit(findings, opts);
|
|
9734
10142
|
}
|
|
9735
|
-
if (!
|
|
10143
|
+
if (!existsSync59(paths.projectContext)) {
|
|
9736
10144
|
findings.push({
|
|
9737
10145
|
severity: "warn",
|
|
9738
10146
|
code: "no-project-context",
|
|
@@ -9752,7 +10160,7 @@ function registerDoctor(program2) {
|
|
|
9752
10160
|
});
|
|
9753
10161
|
}
|
|
9754
10162
|
}
|
|
9755
|
-
const memories =
|
|
10163
|
+
const memories = existsSync59(paths.memoriesDir) ? await loadMemoriesFromDir32(paths.memoriesDir) : [];
|
|
9756
10164
|
const now = Date.now();
|
|
9757
10165
|
if (memories.length === 0) {
|
|
9758
10166
|
findings.push({
|
|
@@ -9876,7 +10284,7 @@ function registerDoctor(program2) {
|
|
|
9876
10284
|
timeout: 3e3,
|
|
9877
10285
|
stdio: ["ignore", "pipe", "ignore"]
|
|
9878
10286
|
}).trim();
|
|
9879
|
-
const cliVersion = "0.9.
|
|
10287
|
+
const cliVersion = "0.9.5";
|
|
9880
10288
|
if (legacyRaw && legacyRaw !== cliVersion) {
|
|
9881
10289
|
findings.push({
|
|
9882
10290
|
severity: "warn",
|
|
@@ -9925,11 +10333,11 @@ function isSearchTool(name) {
|
|
|
9925
10333
|
}
|
|
9926
10334
|
|
|
9927
10335
|
// src/commands/playback.ts
|
|
9928
|
-
import { existsSync as
|
|
10336
|
+
import { existsSync as existsSync60 } from "fs";
|
|
9929
10337
|
import "commander";
|
|
9930
10338
|
import {
|
|
9931
10339
|
findProjectRoot as findProjectRoot39,
|
|
9932
|
-
loadMemoriesFromDir as
|
|
10340
|
+
loadMemoriesFromDir as loadMemoriesFromDir33,
|
|
9933
10341
|
parseSince as parseSince3,
|
|
9934
10342
|
readUsageEvents as readUsageEvents5,
|
|
9935
10343
|
resolveHaivePaths as resolveHaivePaths36
|
|
@@ -9955,7 +10363,7 @@ function registerPlayback(program2) {
|
|
|
9955
10363
|
const filtered = cutoff > 0 ? events.filter((e) => Date.parse(e.at) >= cutoff) : events;
|
|
9956
10364
|
const gapMs = Math.max(1, parseInt(opts.sessionGap ?? "30", 10)) * MS_PER_MINUTE;
|
|
9957
10365
|
const sessions = bucketSessions(filtered, gapMs);
|
|
9958
|
-
const all =
|
|
10366
|
+
const all = existsSync60(paths.memoriesDir) ? await loadMemoriesFromDir33(paths.memoriesDir) : [];
|
|
9959
10367
|
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
10368
|
const enriched = sessions.map((s, i) => {
|
|
9961
10369
|
const startMs = Date.parse(s.start);
|
|
@@ -10148,10 +10556,204 @@ function runCommand3(cmd, args, cwd) {
|
|
|
10148
10556
|
});
|
|
10149
10557
|
}
|
|
10150
10558
|
|
|
10559
|
+
// src/commands/welcome.ts
|
|
10560
|
+
import { existsSync as existsSync61 } from "fs";
|
|
10561
|
+
import "commander";
|
|
10562
|
+
import {
|
|
10563
|
+
findProjectRoot as findProjectRoot41,
|
|
10564
|
+
loadMemoriesFromDir as loadMemoriesFromDir34,
|
|
10565
|
+
resolveHaivePaths as resolveHaivePaths38
|
|
10566
|
+
} from "@hiveai/core";
|
|
10567
|
+
var TYPE_RANK = {
|
|
10568
|
+
decision: 0,
|
|
10569
|
+
architecture: 1,
|
|
10570
|
+
convention: 2,
|
|
10571
|
+
glossary: 3,
|
|
10572
|
+
gotcha: 4,
|
|
10573
|
+
attempt: 5
|
|
10574
|
+
};
|
|
10575
|
+
function registerWelcome(program2) {
|
|
10576
|
+
program2.command("welcome").description(
|
|
10577
|
+
"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"
|
|
10578
|
+
).option("--limit <n>", "maximum memories listed", "20").option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10579
|
+
const root = findProjectRoot41(opts.dir);
|
|
10580
|
+
const paths = resolveHaivePaths38(root);
|
|
10581
|
+
if (!existsSync61(paths.memoriesDir)) {
|
|
10582
|
+
ui.error(`No memories at ${paths.memoriesDir}. Run 'haive init' first.`);
|
|
10583
|
+
process.exitCode = 1;
|
|
10584
|
+
return;
|
|
10585
|
+
}
|
|
10586
|
+
const all = await loadMemoriesFromDir34(paths.memoriesDir);
|
|
10587
|
+
const team = all.filter(
|
|
10588
|
+
({ memory: memory2 }) => memory2.frontmatter.scope === "team" && memory2.frontmatter.status !== "rejected" && memory2.frontmatter.status !== "deprecated" && memory2.frontmatter.status !== "stale" && memory2.frontmatter.type !== "session_recap"
|
|
10589
|
+
);
|
|
10590
|
+
team.sort((a, b) => {
|
|
10591
|
+
const ta = TYPE_RANK[a.memory.frontmatter.type] ?? 99;
|
|
10592
|
+
const tb = TYPE_RANK[b.memory.frontmatter.type] ?? 99;
|
|
10593
|
+
if (ta !== tb) return ta - tb;
|
|
10594
|
+
const sta = a.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
10595
|
+
const stb = b.memory.frontmatter.status === "validated" ? 0 : 1;
|
|
10596
|
+
if (sta !== stb) return sta - stb;
|
|
10597
|
+
return b.memory.frontmatter.created_at.localeCompare(a.memory.frontmatter.created_at);
|
|
10598
|
+
});
|
|
10599
|
+
const cap = Math.max(1, Math.min(500, Number(opts.limit) || 20));
|
|
10600
|
+
const pick = team.slice(0, cap);
|
|
10601
|
+
console.log(ui.bold(`hAIve welcome \u2014 ${pick.length} team memories (${root})`));
|
|
10602
|
+
console.log(ui.dim(`Next: invoke get_briefing with your task or run 'haive briefing --task "\u2026"'`));
|
|
10603
|
+
if (pick.length === 0) {
|
|
10604
|
+
ui.warn("No team memories yet \u2014 add some with 'haive memory add' or promote personal ones.");
|
|
10605
|
+
return;
|
|
10606
|
+
}
|
|
10607
|
+
let i = 1;
|
|
10608
|
+
for (const { memory: memory2 } of pick) {
|
|
10609
|
+
const fm = memory2.frontmatter;
|
|
10610
|
+
const head = memory2.body.match(/^#\s+(.+)/m)?.[1]?.trim();
|
|
10611
|
+
const line = head ?? fm.id;
|
|
10612
|
+
console.log(
|
|
10613
|
+
`${String(i).padStart(2, " ")} ${fm.type.padEnd(12)} ${fm.status.padEnd(10)} ${ui.dim(fm.id)}
|
|
10614
|
+
${line}`
|
|
10615
|
+
);
|
|
10616
|
+
i++;
|
|
10617
|
+
}
|
|
10618
|
+
});
|
|
10619
|
+
}
|
|
10620
|
+
|
|
10621
|
+
// src/commands/memory-lint.ts
|
|
10622
|
+
import { existsSync as existsSync63 } from "fs";
|
|
10623
|
+
import "commander";
|
|
10624
|
+
import {
|
|
10625
|
+
findProjectRoot as findProjectRoot42,
|
|
10626
|
+
loadMemoriesFromDir as loadMemoriesFromDir35,
|
|
10627
|
+
resolveHaivePaths as resolveHaivePaths39
|
|
10628
|
+
} from "@hiveai/core";
|
|
10629
|
+
async function lintMemoriesAsync(root) {
|
|
10630
|
+
const paths = resolveHaivePaths39(root);
|
|
10631
|
+
const out = [];
|
|
10632
|
+
if (!existsSync63(paths.memoriesDir)) return out;
|
|
10633
|
+
const loaded = await loadMemoriesFromDir35(paths.memoriesDir);
|
|
10634
|
+
const ANCHOR_TYPES = /* @__PURE__ */ new Set(["decision", "architecture", "gotcha"]);
|
|
10635
|
+
for (const { filePath, memory: memory2 } of loaded) {
|
|
10636
|
+
const fm = memory2.frontmatter;
|
|
10637
|
+
if (fm.type === "session_recap") continue;
|
|
10638
|
+
const body = memory2.body.trim();
|
|
10639
|
+
const naked = body.replace(/^#.*$/gm, "").replace(/```[\s\S]*?```/g, "").trim();
|
|
10640
|
+
if (naked.length < 40 && fm.status !== "rejected") {
|
|
10641
|
+
out.push({
|
|
10642
|
+
file: filePath,
|
|
10643
|
+
id: fm.id,
|
|
10644
|
+
severity: "warn",
|
|
10645
|
+
code: "SHORT_BODY",
|
|
10646
|
+
message: "Body looks very short (< ~40 chars of prose after headings). Prefer actionable detail."
|
|
10647
|
+
});
|
|
10648
|
+
}
|
|
10649
|
+
if (ANCHOR_TYPES.has(fm.type) && fm.anchor.paths.length === 0 && fm.status === "validated") {
|
|
10650
|
+
out.push({
|
|
10651
|
+
file: filePath,
|
|
10652
|
+
id: fm.id,
|
|
10653
|
+
severity: "warn",
|
|
10654
|
+
code: "MISSING_ANCHOR",
|
|
10655
|
+
message: `${fm.type} is validated without anchor paths \u2014 add anchor.paths so haive sync can flag staleness.`
|
|
10656
|
+
});
|
|
10657
|
+
}
|
|
10658
|
+
if (fm.status === "stale" && !fm.stale_reason) {
|
|
10659
|
+
out.push({
|
|
10660
|
+
file: filePath,
|
|
10661
|
+
id: fm.id,
|
|
10662
|
+
severity: "info",
|
|
10663
|
+
code: "STALE_NO_REASON",
|
|
10664
|
+
message: "Status is stale but stale_reason is empty \u2014 document why when possible."
|
|
10665
|
+
});
|
|
10666
|
+
}
|
|
10667
|
+
if (fm.type === "glossary" && naked.length > 6e3) {
|
|
10668
|
+
out.push({
|
|
10669
|
+
file: filePath,
|
|
10670
|
+
id: fm.id,
|
|
10671
|
+
severity: "info",
|
|
10672
|
+
code: "LONG_GLOSSARY",
|
|
10673
|
+
message: "Very long glossary \u2014 consider splitting concepts for tighter briefings."
|
|
10674
|
+
});
|
|
10675
|
+
}
|
|
10676
|
+
const hasMarkdownHeading = /^#{1,3}\s+\S/m.test(memory2.body.trim());
|
|
10677
|
+
if (!hasMarkdownHeading) {
|
|
10678
|
+
out.push({
|
|
10679
|
+
file: filePath,
|
|
10680
|
+
id: fm.id,
|
|
10681
|
+
severity: "warn",
|
|
10682
|
+
code: "NO_MD_HEADING",
|
|
10683
|
+
message: "No Markdown heading (#/##/###) \u2014 add one so humans and auditors can skim the memo quickly."
|
|
10684
|
+
});
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
return out;
|
|
10688
|
+
}
|
|
10689
|
+
function registerMemoryLint(parent) {
|
|
10690
|
+
parent.command("lint").description(
|
|
10691
|
+
"Heuristic corpus checks (anchors on key types, headings, verbosity). Static analysis only."
|
|
10692
|
+
).option("--json", "emit findings as JSON", false).option("-d, --dir <dir>", "project root").action(async (opts) => {
|
|
10693
|
+
const root = findProjectRoot42(opts.dir);
|
|
10694
|
+
const findings = await lintMemoriesAsync(root);
|
|
10695
|
+
if (opts.json) {
|
|
10696
|
+
console.log(JSON.stringify({ findings_count: findings.length, findings }, null, 2));
|
|
10697
|
+
process.exitCode = findings.some((f) => f.severity === "error") ? 1 : 0;
|
|
10698
|
+
return;
|
|
10699
|
+
}
|
|
10700
|
+
if (findings.length === 0) {
|
|
10701
|
+
ui.success(`memory lint OK \u2014 ${root}`);
|
|
10702
|
+
return;
|
|
10703
|
+
}
|
|
10704
|
+
console.log(ui.bold(`memory lint (${findings.length} finding${findings.length === 1 ? "" : "s"})`) + `
|
|
10705
|
+
`);
|
|
10706
|
+
const order = { error: 0, warn: 1, info: 2 };
|
|
10707
|
+
findings.sort((a, b) => order[a.severity] - order[b.severity] || a.id.localeCompare(b.id));
|
|
10708
|
+
for (const f of findings) {
|
|
10709
|
+
const color = f.severity === "error" ? ui.red : f.severity === "warn" ? ui.yellow : ui.dim;
|
|
10710
|
+
console.log(
|
|
10711
|
+
`${color(f.severity.padEnd(5))} ${ui.dim(f.code)} ${f.id}`
|
|
10712
|
+
);
|
|
10713
|
+
console.log(` ${f.message}`);
|
|
10714
|
+
console.log(ui.dim(` \u2192 ${f.file}`));
|
|
10715
|
+
}
|
|
10716
|
+
process.exitCode = findings.some((x) => x.severity === "error") ? 1 : 0;
|
|
10717
|
+
});
|
|
10718
|
+
}
|
|
10719
|
+
|
|
10720
|
+
// src/commands/memory-suggest-topic.ts
|
|
10721
|
+
import "commander";
|
|
10722
|
+
import { MemoryTypeSchema as MemoryTypeSchema2, suggestTopicKey as suggestTopicKey2 } from "@hiveai/core";
|
|
10723
|
+
function registerMemorySuggestTopic(memory2) {
|
|
10724
|
+
memory2.command("suggest-topic").description("Suggest a stable topic key (topic-upsert) from type + title phrase").requiredOption(
|
|
10725
|
+
"--type <type>",
|
|
10726
|
+
"convention | decision | gotcha | architecture | glossary | attempt | session_recap"
|
|
10727
|
+
).argument("<title>", "Short title or phrase to slugify").action((title, opts) => {
|
|
10728
|
+
const parsed = MemoryTypeSchema2.safeParse(opts.type);
|
|
10729
|
+
if (!parsed.success) {
|
|
10730
|
+
ui.error(`Invalid type: ${opts.type}`);
|
|
10731
|
+
process.exit(1);
|
|
10732
|
+
}
|
|
10733
|
+
const suggestion = suggestTopicKey2(parsed.data, title);
|
|
10734
|
+
console.log(JSON.stringify({ type: parsed.data, ...suggestion }, null, 2));
|
|
10735
|
+
});
|
|
10736
|
+
}
|
|
10737
|
+
|
|
10738
|
+
// src/commands/resolve-project.ts
|
|
10739
|
+
import path40 from "path";
|
|
10740
|
+
import "commander";
|
|
10741
|
+
import { resolveProjectInfo as resolveProjectInfo2 } from "@hiveai/core";
|
|
10742
|
+
function registerResolveProject(program2) {
|
|
10743
|
+
program2.command("resolve-project").description(
|
|
10744
|
+
"Print JSON for hAIve project root resolution (HAIVE_PROJECT_ROOT, markers, .ai layout)."
|
|
10745
|
+
).option("-d, --dir <dir>", "working directory", process.cwd()).action((opts) => {
|
|
10746
|
+
const info = resolveProjectInfo2({ cwd: path40.resolve(opts.dir) });
|
|
10747
|
+
console.log(JSON.stringify({ ok: true, info }, null, 2));
|
|
10748
|
+
});
|
|
10749
|
+
}
|
|
10750
|
+
|
|
10151
10751
|
// 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.
|
|
10752
|
+
var program = new Command44();
|
|
10753
|
+
program.name("haive").description("hAIve \u2014 team-first persistent memory layer for AI coding agents").version("0.9.5");
|
|
10154
10754
|
registerInit(program);
|
|
10755
|
+
registerWelcome(program);
|
|
10756
|
+
registerResolveProject(program);
|
|
10155
10757
|
registerMcp(program);
|
|
10156
10758
|
registerBriefing(program);
|
|
10157
10759
|
registerTui(program);
|
|
@@ -10182,7 +10784,9 @@ registerMemoryImport(memory);
|
|
|
10182
10784
|
registerMemoryImportChangelog(memory);
|
|
10183
10785
|
registerMemoryDigest(memory);
|
|
10184
10786
|
registerMemorySuggest(memory);
|
|
10787
|
+
registerMemorySuggestTopic(memory);
|
|
10185
10788
|
registerMemoryArchive(memory);
|
|
10789
|
+
registerMemoryLint(memory);
|
|
10186
10790
|
var session = program.command("session").description("Manage session lifecycle");
|
|
10187
10791
|
registerSessionEnd(session);
|
|
10188
10792
|
registerSnapshot(program);
|