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.
Files changed (91) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +113 -0
  3. package/dist/capture/bootstrap.d.ts +25 -0
  4. package/dist/capture/bootstrap.js +188 -0
  5. package/dist/capture/commit-miner.d.ts +59 -0
  6. package/dist/capture/commit-miner.js +381 -0
  7. package/dist/capture/harvest.d.ts +50 -0
  8. package/dist/capture/harvest.js +207 -0
  9. package/dist/capture/pr-miner.d.ts +57 -0
  10. package/dist/capture/pr-miner.js +185 -0
  11. package/dist/capture/session-briefing.d.ts +36 -0
  12. package/dist/capture/session-briefing.js +150 -0
  13. package/dist/capture/session-extraction.d.ts +23 -0
  14. package/dist/capture/session-extraction.js +54 -0
  15. package/dist/capture/triage.d.ts +30 -0
  16. package/dist/capture/triage.js +108 -0
  17. package/dist/cli/commands/capture.d.ts +5 -0
  18. package/dist/cli/commands/capture.js +357 -0
  19. package/dist/cli/commands/hosts.d.ts +5 -0
  20. package/dist/cli/commands/hosts.js +98 -0
  21. package/dist/cli/commands/knowledge.d.ts +5 -0
  22. package/dist/cli/commands/knowledge.js +121 -0
  23. package/dist/cli/commands/memory.d.ts +6 -0
  24. package/dist/cli/commands/memory.js +392 -0
  25. package/dist/cli/commands/sync.d.ts +5 -0
  26. package/dist/cli/commands/sync.js +307 -0
  27. package/dist/cli/commands/tickets.d.ts +6 -0
  28. package/dist/cli/commands/tickets.js +328 -0
  29. package/dist/cli/commands/verify.d.ts +5 -0
  30. package/dist/cli/commands/verify.js +136 -0
  31. package/dist/cli/index.d.ts +19 -0
  32. package/dist/cli/index.js +46 -0
  33. package/dist/cli/shared.d.ts +37 -0
  34. package/dist/cli/shared.js +175 -0
  35. package/dist/config.d.ts +55 -0
  36. package/dist/config.js +51 -0
  37. package/dist/context/generate.d.ts +24 -0
  38. package/dist/context/generate.js +115 -0
  39. package/dist/critique/critique.d.ts +40 -0
  40. package/dist/critique/critique.js +110 -0
  41. package/dist/db/schema.d.ts +28 -0
  42. package/dist/db/schema.js +269 -0
  43. package/dist/db/store.d.ts +182 -0
  44. package/dist/db/store.js +906 -0
  45. package/dist/debug.d.ts +14 -0
  46. package/dist/debug.js +25 -0
  47. package/dist/embeddings/provider.d.ts +16 -0
  48. package/dist/embeddings/provider.js +100 -0
  49. package/dist/embeddings/search.d.ts +22 -0
  50. package/dist/embeddings/search.js +95 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +12 -0
  53. package/dist/knowledge/chunk.d.ts +9 -0
  54. package/dist/knowledge/chunk.js +78 -0
  55. package/dist/knowledge/extract.d.ts +34 -0
  56. package/dist/knowledge/extract.js +113 -0
  57. package/dist/knowledge/ingest.d.ts +79 -0
  58. package/dist/knowledge/ingest.js +305 -0
  59. package/dist/knowledge/llm.d.ts +21 -0
  60. package/dist/knowledge/llm.js +110 -0
  61. package/dist/mcp/server.d.ts +11 -0
  62. package/dist/mcp/server.js +532 -0
  63. package/dist/security/evidence.d.ts +11 -0
  64. package/dist/security/evidence.js +15 -0
  65. package/dist/security/seal.d.ts +3 -0
  66. package/dist/security/seal.js +28 -0
  67. package/dist/security/sync-push.d.ts +2 -0
  68. package/dist/security/sync-push.js +37 -0
  69. package/dist/security/url.d.ts +6 -0
  70. package/dist/security/url.js +67 -0
  71. package/dist/sync/client.d.ts +84 -0
  72. package/dist/sync/client.js +391 -0
  73. package/dist/sync/server.d.ts +75 -0
  74. package/dist/sync/server.js +659 -0
  75. package/dist/tickets/provider.d.ts +80 -0
  76. package/dist/tickets/provider.js +375 -0
  77. package/dist/types.d.ts +133 -0
  78. package/dist/types.js +5 -0
  79. package/dist/ui/page.d.ts +5 -0
  80. package/dist/ui/page.js +841 -0
  81. package/dist/ui/server.d.ts +10 -0
  82. package/dist/ui/server.js +437 -0
  83. package/dist/verify/check.d.ts +38 -0
  84. package/dist/verify/check.js +121 -0
  85. package/dist/verify/engine.d.ts +39 -0
  86. package/dist/verify/engine.js +178 -0
  87. package/dist/verify/hooks.d.ts +24 -0
  88. package/dist/verify/hooks.js +125 -0
  89. package/dist/verify/runners.d.ts +26 -0
  90. package/dist/verify/runners.js +193 -0
  91. package/package.json +78 -0
