aidimag 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -0
- package/README.md +113 -0
- package/dist/capture/bootstrap.d.ts +25 -0
- package/dist/capture/bootstrap.js +188 -0
- package/dist/capture/commit-miner.d.ts +59 -0
- package/dist/capture/commit-miner.js +381 -0
- package/dist/capture/harvest.d.ts +50 -0
- package/dist/capture/harvest.js +207 -0
- package/dist/capture/pr-miner.d.ts +57 -0
- package/dist/capture/pr-miner.js +185 -0
- package/dist/capture/session-briefing.d.ts +36 -0
- package/dist/capture/session-briefing.js +150 -0
- package/dist/capture/session-extraction.d.ts +23 -0
- package/dist/capture/session-extraction.js +54 -0
- package/dist/capture/triage.d.ts +30 -0
- package/dist/capture/triage.js +108 -0
- package/dist/cli/commands/capture.d.ts +5 -0
- package/dist/cli/commands/capture.js +357 -0
- package/dist/cli/commands/hosts.d.ts +5 -0
- package/dist/cli/commands/hosts.js +98 -0
- package/dist/cli/commands/knowledge.d.ts +5 -0
- package/dist/cli/commands/knowledge.js +121 -0
- package/dist/cli/commands/memory.d.ts +6 -0
- package/dist/cli/commands/memory.js +392 -0
- package/dist/cli/commands/sync.d.ts +5 -0
- package/dist/cli/commands/sync.js +307 -0
- package/dist/cli/commands/tickets.d.ts +6 -0
- package/dist/cli/commands/tickets.js +328 -0
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +136 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +46 -0
- package/dist/cli/shared.d.ts +37 -0
- package/dist/cli/shared.js +175 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +51 -0
- package/dist/context/generate.d.ts +24 -0
- package/dist/context/generate.js +115 -0
- package/dist/critique/critique.d.ts +40 -0
- package/dist/critique/critique.js +110 -0
- package/dist/db/schema.d.ts +28 -0
- package/dist/db/schema.js +269 -0
- package/dist/db/store.d.ts +182 -0
- package/dist/db/store.js +906 -0
- package/dist/debug.d.ts +14 -0
- package/dist/debug.js +25 -0
- package/dist/embeddings/provider.d.ts +16 -0
- package/dist/embeddings/provider.js +100 -0
- package/dist/embeddings/search.d.ts +22 -0
- package/dist/embeddings/search.js +95 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/knowledge/chunk.d.ts +9 -0
- package/dist/knowledge/chunk.js +78 -0
- package/dist/knowledge/extract.d.ts +34 -0
- package/dist/knowledge/extract.js +113 -0
- package/dist/knowledge/ingest.d.ts +79 -0
- package/dist/knowledge/ingest.js +305 -0
- package/dist/knowledge/llm.d.ts +21 -0
- package/dist/knowledge/llm.js +110 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +532 -0
- package/dist/security/evidence.d.ts +11 -0
- package/dist/security/evidence.js +15 -0
- package/dist/security/seal.d.ts +3 -0
- package/dist/security/seal.js +28 -0
- package/dist/security/sync-push.d.ts +2 -0
- package/dist/security/sync-push.js +37 -0
- package/dist/security/url.d.ts +6 -0
- package/dist/security/url.js +67 -0
- package/dist/sync/client.d.ts +84 -0
- package/dist/sync/client.js +391 -0
- package/dist/sync/server.d.ts +75 -0
- package/dist/sync/server.js +659 -0
- package/dist/tickets/provider.d.ts +80 -0
- package/dist/tickets/provider.js +375 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +5 -0
- package/dist/ui/page.d.ts +5 -0
- package/dist/ui/page.js +841 -0
- package/dist/ui/server.d.ts +10 -0
- package/dist/ui/server.js +437 -0
- package/dist/verify/check.d.ts +38 -0
- package/dist/verify/check.js +121 -0
- package/dist/verify/engine.d.ts +39 -0
- package/dist/verify/engine.js +178 -0
- package/dist/verify/hooks.d.ts +24 -0
- package/dist/verify/hooks.js +125 -0
- package/dist/verify/runners.d.ts +26 -0
- package/dist/verify/runners.js +193 -0
- package/package.json +78 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture & review commands: mine (commits/PRs), bootstrap, harvest, review.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { MemoryStore, findRepoRoot } from "../../db/store.js";
|
|
7
|
+
import { mineCommits, describeMineResult } from "../../capture/commit-miner.js";
|
|
8
|
+
import { fail, autoSync, maybeRegenerateContext, printProposal, createPrompter } from "../shared.js";
|
|
9
|
+
/**
|
|
10
|
+
* Conversational review: walk the queue one proposal at a time —
|
|
11
|
+
* keep / reword / drop / skip. The human gate, made friendly.
|
|
12
|
+
*/
|
|
13
|
+
async function interactiveReview(store) {
|
|
14
|
+
const { triagePending } = await import("../../capture/triage.js");
|
|
15
|
+
const triaged = triagePending(store, 1000);
|
|
16
|
+
const pending = triaged.map((t) => t.proposal);
|
|
17
|
+
const scoreOf = new Map(triaged.map((t) => [t.proposal.id, t]));
|
|
18
|
+
if (pending.length === 0) {
|
|
19
|
+
console.log("✨ Nothing waiting on you — the review queue is empty.");
|
|
20
|
+
return { kept: 0, rejected: 0 };
|
|
21
|
+
}
|
|
22
|
+
const { ask, close } = await createPrompter("q"); // closed stdin = quit
|
|
23
|
+
let kept = 0;
|
|
24
|
+
let rejected = 0;
|
|
25
|
+
let skipped = 0;
|
|
26
|
+
// T2: lazy ticket enrichment — fetched at review time, never at capture time
|
|
27
|
+
const reviewRoot = findRepoRoot();
|
|
28
|
+
const { ticketProviderFor } = await import("../../tickets/provider.js");
|
|
29
|
+
const provider = reviewRoot ? ticketProviderFor(reviewRoot) : null;
|
|
30
|
+
const plural = pending.length === 1 ? "proposal is" : "proposals are";
|
|
31
|
+
console.log(`🧠 ${pending.length} memory ${plural} waiting for your review (best first).\n`);
|
|
32
|
+
try {
|
|
33
|
+
for (let i = 0; i < pending.length; i++) {
|
|
34
|
+
const p = pending[i];
|
|
35
|
+
const src = p.source === "commit-miner" ? `mined from commit ${p.sourceRef?.slice(0, 8) ?? "?"}` : `proposed by ${p.source}`;
|
|
36
|
+
const tri = scoreOf.get(p.id);
|
|
37
|
+
console.log(`── ${i + 1} of ${pending.length} ── ${p.kind} · ${src}${tri ? ` · score ${tri.score.toFixed(2)}` : ""}`);
|
|
38
|
+
if (tri?.reasons.length)
|
|
39
|
+
console.log(` (${tri.reasons.join(", ")})`);
|
|
40
|
+
console.log(`\n “${p.claim}”\n`);
|
|
41
|
+
if (p.paths.length || p.symbols.length)
|
|
42
|
+
console.log(` applies to: ${[...p.paths, ...p.symbols].join(", ")}`);
|
|
43
|
+
if (p.evidence.length)
|
|
44
|
+
console.log(` evidence: ${p.evidence.map((e) => `${e.type}:${e.payload.slice(0, 60)}`).join(" ")}`);
|
|
45
|
+
if (p.ticketRef) {
|
|
46
|
+
let ticketLine = p.ticketRef;
|
|
47
|
+
if (provider) {
|
|
48
|
+
const t = await provider.getTicket(p.ticketRef).catch(() => null);
|
|
49
|
+
if (t) {
|
|
50
|
+
ticketLine = `${t.id} “${t.title}” (${t.type}, ${t.status}) — ${t.url}`;
|
|
51
|
+
if (t.body)
|
|
52
|
+
console.log(` ticket: ${ticketLine}\n ${t.body.slice(0, 200).replace(/\s+/g, " ")}${t.body.length > 200 ? "…" : ""}`);
|
|
53
|
+
else
|
|
54
|
+
console.log(` ticket: ${ticketLine}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log(` ticket: ${ticketLine} (couldn't fetch — provider offline or ticket missing)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(` ticket: ${ticketLine}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (p.rationale)
|
|
65
|
+
console.log(` why: ${p.rationale}`);
|
|
66
|
+
const ans = (await ask("\n Keep this? [y]es · [e]dit wording · [n]o, drop it · [s]kip · [q]uit "))
|
|
67
|
+
.trim()
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
if (ans === "q" || ans === "quit") {
|
|
70
|
+
skipped += pending.length - i;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
else if (ans === "y" || ans === "yes") {
|
|
74
|
+
const m = store.approveProposal(p.id);
|
|
75
|
+
kept++;
|
|
76
|
+
console.log(` ✓ Remembered (${m.id.slice(0, 8)}).\n`);
|
|
77
|
+
}
|
|
78
|
+
else if (ans === "e" || ans === "edit") {
|
|
79
|
+
const claim = (await ask(" Your wording (enter keeps the original):\n › ")).trim();
|
|
80
|
+
const m = store.approveProposal(p.id, claim ? { claim } : undefined);
|
|
81
|
+
kept++;
|
|
82
|
+
console.log(claim ? ` ✓ Remembered with your wording (${m.id.slice(0, 8)}).\n` : ` ✓ Remembered as-is (${m.id.slice(0, 8)}).\n`);
|
|
83
|
+
}
|
|
84
|
+
else if (ans === "n" || ans === "no") {
|
|
85
|
+
store.rejectProposal(p.id);
|
|
86
|
+
rejected++;
|
|
87
|
+
console.log(" ✗ Dropped — it won't be proposed again.\n");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
skipped++;
|
|
91
|
+
console.log(" ↷ Skipped — it'll be here next time.\n");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
close();
|
|
97
|
+
}
|
|
98
|
+
const bits = [
|
|
99
|
+
kept ? `${kept} remembered` : null,
|
|
100
|
+
rejected ? `${rejected} dropped` : null,
|
|
101
|
+
skipped ? `${skipped} left for later` : null,
|
|
102
|
+
].filter(Boolean);
|
|
103
|
+
console.log(`Done — ${bits.length ? bits.join(", ") : "no changes"}.${kept ? " Run `dim verify` to put the new memories to the test." : ""}`);
|
|
104
|
+
return { kept, rejected };
|
|
105
|
+
}
|
|
106
|
+
export function registerCaptureCommands(program) {
|
|
107
|
+
program
|
|
108
|
+
.command("mine")
|
|
109
|
+
.description("Mine git history for memory candidates (queued for review, never auto-saved)")
|
|
110
|
+
.option("-n, --max <n>", "Max commits to scan", "500")
|
|
111
|
+
.option("--full", "Rescan from the beginning of history (ignore cursor)")
|
|
112
|
+
.option("--llm", "Deep mining: LLM reads each commit's message AND diff, synthesizes claims + suggested checks (needs Ollama/OPENAI_API_KEY; slower, much higher quality)")
|
|
113
|
+
.option("--prs", "Mine merged GitHub PRs + review comments instead of commits (needs the `gh` CLI and an LLM provider; review threads carry the unwritten rules)")
|
|
114
|
+
.option("-q, --quiet", "Only speak up when candidates are found (for the post-commit hook)")
|
|
115
|
+
.action(async (opts) => {
|
|
116
|
+
const root = findRepoRoot() ?? fail("not inside a git repo");
|
|
117
|
+
if (!existsSync(path.join(root, ".git")))
|
|
118
|
+
fail("commit mining requires a git repo");
|
|
119
|
+
const store = MemoryStore.open(root, { create: true });
|
|
120
|
+
if (opts.prs) {
|
|
121
|
+
const { minePrs, ghAvailable } = await import("../../capture/pr-miner.js");
|
|
122
|
+
if (!ghAvailable(root)) {
|
|
123
|
+
store.close();
|
|
124
|
+
fail("PR mining needs the GitHub CLI — install `gh` and run `gh auth login`");
|
|
125
|
+
}
|
|
126
|
+
const r = await minePrs(store, root, { max: opts.max ? parseInt(opts.max, 10) : undefined, all: Boolean(opts.full) });
|
|
127
|
+
if (!r.provider) {
|
|
128
|
+
store.close();
|
|
129
|
+
fail("no LLM provider available — review threads need synthesis; run Ollama or set OPENAI_API_KEY (see AIDIMAG_LLM)");
|
|
130
|
+
}
|
|
131
|
+
console.log(`Scanned ${r.scanned} merged PR(s) with ${r.provider}: ${r.proposed.length} proposal(s) queued` +
|
|
132
|
+
(r.skippedDuplicates ? `, ${r.skippedDuplicates} duplicate(s) skipped` : ""));
|
|
133
|
+
for (const p of r.proposed)
|
|
134
|
+
printProposal(p);
|
|
135
|
+
if (r.proposed.length)
|
|
136
|
+
console.log(`\nReview with \`dim review\`.`);
|
|
137
|
+
else if (r.scanned === 0)
|
|
138
|
+
console.log("No newly merged PRs since the last run (use --full to rescan).");
|
|
139
|
+
store.close();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let res;
|
|
143
|
+
let llmProvider = null;
|
|
144
|
+
if (opts.llm) {
|
|
145
|
+
const { mineCommitsLlm } = await import("../../capture/commit-miner.js");
|
|
146
|
+
const r = await mineCommitsLlm(store, root, {
|
|
147
|
+
maxCommits: parseInt(opts.max, 10),
|
|
148
|
+
full: Boolean(opts.full),
|
|
149
|
+
});
|
|
150
|
+
res = r;
|
|
151
|
+
llmProvider = r.provider;
|
|
152
|
+
if (!llmProvider && !opts.quiet) {
|
|
153
|
+
console.log("(no LLM provider available — fell back to keyword mining; run Ollama or set OPENAI_API_KEY)");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
res = mineCommits(store, root, {
|
|
158
|
+
maxCommits: parseInt(opts.max, 10),
|
|
159
|
+
full: Boolean(opts.full),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (opts.quiet) {
|
|
163
|
+
// post-commit hook mode: a single gentle nudge, nothing else
|
|
164
|
+
if (res.proposed.length > 0) {
|
|
165
|
+
const total = store.listProposals("PENDING", 1000).length;
|
|
166
|
+
console.log(`🧠 aidimag: this commit looks memory-worthy — ${res.proposed.length} proposal(s) queued` +
|
|
167
|
+
` (${total} pending). Review with \`dim review\`.`);
|
|
168
|
+
}
|
|
169
|
+
store.close();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (res.noCommits || res.noNewCommits) {
|
|
173
|
+
console.log(describeMineResult(res, { llmProvider, llmRequested: Boolean(opts.llm) }));
|
|
174
|
+
store.close();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(describeMineResult(res, { llmProvider, llmRequested: Boolean(opts.llm) }));
|
|
178
|
+
for (const p of res.proposed)
|
|
179
|
+
printProposal(p);
|
|
180
|
+
store.close();
|
|
181
|
+
});
|
|
182
|
+
program
|
|
183
|
+
.command("bootstrap")
|
|
184
|
+
.description("Give a fresh repo an instant brain: survey README/docs/manifests/structure/churn and LLM-extract an initial memory set (queued for review)")
|
|
185
|
+
.option("--force", "Re-run even if this repo was already bootstrapped")
|
|
186
|
+
.action(async (opts) => {
|
|
187
|
+
const root = findRepoRoot() ?? fail("not inside a git repo");
|
|
188
|
+
const store = MemoryStore.open(root, { create: true });
|
|
189
|
+
const { bootstrapRepo } = await import("../../capture/bootstrap.js");
|
|
190
|
+
console.log("Surveying the repo (docs, manifests, structure, churn)…");
|
|
191
|
+
const res = await bootstrapRepo(store, root, { force: Boolean(opts.force) });
|
|
192
|
+
if (res.alreadyBootstrapped) {
|
|
193
|
+
console.log("Already bootstrapped — use --force to re-run (dedupe absorbs repeats).");
|
|
194
|
+
}
|
|
195
|
+
else if (!res.provider) {
|
|
196
|
+
fail("no LLM provider available — run Ollama locally or set OPENAI_API_KEY (see AIDIMAG_LLM)");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log(`Surveyed ${res.surveyedFiles.length} file(s) with ${res.provider}: ` +
|
|
200
|
+
`${res.proposed} proposal(s) queued${res.duplicates ? `, ${res.duplicates} duplicate(s) skipped` : ""}.`);
|
|
201
|
+
if (res.proposed) {
|
|
202
|
+
console.log(`\nYour repo's starter brain is ready for review: \`dim review\``);
|
|
203
|
+
console.log(`(then \`dim verify\` to put the suggested checks to the test)`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
console.log("No durable claims extracted — the survey found little written-down knowledge. Feed docs into knowledge/ or use `dim mine --llm`.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
store.close();
|
|
210
|
+
});
|
|
211
|
+
program
|
|
212
|
+
.command("harvest")
|
|
213
|
+
.description("Harvest durable facts YOU typed into AI chats (Claude Code transcripts) into the review queue — local-only, secrets redacted")
|
|
214
|
+
.option("--all", "Rescan every session (ignore cursor; dedupe absorbs repeats)")
|
|
215
|
+
.option("--install-hook", "Wire `dim harvest -q` into this repo's Claude Code SessionEnd hook (.claude/settings.json)")
|
|
216
|
+
.option("-q, --quiet", "Only speak up when proposals are queued (for the SessionEnd hook)")
|
|
217
|
+
.action(async (opts) => {
|
|
218
|
+
const root = findRepoRoot() ?? fail("not inside a git repo");
|
|
219
|
+
const { harvestClaudeSessions, installClaudeSessionEndHook, claudeProjectDir } = await import("../../capture/harvest.js");
|
|
220
|
+
if (opts.installHook) {
|
|
221
|
+
const { installed, settingsPath } = installClaudeSessionEndHook(root);
|
|
222
|
+
console.log(installed
|
|
223
|
+
? `✓ SessionEnd hook installed in ${settingsPath} — every Claude Code session is now harvested on close.`
|
|
224
|
+
: `Hook already present in ${settingsPath} — nothing to do.`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const store = MemoryStore.open(root, { create: true });
|
|
228
|
+
const res = await harvestClaudeSessions(store, root, { all: Boolean(opts.all) });
|
|
229
|
+
if (opts.quiet) {
|
|
230
|
+
if (res.proposed > 0) {
|
|
231
|
+
console.log(`🧠 aidimag: harvested ${res.proposed} memory candidate(s) from your AI chat — review with \`dim review\`.`);
|
|
232
|
+
}
|
|
233
|
+
store.close();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!res.transcriptDir) {
|
|
237
|
+
console.log(`No Claude Code transcripts found for this repo (${claudeProjectDir(root) ?? "~/.claude/projects/<repo-slug>"} missing).`);
|
|
238
|
+
console.log("Transcripts appear after your first Claude Code session here. Cursor/Copilot chat harvesting: planned.");
|
|
239
|
+
}
|
|
240
|
+
else if (!res.provider) {
|
|
241
|
+
fail("no LLM provider available — run Ollama locally or set OPENAI_API_KEY (see AIDIMAG_LLM)");
|
|
242
|
+
}
|
|
243
|
+
else if (res.sessionsScanned === 0) {
|
|
244
|
+
console.log("No new sessions since the last harvest. Use --all to rescan everything.");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(`Scanned ${res.sessionsScanned} session(s), ${res.messagesConsidered} user message(s) via ${res.provider}: ` +
|
|
248
|
+
`${res.proposed} proposal(s) queued` +
|
|
249
|
+
(res.duplicates ? `, ${res.duplicates} duplicate(s) skipped` : "") +
|
|
250
|
+
".");
|
|
251
|
+
if (res.proposed)
|
|
252
|
+
console.log(`Review with \`dim review\`.`);
|
|
253
|
+
}
|
|
254
|
+
if (!opts.installHook && res.transcriptDir) {
|
|
255
|
+
console.log(`(tip: \`dim harvest --install-hook\` runs this automatically when each Claude Code session ends)`);
|
|
256
|
+
}
|
|
257
|
+
store.close();
|
|
258
|
+
});
|
|
259
|
+
program
|
|
260
|
+
.command("review")
|
|
261
|
+
.description("Review pending memory proposals — interactive walkthrough by default (list | approve | reject for scripting)")
|
|
262
|
+
.argument("[action]", "interactive (default in a terminal) | list | approve | reject")
|
|
263
|
+
.argument("[id]", "Proposal id (8-char prefix ok); 'all' with approve/reject applies to every pending proposal")
|
|
264
|
+
.option("-n, --limit <n>", "Max proposals to list", "50")
|
|
265
|
+
.option("--min-score <s>", "With 'approve all': only approve proposals triaged at or above this score (0–1)")
|
|
266
|
+
.action(async (action, id, opts) => {
|
|
267
|
+
const store = MemoryStore.open();
|
|
268
|
+
const effective = action ?? (process.stdin.isTTY && process.stdout.isTTY ? "interactive" : "list");
|
|
269
|
+
switch (effective) {
|
|
270
|
+
case "interactive": {
|
|
271
|
+
const { kept, rejected } = await interactiveReview(store);
|
|
272
|
+
if (kept + rejected > 0)
|
|
273
|
+
await autoSync(store);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "list": {
|
|
277
|
+
const { triagePending } = await import("../../capture/triage.js");
|
|
278
|
+
const triaged = triagePending(store, parseInt(opts.limit, 10));
|
|
279
|
+
if (triaged.length === 0)
|
|
280
|
+
console.log("No pending proposals.");
|
|
281
|
+
for (const t of triaged) {
|
|
282
|
+
console.log(` score ${t.score.toFixed(2)}${t.reasons.length ? ` (${t.reasons.join(", ")})` : ""}`);
|
|
283
|
+
printProposal(t.proposal);
|
|
284
|
+
}
|
|
285
|
+
if (triaged.length) {
|
|
286
|
+
console.log(`\nApprove: dim review approve <id> | Reject: dim review reject <id> | Walkthrough: dim review`);
|
|
287
|
+
console.log(`Batch: dim review approve all --min-score 0.7`);
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "approve": {
|
|
292
|
+
if (!id)
|
|
293
|
+
fail("usage: dim review approve <id|all> [--min-score <s>]");
|
|
294
|
+
let targets;
|
|
295
|
+
if (id === "all") {
|
|
296
|
+
const { triagePending } = await import("../../capture/triage.js");
|
|
297
|
+
const minScore = opts.minScore !== undefined ? parseFloat(opts.minScore) : null;
|
|
298
|
+
const triaged = triagePending(store, 1000);
|
|
299
|
+
const chosen = minScore === null ? triaged : triaged.filter((t) => t.score >= minScore);
|
|
300
|
+
targets = chosen.map((t) => t.proposal.id);
|
|
301
|
+
if (minScore !== null) {
|
|
302
|
+
console.log(`${chosen.length} of ${triaged.length} pending proposal(s) scored ≥ ${minScore}.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
targets = [id];
|
|
307
|
+
}
|
|
308
|
+
for (const t of targets) {
|
|
309
|
+
const entry = store.approveProposal(t);
|
|
310
|
+
console.log(`✓ approved → memory ${entry.id.slice(0, 8)}: ${entry.claim}`);
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "reject": {
|
|
315
|
+
if (!id)
|
|
316
|
+
fail("usage: dim review reject <id|all>");
|
|
317
|
+
const targets = id === "all" ? store.listProposals("PENDING", 1000).map((p) => p.id) : [id];
|
|
318
|
+
for (const t of targets) {
|
|
319
|
+
const p = store.rejectProposal(t);
|
|
320
|
+
console.log(`✗ rejected ${p.id.slice(0, 8)}: ${p.claim}`);
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
default:
|
|
325
|
+
fail(`unknown action '${action}'. Use: list | approve | reject (or no action for the walkthrough)`);
|
|
326
|
+
}
|
|
327
|
+
if (effective === "approve" || effective === "reject")
|
|
328
|
+
await autoSync(store);
|
|
329
|
+
await maybeRegenerateContext(store);
|
|
330
|
+
store.close();
|
|
331
|
+
});
|
|
332
|
+
const proposalsCmd = program.command("proposals").description("Proposal housekeeping");
|
|
333
|
+
proposalsCmd
|
|
334
|
+
.command("gc")
|
|
335
|
+
.description("Remove resolved (approved/rejected) proposal rows and tombstone for team sync")
|
|
336
|
+
.option("--dry-run", "Report how many rows would be removed without deleting")
|
|
337
|
+
.action(async (opts) => {
|
|
338
|
+
const store = MemoryStore.open();
|
|
339
|
+
try {
|
|
340
|
+
const { removed } = store.gcResolvedProposals({ dryRun: Boolean(opts.dryRun) });
|
|
341
|
+
if (removed === 0) {
|
|
342
|
+
console.log("No resolved proposals to remove.");
|
|
343
|
+
}
|
|
344
|
+
else if (opts.dryRun) {
|
|
345
|
+
console.log(`${removed} resolved proposal row(s) would be removed. Run without --dry-run to apply.`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.log(`Removed ${removed} resolved proposal row(s). Run \`dim sync\` to propagate deletions.`);
|
|
349
|
+
await autoSync(store);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
store.close();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-process & integration commands: ui (dashboard), generate-context, mcp.
|
|
3
|
+
*/
|
|
4
|
+
import { MemoryStore, findRepoRoot } from "../../db/store.js";
|
|
5
|
+
import { fail, openBrowser } from "../shared.js";
|
|
6
|
+
export function registerHostCommands(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("ui")
|
|
9
|
+
.argument("[action]", "start (default) | stop")
|
|
10
|
+
.description("Manage the local web dashboard (memory list, review queue, visual graph)")
|
|
11
|
+
.option("-p, --port <n>", "Port", "4517")
|
|
12
|
+
.option("--no-open", "Don't open the browser automatically (start only)")
|
|
13
|
+
.action(async (action, opts) => {
|
|
14
|
+
const effectiveAction = action ?? "start";
|
|
15
|
+
if (effectiveAction === "stop") {
|
|
16
|
+
const port = parseInt(opts.port, 10);
|
|
17
|
+
const { exec } = await import("node:child_process");
|
|
18
|
+
const { promisify } = await import("node:util");
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
try {
|
|
21
|
+
// Find process listening on the port
|
|
22
|
+
const cmd = process.platform === "win32"
|
|
23
|
+
? `netstat -ano | findstr :${port}`
|
|
24
|
+
: `lsof -ti:${port}`;
|
|
25
|
+
const { stdout } = await execAsync(cmd);
|
|
26
|
+
if (!stdout.trim()) {
|
|
27
|
+
console.log(`No server found running on port ${port}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Kill the process
|
|
31
|
+
const pids = process.platform === "win32"
|
|
32
|
+
? stdout.split("\n").map(line => line.trim().split(/\s+/).pop()).filter(Boolean)
|
|
33
|
+
: stdout.trim().split("\n");
|
|
34
|
+
for (const pid of pids) {
|
|
35
|
+
const killCmd = process.platform === "win32" ? `taskkill /F /PID ${pid}` : `kill ${pid}`;
|
|
36
|
+
await execAsync(killCmd);
|
|
37
|
+
}
|
|
38
|
+
console.log(`✓ Stopped server on port ${port}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err instanceof Error && "code" in err && err.code === 1) {
|
|
42
|
+
console.log(`No server found running on port ${port}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
fail(`Failed to stop server: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (effectiveAction === "start") {
|
|
50
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
51
|
+
const store = MemoryStore.open(root);
|
|
52
|
+
const { startUiServer } = await import("../../ui/server.js");
|
|
53
|
+
const url = await startUiServer(store, root, parseInt(opts.port, 10));
|
|
54
|
+
console.log(`aidimag dashboard: ${url} (Ctrl+C to stop)`);
|
|
55
|
+
if (opts.open)
|
|
56
|
+
await openBrowser(url);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
fail(`unknown action '${action}'. Use: start | stop`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
program
|
|
63
|
+
.command("generate-context")
|
|
64
|
+
.description("Render trustworthy memory into a static context file (CLAUDE.md, .cursorrules, .windsurfrules, AGENTS.md, copilot-instructions) for non-MCP AI tools")
|
|
65
|
+
.option("-f, --format <format>", "claude | cursorrules | copilot | windsurfrules | agents | all", "claude")
|
|
66
|
+
.option("--auto", "Also persist generateContext.auto in .aidimag/config.json so verify/review/sync keep it fresh")
|
|
67
|
+
.option("--no-auto", "Disable auto-regeneration (clears generateContext.auto)")
|
|
68
|
+
.action(async (opts) => {
|
|
69
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
70
|
+
const store = MemoryStore.open(root);
|
|
71
|
+
const { generateContext } = await import("../../context/generate.js");
|
|
72
|
+
const format = String(opts.format).toLowerCase();
|
|
73
|
+
if (!["claude", "cursorrules", "copilot", "windsurfrules", "agents", "all"].includes(format)) {
|
|
74
|
+
fail(`invalid --format '${opts.format}'. Use: claude | cursorrules | copilot | windsurfrules | agents | all`);
|
|
75
|
+
}
|
|
76
|
+
const r = generateContext(store, root, format);
|
|
77
|
+
console.log(`📝 Wrote ${r.files.join(", ")} — ${r.total} memories (${r.pinned} pinned).`);
|
|
78
|
+
if (r.total === 0) {
|
|
79
|
+
console.log(" (no verified memories yet — run `dim remember` or approve proposals with `dim review`)");
|
|
80
|
+
}
|
|
81
|
+
// commander sets opts.auto=false only when --no-auto is passed; undefined otherwise
|
|
82
|
+
if (opts.auto === true || opts.auto === false) {
|
|
83
|
+
const { writeConfig } = await import("../../config.js");
|
|
84
|
+
writeConfig(root, { generateContext: opts.auto ? { auto: true, format: format } : { auto: false } });
|
|
85
|
+
console.log(opts.auto
|
|
86
|
+
? `🔄 Auto-regeneration ON — verify/review/sync will refresh ${format === "all" ? "the context files" : r.files[0]}.`
|
|
87
|
+
: `Auto-regeneration OFF.`);
|
|
88
|
+
}
|
|
89
|
+
store.close();
|
|
90
|
+
});
|
|
91
|
+
program
|
|
92
|
+
.command("mcp")
|
|
93
|
+
.description("Run the aidimag MCP server (stdio)")
|
|
94
|
+
.action(async () => {
|
|
95
|
+
await import("../../mcp/server.js");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=hosts.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge-inbox commands: knowledge sync | status | list | watch.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { MemoryStore, findRepoRoot } from "../../db/store.js";
|
|
7
|
+
import { resolveKnowledgeConfig } from "../../config.js";
|
|
8
|
+
import { fail, autoSync, maybeRegenerateContext, printIngestReport } from "../shared.js";
|
|
9
|
+
export function registerKnowledgeCommands(program) {
|
|
10
|
+
const knowledge = program
|
|
11
|
+
.command("knowledge")
|
|
12
|
+
.description("Manage the knowledge inbox: summarize dropped docs into reviewed, pinned memories")
|
|
13
|
+
.action(() => knowledge.help());
|
|
14
|
+
knowledge
|
|
15
|
+
.command("sync", { isDefault: true })
|
|
16
|
+
.description("Process the knowledge inbox now: summarize new docs into proposals (review with `dim review`)")
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
19
|
+
const store = MemoryStore.open(root);
|
|
20
|
+
const { ingestAll } = await import("../../knowledge/ingest.js");
|
|
21
|
+
const cfg = resolveKnowledgeConfig(root);
|
|
22
|
+
const report = await ingestAll(store, root, cfg);
|
|
23
|
+
printIngestReport(report);
|
|
24
|
+
// Auto-approved knowledge (requireReview=false) becomes active memory → keep context fresh.
|
|
25
|
+
if (report.processed.some((d) => d.pinned)) {
|
|
26
|
+
await autoSync(store);
|
|
27
|
+
await maybeRegenerateContext(store);
|
|
28
|
+
}
|
|
29
|
+
store.close();
|
|
30
|
+
});
|
|
31
|
+
knowledge
|
|
32
|
+
.command("status")
|
|
33
|
+
.description("Show pending / skipped / processed counts for the knowledge inbox")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
36
|
+
const { knowledgeStatus } = await import("../../knowledge/ingest.js");
|
|
37
|
+
const s = await knowledgeStatus(root, resolveKnowledgeConfig(root));
|
|
38
|
+
console.log(`Knowledge inbox: ${s.folder}/`);
|
|
39
|
+
console.log(` pending ${s.pending.length}${s.pending.length ? " → run \`dim knowledge sync\`" : ""}`);
|
|
40
|
+
for (const p of s.pending)
|
|
41
|
+
console.log(` • ${p.file} (${p.bytes} bytes)`);
|
|
42
|
+
console.log(` unsupported ${s.unsupported.length}${s.unsupported.length ? " (will move to skipped/ on next sync)" : ""}`);
|
|
43
|
+
for (const u of s.unsupported)
|
|
44
|
+
console.log(` • ${u.file} — ${u.reason}`);
|
|
45
|
+
console.log(` skipped/ ${s.skippedOnDisk.length}`);
|
|
46
|
+
for (const f of s.skippedOnDisk)
|
|
47
|
+
console.log(` • ${f}`);
|
|
48
|
+
console.log(` processed ${s.processed.length}`);
|
|
49
|
+
});
|
|
50
|
+
knowledge
|
|
51
|
+
.command("list")
|
|
52
|
+
.description("List processed knowledge docs and the memories they produced")
|
|
53
|
+
.action(async () => {
|
|
54
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
55
|
+
const { readManifest } = await import("../../knowledge/ingest.js");
|
|
56
|
+
const docs = readManifest(root).docs;
|
|
57
|
+
if (!docs.length) {
|
|
58
|
+
console.log("No knowledge docs processed yet. Drop files in the inbox and run `dim knowledge sync`.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const d of docs) {
|
|
62
|
+
const memo = d.memoryIds.length ? `${d.memoryIds.length} pinned` : `${d.proposalIds.length} proposed`;
|
|
63
|
+
console.log(`📄 ${d.file} · ${d.claimCount} claim(s) (${memo}) · via ${d.via} · ${d.date.slice(0, 10)}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
knowledge
|
|
67
|
+
.command("watch")
|
|
68
|
+
.description("Foreground watcher: process the inbox automatically whenever a doc is dropped (Ctrl-C to stop)")
|
|
69
|
+
.option("-d, --debounce <ms>", "Settle time before processing a batch of drops", "750")
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
72
|
+
const cfg = resolveKnowledgeConfig(root);
|
|
73
|
+
const inbox = path.join(root, cfg.folder);
|
|
74
|
+
mkdirSync(inbox, { recursive: true });
|
|
75
|
+
const { ingestAll } = await import("../../knowledge/ingest.js");
|
|
76
|
+
const debounceMs = Math.max(100, parseInt(opts.debounce, 10) || 750);
|
|
77
|
+
let running = false;
|
|
78
|
+
let queued = false;
|
|
79
|
+
const run = async () => {
|
|
80
|
+
if (running) {
|
|
81
|
+
queued = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
running = true;
|
|
85
|
+
try {
|
|
86
|
+
const store = MemoryStore.open(root);
|
|
87
|
+
const report = await ingestAll(store, root, cfg);
|
|
88
|
+
if (report.processed.length || report.skipped.length || report.duplicates.length) {
|
|
89
|
+
printIngestReport(report);
|
|
90
|
+
if (report.processed.some((d) => d.pinned)) {
|
|
91
|
+
await autoSync(store);
|
|
92
|
+
await maybeRegenerateContext(store);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
store.close();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(`dim knowledge watch: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
running = false;
|
|
102
|
+
if (queued) {
|
|
103
|
+
queued = false;
|
|
104
|
+
void run();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
let timer;
|
|
109
|
+
const { watch } = await import("node:fs");
|
|
110
|
+
const watcher = watch(inbox, { persistent: true }, () => {
|
|
111
|
+
if (timer)
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
114
|
+
});
|
|
115
|
+
console.log(`👀 Watching ${cfg.folder}/ for dropped docs — Ctrl-C to stop.`);
|
|
116
|
+
await run(); // catch up on anything already sitting in the inbox
|
|
117
|
+
process.on("SIGINT", () => { watcher.close(); console.log("\nStopped."); process.exit(0); });
|
|
118
|
+
await new Promise(() => { });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=knowledge.js.map
|