@@ -0,0 +1,532 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aidimag MCP server — exposes repo memory to any MCP-compatible agent
4
+ * (Claude Code, Cursor, Copilot, ...) over stdio.
5
+ *
6
+ * Tools: memory_search, memory_get_for_files, memory_write, memory_refute, memory_status,
7
+ * commits_mine, context_note (passive in-chat fact capture), … — searches are logged so zero-hit
8
+ * queries surface as coverage gaps (`dim gaps`).
9
+ * Resource: aidimag://digest — repo memory digest for session bootstrapping.
10
+ */
11
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { z } from "zod";
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { MemoryStore, findRepoRoot } from "../db/store.js";
18
+ import { sessionEndPromptFor, proposalSummaryLine } from "../capture/session-extraction.js";
19
+ import { mineCommits, mineCommitsLlm, describeMineResult } from "../capture/commit-miner.js";
20
+ import { buildSessionBriefing, renderBriefing, sessionStartPrompt } from "../capture/session-briefing.js";
21
+ import { critique } from "../critique/critique.js";
22
+ import { ticketProviderFor, detectBranchTicket } from "../tickets/provider.js";
23
+ import { verifyAll } from "../verify/engine.js";
24
+ import { hybridSearch, indexMemory } from "../embeddings/search.js";
25
+ import { stripExecutableEvidence } from "../security/evidence.js";
26
+ import { resolveKnowledgeConfig } from "../config.js";
27
+ import { classifyInbox, finalizeDoc } from "../knowledge/ingest.js";
28
+ import { KNOWLEDGE_EXTRACT_INSTRUCTIONS, buildExtractionUser, parseClaims } from "../knowledge/extract.js";
29
+ import { debugLog } from "../debug.js";
30
+ const PKG_VERSION = JSON.parse(readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8")).version;
31
+ const KINDS = [
32
+ "DECISION",
33
+ "CONVENTION",
34
+ "GOTCHA",
35
+ "FAILED_APPROACH",
36
+ "ARCHITECTURE",
37
+ "INVARIANT",
38
+ "TODO_CONTEXT",
39
+ "GUARDRAIL",
40
+ "SKILL",
41
+ ];
42
+ const GUARDRAIL_LEVELS = ["always", "ask-first", "never"];
43
+ const GUARDRAIL_ICON = { never: "🚫", always: "✅", "ask-first": "🤚" };
44
+ const STATUSES = ["VERIFIED", "UNVERIFIED", "STALE", "REFUTED"];
45
+ const EVIDENCE_TYPES = [
46
+ "COMMIT_REF",
47
+ "TEST_RESULT",
48
+ "EXEC_TRACE",
49
+ "STATIC_CHECK",
50
+ "HUMAN_ATTESTED",
51
+ "TICKET_REF",
52
+ ];
53
+ function renderMemory(m) {
54
+ const scope = m.scope.paths.length || m.scope.symbols.length
55
+ ? ` [scope: ${[...m.scope.paths, ...m.scope.symbols].join(", ")}]`
56
+ : " [scope: repo-wide]";
57
+ const evidence = m.grounding.length
58
+ ? `\n evidence: ${m.grounding.map((e) => `${e.type}(${e.result})`).join(", ")}`
59
+ : "";
60
+ const guard = m.kind === "GUARDRAIL" && m.guardrailLevel
61
+ ? ` ${GUARDRAIL_ICON[m.guardrailLevel]} ${m.guardrailLevel.toUpperCase()}`
62
+ : "";
63
+ return `- (${m.status}${m.pinned ? ", PINNED" : ""}, ${m.kind}${guard}, conf=${m.confidence.toFixed(2)}, id=${m.id})${scope}\n ${m.claim}${evidence}`;
64
+ }
65
+ function renderList(memories) {
66
+ if (memories.length === 0)
67
+ return "No matching memories.";
68
+ return memories.map(renderMemory).join("\n");
69
+ }
70
+ function openStore() {
71
+ // Repo root resolution: AIDIMAG_REPO env var wins, else walk up from cwd.
72
+ const start = process.env.AIDIMAG_REPO ?? process.cwd();
73
+ return MemoryStore.open(start, { create: true });
74
+ }
75
+ async function main() {
76
+ const server = new McpServer({ name: "aidimag", version: PKG_VERSION });
77
+ const store = openStore();
78
+ server.tool("memory_search", "Search the repo's verified memory for decisions, conventions, gotchas, failed approaches, and invariants. Use BEFORE exploring the codebase — past sessions may already know the answer.", {
79
+ query: z.string().describe("Keywords to search for (e.g. 'auth token refresh')"),
80
+ kind: z.enum(KINDS).optional().describe("Filter by memory kind"),
81
+ status: z.enum(STATUSES).optional().describe("Filter by verification status"),
82
+ paths: z.array(z.string()).optional().describe("Restrict to memories scoped to these paths"),
83
+ limit: z.number().int().min(1).max(50).optional(),
84
+ }, async (args) => {
85
+ const { results } = await hybridSearch(store, {
86
+ query: args.query,
87
+ kind: args.kind,
88
+ status: args.status,
89
+ paths: args.paths,
90
+ limit: args.limit,
91
+ });
92
+ try {
93
+ store.logSearch(args.query, args.paths ?? [], results.length, "mcp");
94
+ }
95
+ catch (err) {
96
+ // gap logging is best-effort; never break search
97
+ debugLog("mcp search-gap logging", err);
98
+ }
99
+ let text = renderList(results);
100
+ if (results.length === 0) {
101
+ text +=
102
+ "\n(Coverage gap logged. If you learn the answer this session — from the code or the user — persist it with context_note or memory_propose so future sessions don't hit this gap.)";
103
+ }
104
+ return { content: [{ type: "text", text }] };
105
+ });
106
+ server.tool("memory_get_for_files", "Get all memories relevant to specific files before editing them — conventions, gotchas, and invariants that apply to those paths.", {
107
+ paths: z.array(z.string()).min(1).describe("Repo-relative file paths you are about to read or edit"),
108
+ limit: z.number().int().min(1).max(50).optional(),
109
+ }, async (args) => {
110
+ const results = store.getForFiles(args.paths, args.limit ?? 20);
111
+ return { content: [{ type: "text", text: renderList(results) }] };
112
+ });
113
+ server.tool("memory_write", "Persist a new memory about this codebase. Write the claim as a FALSIFIABLE statement (something that could be checked against the code). Attach evidence whenever possible. For kind=GUARDRAIL, set guardrail_level (never|always|ask-first).", {
114
+ kind: z.enum(KINDS),
115
+ claim: z.string().min(10).describe("Falsifiable statement, e.g. 'All DB access goes through src/db/store.ts; nothing else imports better-sqlite3'"),
116
+ paths: z.array(z.string()).optional().describe("Paths this memory applies to (omit for repo-wide)"),
117
+ symbols: z.array(z.string()).optional().describe("Symbols (functions/classes) this applies to"),
118
+ guardrail_level: z
119
+ .enum(GUARDRAIL_LEVELS)
120
+ .optional()
121
+ .describe("Required for kind=GUARDRAIL: 'never' (refuse), 'always' (do without asking), 'ask-first' (confirm with user)"),
122
+ evidence: z
123
+ .array(z.object({ type: z.enum(EVIDENCE_TYPES), payload: z.string() }))
124
+ .optional()
125
+ .describe("Grounding evidence, e.g. {type:'COMMIT_REF', payload:'abc123'} or {type:'STATIC_CHECK', payload:'grep -rL better-sqlite3 src --include=*.ts'}"),
126
+ created_by: z.string().optional().describe("Agent identifier, e.g. 'claude-code'"),
127
+ }, async (args) => {
128
+ const { safe, stripped } = stripExecutableEvidence(args.evidence);
129
+ const entry = store.write({
130
+ kind: args.kind,
131
+ claim: args.claim,
132
+ paths: args.paths,
133
+ symbols: args.symbols,
134
+ evidence: safe,
135
+ createdBy: args.created_by ?? "agent",
136
+ guardrailLevel: args.guardrail_level,
137
+ });
138
+ await indexMemory(store, entry).catch(() => false);
139
+ const stripNote = stripped > 0
140
+ ? `\n(stripped ${stripped} executable evidence item(s) — use memory_propose + dim review for shell checks)`
141
+ : "";
142
+ return {
143
+ content: [{ type: "text", text: `Memory saved (id=${entry.id}, status=${entry.status}).${stripNote}\n${renderMemory(entry)}` }],
144
+ };
145
+ });
146
+ server.tool("memory_refute", "Mark a memory as REFUTED when you discover it no longer holds. Optionally provide the id of a new memory that supersedes it. Refuted memories are kept as negative knowledge.", {
147
+ id: z.string().describe("Memory id to refute"),
148
+ superseded_by: z.string().optional().describe("Id of a newer memory replacing it"),
149
+ }, async (args) => {
150
+ store.refute(args.id, args.superseded_by);
151
+ return { content: [{ type: "text", text: `Memory ${args.id} marked REFUTED.` }] };
152
+ });
153
+ server.tool("memory_status", "Get a summary of the repo's memory store: counts by verification status and kind.", {}, async () => {
154
+ const s = store.statusSummary();
155
+ const lines = [
156
+ `aidimag memory @ ${s.dbPath}`,
157
+ `total: ${s.total}`,
158
+ `by status: ${Object.entries(s.byStatus).map(([k, v]) => `${k}=${v}`).join(", ")}`,
159
+ `by kind: ${Object.entries(s.byKind).map(([k, v]) => `${k}=${v}`).join(", ") || "(none)"}`,
160
+ `pending proposals: ${s.pendingProposals ?? 0}`,
161
+ ];
162
+ return { content: [{ type: "text", text: lines.join("\n") }] };
163
+ });
164
+ server.tool("memory_verify", "Re-run cheap evidence checks (STATIC_CHECK, COMMIT_REF) and update memory statuses. Use before relying on VERIFIED memories if the repo may have changed, or to verify specific memories by id.", {
165
+ ids: z.array(z.string()).optional().describe("Specific memory ids to verify (prefix ok); omit for all"),
166
+ deep: z.boolean().optional().describe("Also run expensive evidence (TEST_RESULT, EXEC_TRACE). Slower; use when cheap checks aren't enough."),
167
+ }, async (args) => {
168
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
169
+ const report = verifyAll(store, root, { ids: args.ids, deep: args.deep });
170
+ const changes = report.results.filter((r) => r.after !== r.before || r.decayed);
171
+ const lines = [
172
+ `checked ${report.checked}: ${report.verified} verified, ${report.stale} stale, ${report.decayed} decayed, ${report.unchanged} unchanged`,
173
+ ...changes.map((r) => `${r.before} → ${r.after}${r.decayed ? " (decayed)" : ""} (conf ${r.confidenceBefore.toFixed(2)}→${r.confidenceAfter.toFixed(2)}): ${r.claim}`),
174
+ ];
175
+ return { content: [{ type: "text", text: lines.join("\n") }] };
176
+ });
177
+ server.tool("memory_propose", "Propose a memory for the human review queue. Use at SESSION END for learnings that should persist but warrant review before becoming active memory. Prefer this over memory_write for inferred/uncertain knowledge.", {
178
+ kind: z.enum(KINDS),
179
+ claim: z.string().min(10).describe("Falsifiable statement about the codebase"),
180
+ paths: z.array(z.string()).optional(),
181
+ symbols: z.array(z.string()).optional(),
182
+ guardrail_level: z
183
+ .enum(GUARDRAIL_LEVELS)
184
+ .optional()
185
+ .describe("For kind=GUARDRAIL: never | always | ask-first"),
186
+ evidence: z
187
+ .array(z.object({ type: z.enum(EVIDENCE_TYPES), payload: z.string() }))
188
+ .optional(),
189
+ rationale: z.string().optional().describe("Why this is worth remembering (helps the reviewer)"),
190
+ ticket_ref: z
191
+ .string()
192
+ .optional()
193
+ .describe("Ticket id this work belongs to (e.g. XXX-2100). Omit to auto-detect from the current branch."),
194
+ agent_id: z.string().optional().describe("Your agent identifier, e.g. 'claude-code'"),
195
+ }, async (args) => {
196
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
197
+ // the best prompt is the one the branch name already answered
198
+ const ticketRef = args.ticket_ref ?? detectBranchTicket(root) ?? undefined;
199
+ const evidence = [...(args.evidence ?? [])];
200
+ if (ticketRef && !evidence.some((e) => e.type === "TICKET_REF")) {
201
+ evidence.push({ type: "TICKET_REF", payload: ticketRef });
202
+ }
203
+ const p = store.propose({
204
+ kind: args.kind,
205
+ claim: args.claim,
206
+ paths: args.paths,
207
+ symbols: args.symbols,
208
+ evidence: evidence.length ? evidence : undefined,
209
+ rationale: args.rationale,
210
+ ticketRef,
211
+ guardrailLevel: args.guardrail_level,
212
+ source: `session:${args.agent_id ?? "agent"}`,
213
+ });
214
+ if (!p) {
215
+ return { content: [{ type: "text", text: "Duplicate — an identical proposal already exists." }] };
216
+ }
217
+ return {
218
+ content: [
219
+ {
220
+ type: "text",
221
+ text: `Proposal queued for human review (id=${p.id}${ticketRef ? `, ticket=${ticketRef}` : ""}). ` +
222
+ `It becomes active memory only after \`dim review\` approval.`,
223
+ },
224
+ ],
225
+ };
226
+ });
227
+ server.tool("context_note", "Capture a durable fact the USER just stated in chat. Call this IMMEDIATELY when the user shares codebase knowledge — don't wait for session end. ALWAYS trigger on: 'we use X because Y' (DECISION), 'never do X' (GUARDRAIL), 'we always X' (CONVENTION), 'we tried X, it failed' (FAILED_APPROACH), 'the architecture is...' (ARCHITECTURE). Skip task-specific requests like 'fix this bug'. User-stated facts are queued for review with high trust (HUMAN_ATTESTED evidence).", {
228
+ statement: z
229
+ .string()
230
+ .min(10)
231
+ .describe("The fact, rephrased as a falsifiable claim about the codebase (e.g. 'Payments retries are handled in src/queue; handlers must be idempotent')"),
232
+ kind: z.enum(KINDS).describe("Best-fit memory kind (e.g. user says 'never do X' → GUARDRAIL, 'we tried X, failed' → FAILED_APPROACH, 'we always X' → CONVENTION)"),
233
+ quote: z.string().optional().describe("The user's own words, verbatim (preserves nuance for the reviewer)"),
234
+ paths: z.array(z.string()).optional().describe("Repo-relative paths the fact applies to (omit for repo-wide)"),
235
+ symbols: z.array(z.string()).optional().describe("Symbols (functions/classes) it applies to"),
236
+ guardrail_level: z
237
+ .enum(GUARDRAIL_LEVELS)
238
+ .optional()
239
+ .describe("For kind=GUARDRAIL: never | always | ask-first"),
240
+ agent_id: z.string().optional().describe("Your agent identifier, e.g. 'claude-code'"),
241
+ }, async (args) => {
242
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
243
+ const ticketRef = detectBranchTicket(root) ?? undefined;
244
+ // User-stated facts are attestations: HUMAN_ATTESTED evidence verifies once
245
+ // on approval (then decays fastest), giving them a higher-trust start than
246
+ // agent-inferred proposals without pretending they're machine-checkable.
247
+ const evidence = [
248
+ { type: "HUMAN_ATTESTED", payload: args.quote?.trim() || `stated by user in chat, ${new Date().toISOString().slice(0, 10)}` },
249
+ ];
250
+ if (ticketRef)
251
+ evidence.push({ type: "TICKET_REF", payload: ticketRef });
252
+ const p = store.propose({
253
+ kind: args.kind,
254
+ claim: args.statement,
255
+ paths: args.paths,
256
+ symbols: args.symbols,
257
+ evidence,
258
+ rationale: args.quote
259
+ ? `User said (verbatim): "${args.quote.trim()}"`
260
+ : "Stated directly by the user in an AI chat session.",
261
+ ticketRef,
262
+ guardrailLevel: args.guardrail_level,
263
+ source: `context:${args.agent_id ?? "agent"}`,
264
+ });
265
+ if (!p) {
266
+ return { content: [{ type: "text", text: "Already captured — an identical note is in the review queue." }] };
267
+ }
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `Noted (proposal id=${p.id}). Queued for \`dim review\`; continue the conversation — no need to mention this unless asked.`,
273
+ },
274
+ ],
275
+ };
276
+ });
277
+ server.tool("memory_critique", "Review what you just did (or plan to do) against the project's VERIFIED memory and guardrails — a 'second critic' grounded in real, falsifiable beliefs rather than another model's opinion. Call BEFORE committing or proposing memories. Resolve guardrail violations and contradictions first.", {
278
+ summary: z.string().min(10).describe("What you did or plan to do, in a sentence or two"),
279
+ files_changed: z.array(z.string()).optional().describe("Repo-relative paths you touched (improves scoping)"),
280
+ }, async (args) => {
281
+ const result = await critique(store, { summary: args.summary, filesChanged: args.files_changed ?? [] });
282
+ const lines = [];
283
+ if (result.guardrailsViolated.length) {
284
+ lines.push("⚠️ GUARDRAILS — resolve before proceeding:");
285
+ for (const g of result.guardrailsViolated) {
286
+ lines.push(` ${GUARDRAIL_ICON[g.level]} ${g.level.toUpperCase()} — ${g.memory.claim}\n ${g.concern}`);
287
+ }
288
+ }
289
+ if (result.contradictions.length) {
290
+ lines.push("", "Possible contradictions (in-scope rules you didn't confirm):");
291
+ for (const c of result.contradictions)
292
+ lines.push(` • ${c.memory.claim}\n ${c.concern}`);
293
+ }
294
+ if (result.confirmations.length) {
295
+ lines.push("", "Consistent with existing memory:");
296
+ for (const c of result.confirmations.slice(0, 8))
297
+ lines.push(` ✓ (${c.memory.kind}) ${c.memory.claim}`);
298
+ }
299
+ if (result.missingCoverage.length) {
300
+ lines.push("", `No memory covers: ${result.missingCoverage.slice(0, 10).join(", ")} — consider asking the user or writing a memory.`);
301
+ }
302
+ const text = lines.length
303
+ ? lines.join("\n")
304
+ : "No conflicts, guardrail issues, or relevant memory found for this change.";
305
+ return { content: [{ type: "text", text }] };
306
+ });
307
+ server.tool("ticket_get", "Fetch the ticket behind the current work (title, description, type, status) from the connected ticketing app. Use at session end: the ticket carries the WHY that commits lack. Omit the id to auto-detect it from the current branch name.", {
308
+ id: z.string().optional().describe("Ticket id, e.g. XXX-2100 or #123; omit to use the current branch's ticket"),
309
+ }, async (args) => {
310
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
311
+ const id = args.id ?? detectBranchTicket(root);
312
+ if (!id) {
313
+ return { content: [{ type: "text", text: "No ticket id given and none detectable from the current branch name." }] };
314
+ }
315
+ const provider = ticketProviderFor(root);
316
+ if (!provider) {
317
+ return {
318
+ content: [
319
+ { type: "text", text: `Ticket ${id} (no ticketing app connected — the human can run \`dim ticket connect\`). Use the id as ticket_ref anyway.` },
320
+ ],
321
+ };
322
+ }
323
+ try {
324
+ const t = await provider.getTicket(id);
325
+ if (!t)
326
+ return { content: [{ type: "text", text: `Ticket ${id} not found.` }] };
327
+ const lines = [
328
+ `${t.id}: ${t.title}`,
329
+ `type=${t.type} status=${t.status}${t.labels.length ? ` labels=${t.labels.join(",")}` : ""}`,
330
+ ...(t.parent ? [`parent: ${t.parent.id} "${t.parent.title}"`] : []),
331
+ t.url,
332
+ ...(t.body ? ["", t.body] : []),
333
+ ];
334
+ return { content: [{ type: "text", text: lines.join("\n") }] };
335
+ }
336
+ catch (err) {
337
+ return {
338
+ content: [
339
+ { type: "text", text: `Ticket ${id}: provider unreachable (${err instanceof Error ? err.message : err}). Proceed without it; still pass ticket_ref=${id} on proposals.` },
340
+ ],
341
+ };
342
+ }
343
+ });
344
+ server.tool("commits_mine", "Mine git commit history for memory-worthy candidates (same as `dim mine`). Queues proposals for `dim review` — never writes active memory directly. Use after meaningful commits, or with full=true to rescan all history.", {
345
+ full: z
346
+ .boolean()
347
+ .optional()
348
+ .describe("Rescan from the beginning of history (ignore cursor). Default: only commits since the last mine."),
349
+ llm: z
350
+ .boolean()
351
+ .optional()
352
+ .describe("Deep mining: LLM reads each commit message and diff (needs Ollama or OPENAI_API_KEY). Falls back to keyword heuristics if unavailable."),
353
+ max: z
354
+ .number()
355
+ .int()
356
+ .min(1)
357
+ .max(500)
358
+ .optional()
359
+ .describe("Max commits to scan (default 500 for keyword mining, 40 for llm)"),
360
+ }, async (args) => {
361
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
362
+ if (!existsSync(path.join(root, ".git"))) {
363
+ return {
364
+ isError: true,
365
+ content: [{ type: "text", text: "Commit mining requires a git repo (no .git directory found)." }],
366
+ };
367
+ }
368
+ const max = args.max ?? (args.llm ? 40 : 500);
369
+ let llmProvider = null;
370
+ let result;
371
+ if (args.llm) {
372
+ const r = await mineCommitsLlm(store, root, { maxCommits: max, full: Boolean(args.full) });
373
+ llmProvider = r.provider;
374
+ result = r;
375
+ }
376
+ else {
377
+ result = mineCommits(store, root, { maxCommits: max, full: Boolean(args.full) });
378
+ }
379
+ let text = describeMineResult(result, { llmProvider, llmRequested: Boolean(args.llm) });
380
+ if (result.proposed.length) {
381
+ text += "\n\n" + result.proposed.map(proposalSummaryLine).join("\n");
382
+ }
383
+ return { content: [{ type: "text", text }] };
384
+ });
385
+ server.tool("proposals_pending", "List memory proposals awaiting human review.", {
386
+ limit: z.number().int().min(1).max(100).optional(),
387
+ }, async (args) => {
388
+ const pending = store.listProposals("PENDING", args.limit ?? 50);
389
+ const text = pending.length === 0
390
+ ? "No pending proposals."
391
+ : pending.map(proposalSummaryLine).join("\n");
392
+ return { content: [{ type: "text", text }] };
393
+ });
394
+ server.tool("knowledge_pending", "List documents waiting in the knowledge inbox to be summarized into pinned-on-approve memory proposals. Use with the knowledge_ingest prompt to process them in-session.", {}, async () => {
395
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
396
+ const cfg = resolveKnowledgeConfig(root);
397
+ const { pending, toSkip } = await classifyInbox(root, cfg);
398
+ const lines = [];
399
+ lines.push(pending.length ? `${pending.length} doc(s) pending in ${cfg.folder}/:` : `No docs pending in ${cfg.folder}/.`);
400
+ for (const d of pending)
401
+ lines.push(` • ${d.file} (${d.bytes} bytes, sha256 ${d.hash.slice(0, 12)})`);
402
+ if (toSkip.length) {
403
+ lines.push(`${toSkip.length} unsupported file(s) (will move to skipped/ on next sync):`);
404
+ for (const s of toSkip)
405
+ lines.push(` • ${s.file} — ${s.reason}`);
406
+ }
407
+ return { content: [{ type: "text", text: lines.join("\n") }] };
408
+ });
409
+ server.tool("knowledge_ingest_submit", "Submit the FALSIFIABLE claims you extracted from a pending knowledge doc (see the knowledge_ingest prompt). Queues them as proposals (source knowledge:<doc>), writes the durable summary, backs up the original, and clears the inbox copy. Claims become PINNED memory only after `dim review` (unless the repo opted out).", {
410
+ file: z.string().describe("The pending doc's filename, exactly as shown by knowledge_pending"),
411
+ claims: z
412
+ .string()
413
+ .describe('JSON: {"claims":[{"kind":"CONVENTION","claim":"...","paths":["src/x"],"symbols":[],"guardrail_level":null,"rationale":"..."}]} — 0–12 durable, scoped, falsifiable claims'),
414
+ }, async (args) => {
415
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
416
+ const cfg = resolveKnowledgeConfig(root);
417
+ const { pending } = await classifyInbox(root, cfg);
418
+ const doc = pending.find((d) => d.file === args.file);
419
+ if (!doc) {
420
+ return {
421
+ isError: true,
422
+ content: [{ type: "text", text: `No pending doc named '${args.file}' in ${cfg.folder}/ (already processed, or check knowledge_pending).` }],
423
+ };
424
+ }
425
+ const claims = parseClaims(args.claims);
426
+ const result = finalizeDoc(store, root, cfg, { file: doc.file, hash: doc.hash, abs: doc.abs }, claims, "agent:mcp");
427
+ const tail = result.pinned
428
+ ? `auto-approved as ${result.memoryIds.length} ACTIVE (unpinned) memory(ies) — requireReview is off; no human reviewed them.`
429
+ : `queued as ${result.proposalIds.length} proposal(s) — approve with \`dim review\`.`;
430
+ return { content: [{ type: "text", text: `Ingested ${doc.file}: ${result.claimCount} claim(s) ${tail}` }] };
431
+ });
432
+ server.prompt("knowledge_ingest", "Process the knowledge inbox in-session: read each pending doc, extract durable falsifiable claims, and submit them with knowledge_ingest_submit (queued for `dim review`).", async () => {
433
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
434
+ const cfg = resolveKnowledgeConfig(root);
435
+ const { pending, toSkip } = await classifyInbox(root, cfg);
436
+ let text;
437
+ if (pending.length === 0) {
438
+ text =
439
+ `The knowledge inbox (${cfg.folder}/) has no documents to summarize.` +
440
+ (toSkip.length ? ` ${toSkip.length} unsupported file(s) will be moved to skipped/ on the next \`dim knowledge sync\`.` : "");
441
+ }
442
+ else {
443
+ const docs = pending
444
+ .map((d) => `### ${d.file}\n${buildExtractionUser(d.file, d.content)}`)
445
+ .join("\n\n");
446
+ text =
447
+ `${KNOWLEDGE_EXTRACT_INSTRUCTIONS}\n\n` +
448
+ `Process EACH document below independently. For each one, extract its claims and call the \`knowledge_ingest_submit\` tool with that doc's exact \`file\` name and a JSON \`claims\` payload of the shape shown above. Do not skip any document; submit an empty claims array if a doc has no durable facts.\n\n` +
449
+ `Pending documents (${pending.length}):\n\n${docs}`;
450
+ }
451
+ return {
452
+ messages: [
453
+ { role: "user", content: { type: "text", text } },
454
+ ],
455
+ };
456
+ });
457
+ server.prompt("session_end_extraction", "Run at the end of a coding session to extract durable codebase knowledge into the memory proposal queue.", () => {
458
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
459
+ return {
460
+ messages: [
461
+ {
462
+ role: "user",
463
+ content: { type: "text", text: sessionEndPromptFor(detectBranchTicket(root)) },
464
+ },
465
+ ],
466
+ };
467
+ });
468
+ server.prompt("session_start", "Run at the START of a coding session: surfaces in-scope memory, guardrails, stale warnings, and clarifying questions to ask the user before writing any code.", async () => {
469
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
470
+ const briefing = buildSessionBriefing(store, root);
471
+ let text = sessionStartPrompt(briefing);
472
+ // Catch-up trigger: nudge the agent to drain any docs sitting in the knowledge inbox.
473
+ const { pending } = await classifyInbox(root, resolveKnowledgeConfig(root));
474
+ if (pending.length) {
475
+ text +=
476
+ `\n\n---\n📚 ${pending.length} document(s) are waiting in the knowledge inbox ` +
477
+ `(${pending.map((d) => d.file).join(", ")}). Run the \`knowledge_ingest\` prompt to summarize them into reviewable, pinned-on-approve memories.`;
478
+ }
479
+ return {
480
+ messages: [
481
+ {
482
+ role: "user",
483
+ content: { type: "text", text },
484
+ },
485
+ ],
486
+ };
487
+ });
488
+ server.resource("instructions", "aidimag://instructions", async () => {
489
+ return {
490
+ contents: [
491
+ {
492
+ uri: "aidimag://instructions",
493
+ mimeType: "text/plain",
494
+ text: `# aiDimag Instructions\n\n` +
495
+ `When the user states facts about the codebase, IMMEDIATELY capture them with context_note:\n` +
496
+ `• Hosting/deployment details → DECISION\n` +
497
+ `• Design requirements → DECISION\n` +
498
+ `• "We use X because Y" → DECISION\n` +
499
+ `• "Never do X" → GUARDRAIL\n` +
500
+ `• "We always do X" → CONVENTION\n` +
501
+ `• "We tried X, it failed" → FAILED_APPROACH\n` +
502
+ `• Architecture descriptions → ARCHITECTURE\n\n` +
503
+ `Capture facts as they're stated, then continue naturally. Don't ask permission.\n`,
504
+ },
505
+ ],
506
+ };
507
+ });
508
+ server.resource("session-briefing", "aidimag://session-briefing", async () => {
509
+ const root = process.env.AIDIMAG_REPO ?? findRepoRoot() ?? process.cwd();
510
+ const briefing = buildSessionBriefing(store, root);
511
+ return {
512
+ contents: [
513
+ { uri: "aidimag://session-briefing", mimeType: "text/markdown", text: renderBriefing(briefing) },
514
+ ],
515
+ };
516
+ });
517
+ server.resource("digest", "aidimag://digest", async () => {
518
+ const memories = store.list(50).filter((m) => m.status !== "REFUTED");
519
+ const text = memories.length === 0
520
+ ? "No memories recorded for this repo yet."
521
+ : `# Repo memory digest (${memories.length} entries)\n\n${renderList(memories)}`;
522
+ return { contents: [{ uri: "aidimag://digest", mimeType: "text/markdown", text }] };
523
+ });
524
+ const transport = new StdioServerTransport();
525
+ await server.connect(transport);
526
+ console.error(`aidimag MCP server running (db: ${store.dbPath})`);
527
+ }
528
+ main().catch((err) => {
529
+ console.error("aidimag MCP server failed:", err);
530
+ process.exit(1);
531
+ });
532
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,11 @@
1
+ import type { EvidenceType } from "../types.js";
2
+ /** Evidence types that run shell commands on the local machine during verify. */
3
+ export declare const EXECUTABLE_EVIDENCE_TYPES: Set<EvidenceType>;
4
+ export declare function isExecutableEvidence(type: EvidenceType): boolean;
5
+ export declare function stripExecutableEvidence<T extends {
6
+ type: EvidenceType;
7
+ payload: string;
8
+ }>(evidence: T[] | undefined): {
9
+ safe: T[];
10
+ stripped: number;
11
+ };
@@ -0,0 +1,15 @@
1
+ /** Evidence types that run shell commands on the local machine during verify. */
2
+ export const EXECUTABLE_EVIDENCE_TYPES = new Set([
3
+ "STATIC_CHECK",
4
+ "TEST_RESULT",
5
+ "EXEC_TRACE",
6
+ ]);
7
+ export function isExecutableEvidence(type) {
8
+ return EXECUTABLE_EVIDENCE_TYPES.has(type);
9
+ }
10
+ export function stripExecutableEvidence(evidence) {
11
+ const list = evidence ?? [];
12
+ const safe = list.filter((e) => !isExecutableEvidence(e.type));
13
+ return { safe, stripped: list.length - safe.length };
14
+ }
15
+ //# sourceMappingURL=evidence.js.map
@@ -0,0 +1,3 @@
1
+ /** Encrypt a one-time device-flow token for brief DB storage (decrypted on poll). */
2
+ export declare function sealDeviceToken(plaintext: string, secret: string): string;
3
+ export declare function unsealDeviceToken(sealed: string, secret: string): string | null;
@@ -0,0 +1,28 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
2
+ /** Encrypt a one-time device-flow token for brief DB storage (decrypted on poll). */
3
+ export function sealDeviceToken(plaintext, secret) {
4
+ const key = createHash("sha256").update(`aidimag-device:${secret}`).digest();
5
+ const iv = randomBytes(12);
6
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
7
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
8
+ const tag = cipher.getAuthTag();
9
+ return Buffer.concat([iv, tag, enc]).toString("base64url");
10
+ }
11
+ export function unsealDeviceToken(sealed, secret) {
12
+ try {
13
+ const buf = Buffer.from(sealed, "base64url");
14
+ if (buf.length < 29)
15
+ return null;
16
+ const iv = buf.subarray(0, 12);
17
+ const tag = buf.subarray(12, 28);
18
+ const enc = buf.subarray(28);
19
+ const key = createHash("sha256").update(`aidimag-device:${secret}`).digest();
20
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
21
+ decipher.setAuthTag(tag);
22
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ //# sourceMappingURL=seal.js.map
@@ -0,0 +1,2 @@
1
+ import type { SyncItem } from "../sync/server.js";
2
+ export declare function validateSyncPushItems(items: SyncItem[]): SyncItem[];
@@ -0,0 +1,37 @@
1
+ const MAX_PUSH_ITEMS = 500;
2
+ const MAX_PAYLOAD_CHARS = 512_000;
3
+ const MAX_CLAIM_CHARS = 8_000;
4
+ export function validateSyncPushItems(items) {
5
+ if (!Array.isArray(items))
6
+ return [];
7
+ const out = [];
8
+ for (const it of items.slice(0, MAX_PUSH_ITEMS)) {
9
+ if (!it?.id || !it.tbl || !it.updatedAt)
10
+ continue;
11
+ if (it.tbl !== "memories" && it.tbl !== "proposals")
12
+ continue;
13
+ if (typeof it.updatedAt !== "string" || it.updatedAt.length > 64)
14
+ continue;
15
+ if (it.deleted) {
16
+ out.push({ tbl: it.tbl, id: String(it.id), updatedAt: it.updatedAt, deleted: true, payload: null });
17
+ continue;
18
+ }
19
+ if (it.payload === null || it.payload === undefined)
20
+ continue;
21
+ const payloadStr = JSON.stringify(it.payload);
22
+ if (payloadStr.length > MAX_PAYLOAD_CHARS)
23
+ continue;
24
+ const claim = it.payload.claim;
25
+ if (typeof claim === "string" && claim.length > MAX_CLAIM_CHARS)
26
+ continue;
27
+ out.push({
28
+ tbl: it.tbl,
29
+ id: String(it.id),
30
+ updatedAt: it.updatedAt,
31
+ deleted: false,
32
+ payload: it.payload,
33
+ });
34
+ }
35
+ return out;
36
+ }
37
+ //# sourceMappingURL=sync-push.js.map
@@ -0,0 +1,6 @@
1
+ /** Block SSRF to private/link-local/metadata hosts when fetching user-supplied URLs. */
2
+ export declare function isBlockedFetchHost(hostname: string): boolean;
3
+ /** Validate http(s) ticket provider base URLs before server-side fetch. */
4
+ export declare function isAllowedTicketBaseUrl(raw: string): boolean;
5
+ /** Validate cloud/sync server URLs before storing from the local UI. */
6
+ export declare function isAllowedSyncServerUrl(raw: string): boolean;