@balpal4495/quorum 3.5.0 → 3.6.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/README.md CHANGED
@@ -205,6 +205,79 @@ quorum advisor brief
205
205
 
206
206
  ---
207
207
 
208
+ ## LLM setup
209
+
210
+ LLM-powered commands (`advisor`, `evolve`, `check`, `compass`, `serve`) auto-detect whichever provider is available. No config file is needed — Quorum picks the first working option from the list below.
211
+
212
+ **Recommended: use a CLI tool you're already logged into.** No API key required.
213
+
214
+ ### Option 1 — Claude Code CLI (recommended)
215
+
216
+ If you have [Claude Code](https://claude.ai/code) installed and are logged in, Quorum uses it automatically.
217
+
218
+ ```bash
219
+ # Verify it's working
220
+ echo "say hi" | claude --print
221
+ ```
222
+
223
+ Quorum detects the `Claude Code-credentials` keychain entry (macOS) or a session file in `~/.claude/sessions/`. If the command above works, Quorum will use it.
224
+
225
+ ### Option 2 — GitHub Copilot CLI
226
+
227
+ If you have the [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli) installed and are logged in via VS Code, Quorum uses it automatically.
228
+
229
+ ```bash
230
+ # Verify it's working
231
+ copilot -p "say hi"
232
+ ```
233
+
234
+ Quorum detects `~/.copilot/session-state/` having at least one session (created after first auth). If the command above works, Quorum will use it.
235
+
236
+ ### Option 3 — API keys
237
+
238
+ Set any one of these environment variables:
239
+
240
+ | Variable | Provider |
241
+ |---|---|
242
+ | `ANTHROPIC_API_KEY` | Anthropic Claude |
243
+ | `OPENAI_API_KEY` | OpenAI |
244
+ | `GEMINI_API_KEY` | Google Gemini |
245
+ | `OPENAI_BASE_URL` | Any OpenAI-compatible endpoint (Azure, Groq, etc.) |
246
+
247
+ ```bash
248
+ export ANTHROPIC_API_KEY=sk-ant-...
249
+ ```
250
+
251
+ ### Option 4 — Gemini CLI
252
+
253
+ If you have the [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated, Quorum uses it automatically.
254
+
255
+ ```bash
256
+ # Verify it's working
257
+ gemini -p "say hi"
258
+ ```
259
+
260
+ ### Option 5 — Ollama (local, last resort)
261
+
262
+ If Ollama is running at `localhost:11434`, Quorum uses the first available model. Set `OLLAMA_MODEL` to pin a specific model, or `OLLAMA_HOST` for a non-default address.
263
+
264
+ ```bash
265
+ ollama serve
266
+ export OLLAMA_MODEL=llama3.2 # optional
267
+ ```
268
+
269
+ ### Checking what was detected
270
+
271
+ ```bash
272
+ quorum serve # startup line shows: LLM: Claude Code CLI
273
+ ```
274
+
275
+ ### No LLM — still useful
276
+
277
+ Without any provider, `advisor query`, `advisor brief`, `check`, and `coverage` all work with no LLM. Commands output Chronicle evidence and a synthesis request that your agent (Claude Code, Copilot, Codex) can answer inline.
278
+
279
+ ---
280
+
208
281
  ## Upgrading from v1
209
282
 
210
283
  If your project has a `quorum/modules/` folder (the v1 vendored pattern), migrate in one step:
@@ -602,7 +675,7 @@ Refuted entries always elevate risk by at least one level. Citation validation s
602
675
 
603
676
  ### LLM auto-detection
604
677
 
605
- Quorum finds whichever LLM is available: `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` → `OPENAI_BASE_URL` → Ollama at `localhost:11434` authenticated `gemini` CLI. When running inside an AI agent with no separate key, commands output Chronicle evidence and a synthesis request — the agent answers inline.
678
+ Quorum tries providers in this order: **Claude Code CLI** → **Copilot CLI** → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` → `OPENAI_BASE_URL` → Gemini CLIOllama. See [LLM setup](#llm-setup) for how to configure each option.
606
679
 
607
680
  ---
608
681
 
@@ -0,0 +1,108 @@
1
+ import { probeAll, detectProvider } from "../shared/llm.js"
2
+ import { c } from "../shared/colors.js"
3
+
4
+ function row(detected, name, note, suffix = "") {
5
+ const icon = detected ? c.green("✓") : c.dim("·")
6
+ const label = detected ? c.bold(name) : c.dim(name)
7
+ const right = detected
8
+ ? (note ? c.dim(` ${note}`) : "") + (suffix ? ` ${suffix}` : "")
9
+ : (note ? ` ${c.dim(note)}` : "")
10
+ return ` ${icon} ${label.padEnd(26)}${right}`
11
+ }
12
+
13
+ export async function run(args) {
14
+ const test = args.includes("--test")
15
+
16
+ console.log("")
17
+ console.log(`${c.bold("quorum llm")} ${c.dim("— LLM provider status")}`)
18
+ console.log("")
19
+
20
+ // Run all probes in parallel with active provider detection
21
+ const [providers, active] = await Promise.all([probeAll(), detectProvider()])
22
+ const activeName = active?.name ?? null
23
+
24
+ console.log(" Provider scan")
25
+ console.log(c.dim(" ─────────────────────────────────────────────────────"))
26
+
27
+ // Normalize names for active matching (gatherCandidates uses "Gemini", probeAll uses "Gemini API")
28
+ const norm = n => n.replace(/ API$/, "").replace(/ \(.*?\)$/, "").toLowerCase()
29
+
30
+ for (const p of providers) {
31
+ const isActive = p.detected && !!activeName && norm(p.name) === norm(activeName)
32
+
33
+ const badges = []
34
+ if (isActive) badges.push(c.green("← active"))
35
+ if (p.detected && p.id === "ollama" && p.note) badges.push(c.dim(`(${p.note})`))
36
+ const suffix = badges.join(" ")
37
+
38
+ console.log(row(p.detected, p.name, null, suffix))
39
+ if (!p.detected && p.note) {
40
+ console.log(` ${c.dim(p.note)}`)
41
+ }
42
+ }
43
+
44
+ console.log("")
45
+
46
+ if (!activeName) {
47
+ console.log(` ${c.yellow("No provider detected.")}`)
48
+ console.log("")
49
+ printSetupGuide()
50
+ return
51
+ }
52
+
53
+ console.log(` Active: ${c.bold(activeName)}`)
54
+ console.log("")
55
+
56
+ if (!test) {
57
+ console.log(c.dim(" Run 'quorum llm --test' to send a live request and verify it works."))
58
+ console.log("")
59
+ return
60
+ }
61
+
62
+ // ── Live test ──────────────────────────────────────────────────────────────
63
+ process.stdout.write(` Testing ${c.bold(activeName)}… `)
64
+ const t0 = Date.now()
65
+
66
+ try {
67
+ const result = await active.llm([
68
+ { role: "user", content: "Respond with exactly the word OK and nothing else." },
69
+ ])
70
+ const ms = Date.now() - t0
71
+ const ok = /\bOK\b/i.test(result?.trim() ?? "")
72
+ if (ok) {
73
+ console.log(`${c.green("✓")} ${c.dim(`(${ms}ms)`)}`)
74
+ } else {
75
+ console.log(`${c.yellow("✓ (unexpected response)")} ${c.dim(`(${ms}ms)`)}`)
76
+ console.log(c.dim(` → ${(result ?? "").slice(0, 120)}`))
77
+ }
78
+ } catch (err) {
79
+ console.log(c.red("✗"))
80
+ console.log(` ${c.red(err.message?.slice(0, 200) ?? String(err))}`)
81
+ console.log("")
82
+ console.log(` ${c.yellow("The detected provider failed.")} Check that you're signed in, then retry.`)
83
+ }
84
+ console.log("")
85
+ }
86
+
87
+ function printSetupGuide() {
88
+ console.log(" Quickest options:")
89
+ console.log("")
90
+ console.log(` ${c.bold("A")} ${c.bold("Claude Code CLI")} ${c.dim("(no API key needed)")}`)
91
+ console.log(c.dim(" Install and sign in: https://claude.ai/code"))
92
+ console.log(c.dim(" Quorum auto-detects it once you're signed in."))
93
+ console.log("")
94
+ console.log(` ${c.bold("B")} ${c.bold("GitHub Copilot CLI")} ${c.dim("(no API key needed)")}`)
95
+ console.log(c.dim(" Install VS Code + GitHub Copilot Chat extension, then sign in."))
96
+ console.log(c.dim(" Quorum auto-detects it once a session exists."))
97
+ console.log("")
98
+ console.log(` ${c.bold("C")} ${c.bold("API key")} ${c.dim("(Anthropic, OpenAI, or Gemini)")}`)
99
+ console.log(c.dim(" export ANTHROPIC_API_KEY=sk-ant-…"))
100
+ console.log(c.dim(" export OPENAI_API_KEY=sk-…"))
101
+ console.log(c.dim(" export GEMINI_API_KEY=…"))
102
+ console.log("")
103
+ console.log(` ${c.bold("D")} ${c.bold("Ollama")} ${c.dim("(local, free)")}`)
104
+ console.log(c.dim(" brew install ollama && ollama serve && ollama pull llama3.2"))
105
+ console.log("")
106
+ console.log(` ${c.dim("After setup, run 'quorum llm' again to confirm detection.")}`)
107
+ console.log("")
108
+ }
package/bin/quorum.js CHANGED
@@ -23,6 +23,8 @@ ${c.bold("Usage:")}
23
23
  ${c.cyan("quorum advisor")} ${c.dim('"question"')} Ask a plain-language question (uses LLM)
24
24
  ${c.cyan("quorum advisor query")} ${c.dim('"topic"')} Search Chronicle entries (no LLM)
25
25
  ${c.cyan("quorum advisor brief")} High-level Chronicle summary (no LLM)
26
+ ${c.cyan("quorum llm")} Show LLM provider status and setup guide
27
+ ${c.cyan("quorum llm --test")} Send a live test request to the active provider
26
28
  ${c.cyan("quorum init")} Scaffold Quorum into a project
27
29
  ${c.cyan("quorum status")} Show Chronicle health and pending proposals
28
30
  ${c.cyan("quorum check")} --outcome <x> --design <y> Preflight + risk (no LLM)
@@ -116,6 +118,12 @@ async function cli() {
116
118
  return
117
119
  }
118
120
 
121
+ if (command === "llm") {
122
+ const { run } = await import(path.join(__dirname, "commands/llm.js"))
123
+ await run(rest)
124
+ return
125
+ }
126
+
119
127
  if (command === "init") {
120
128
  const { run } = await import(path.join(__dirname, "commands/init.js"))
121
129
  await run(PKG_VERSION)
package/bin/shared/llm.js CHANGED
@@ -11,10 +11,12 @@ const execAsync = promisify(exec)
11
11
  * Detection order (highest priority first):
12
12
  * 1. ANTHROPIC_API_KEY → Anthropic Claude
13
13
  * 2. OPENAI_API_KEY → OpenAI (or compatible via OPENAI_BASE_URL)
14
- * 3. GEMINI_API_KEY Google Gemini (API)
15
- * 4. OPENAI_BASE_URL (no key) OpenAI-compatible endpoint (Azure, Groq, etc.)
16
- * 5. OLLAMA_HOST / localhost:11434Ollama (probed in parallel with the above)
17
- * 6. gemini CLI in PATH Google Gemini (CLI subprocess)
14
+ * 3. OPENAI_BASE_URL (no key) OpenAI-compatible endpoint (Azure, Groq, etc.)
15
+ * 4. GEMINI_API_KEY Google Gemini (API)
16
+ * 5. claude CLI in PATH Claude Code (CLI subprocess, --print mode)
17
+ * 6. copilot CLI in PATH GitHub Copilot CLI (-p / --prompt mode)
18
+ * 7. gemini CLI in PATH → Google Gemini (CLI subprocess)
19
+ * 8. OLLAMA_HOST / localhost:11434 → Ollama (last resort — local models)
18
20
  *
19
21
  * All detected providers are tried in order. A 429 / quota / rate-limit error
20
22
  * from one provider causes a silent fallback to the next rather than a hard
@@ -39,8 +41,10 @@ export async function detectProvider() {
39
41
  */
40
42
  async function gatherCandidates() {
41
43
  // Probe async sources concurrently
42
- const [ollamaModel, geminiCLIAvail] = await Promise.all([
44
+ const [ollamaModel, claudeCLIAvail, copilotCLIAvail, geminiCLIAvail] = await Promise.all([
43
45
  probeOllama(process.env.OLLAMA_HOST || "http://localhost:11434"),
46
+ probeClaudeCLI(),
47
+ probeCopilotCLI(),
44
48
  probeGeminiCLI(),
45
49
  ])
46
50
 
@@ -77,11 +81,17 @@ async function gatherCandidates() {
77
81
  })
78
82
  }
79
83
 
80
- if (ollamaModel) {
81
- const host = process.env.OLLAMA_HOST || "http://localhost:11434"
84
+ if (claudeCLIAvail) {
82
85
  candidates.push({
83
- llm: createOpenAICompatProvider("", `${host}/v1`, ollamaModel),
84
- name: `Ollama (${ollamaModel})`,
86
+ llm: createClaudeCLIProvider(),
87
+ name: "Claude Code CLI",
88
+ })
89
+ }
90
+
91
+ if (copilotCLIAvail) {
92
+ candidates.push({
93
+ llm: createCopilotCLIProvider(),
94
+ name: "Copilot CLI",
85
95
  })
86
96
  }
87
97
 
@@ -92,6 +102,15 @@ async function gatherCandidates() {
92
102
  })
93
103
  }
94
104
 
105
+ // Ollama is last — local models are the slowest fallback
106
+ if (ollamaModel) {
107
+ const host = process.env.OLLAMA_HOST || "http://localhost:11434"
108
+ candidates.push({
109
+ llm: createOpenAICompatProvider("", `${host}/v1`, ollamaModel),
110
+ name: `Ollama (${ollamaModel})`,
111
+ })
112
+ }
113
+
95
114
  return candidates
96
115
  }
97
116
 
@@ -139,6 +158,113 @@ export async function detectLLMName() {
139
158
  return (await detectProvider())?.name ?? null
140
159
  }
141
160
 
161
+ /**
162
+ * Probe all providers and return a priority-ordered status array suitable for
163
+ * display (e.g. quorum llm). Each entry:
164
+ * { id, name, detected: bool, note: string|null }
165
+ * 'detected' = true → provider is ready to use right now
166
+ * 'note' → setup hint when not detected, or detail when detected
167
+ */
168
+ export async function probeAll() {
169
+ // Run async probes in parallel
170
+ const [ollamaModel, claudeDetail, copilotDetail, geminiDetail] = await Promise.all([
171
+ probeOllama(process.env.OLLAMA_HOST || "http://localhost:11434"),
172
+ probeCliDetail("claude", async () => {
173
+ const { platform } = await import("os")
174
+ if (platform() === "darwin") {
175
+ await execAsync("security find-generic-password -s 'Claude Code-credentials' 2>/dev/null")
176
+ return true
177
+ }
178
+ const { readdir } = await import("fs/promises")
179
+ const { homedir } = await import("os")
180
+ const sessions = await readdir(path.join(homedir(), ".claude", "sessions")).catch(() => [])
181
+ return sessions.length > 0
182
+ }),
183
+ probeCliDetail("copilot", async () => {
184
+ const { readdir } = await import("fs/promises")
185
+ const { homedir } = await import("os")
186
+ const sessions = await readdir(path.join(homedir(), ".copilot", "session-state")).catch(() => [])
187
+ return sessions.length > 0
188
+ }),
189
+ probeCliDetail("gemini", async () => {
190
+ if (process.env.GOOGLE_GENAI_USE_VERTEXAI || process.env.GOOGLE_APPLICATION_CREDENTIALS) return true
191
+ const { readFile } = await import("fs/promises")
192
+ const { homedir } = await import("os")
193
+ const raw = await readFile(path.join(homedir(), ".gemini", "settings.json"), "utf8")
194
+ return !!JSON.parse(raw).selectedAuthType
195
+ }),
196
+ ])
197
+
198
+ const hasOpenAIKey = !!process.env.OPENAI_API_KEY
199
+ const hasOpenAIBase = !!process.env.OPENAI_BASE_URL
200
+ let openAIName = "OpenAI API"
201
+ if (hasOpenAIBase && !hasOpenAIKey) {
202
+ try { openAIName = `OpenAI-compatible (${new URL(process.env.OPENAI_BASE_URL).hostname})` } catch { /**/ }
203
+ }
204
+
205
+ return [
206
+ {
207
+ id: "anthropic",
208
+ name: "Anthropic API",
209
+ detected: !!process.env.ANTHROPIC_API_KEY,
210
+ note: process.env.ANTHROPIC_API_KEY ? null : "export ANTHROPIC_API_KEY=sk-ant-…",
211
+ },
212
+ {
213
+ id: "openai",
214
+ name: openAIName,
215
+ detected: hasOpenAIKey || hasOpenAIBase,
216
+ note: hasOpenAIKey || hasOpenAIBase ? null : "export OPENAI_API_KEY=sk-…",
217
+ },
218
+ {
219
+ id: "gemini-api",
220
+ name: "Gemini API",
221
+ detected: !!process.env.GEMINI_API_KEY,
222
+ note: process.env.GEMINI_API_KEY ? null : "export GEMINI_API_KEY=…",
223
+ },
224
+ {
225
+ id: "claude-cli",
226
+ name: "Claude Code CLI",
227
+ detected: claudeDetail.authed,
228
+ note: claudeDetail.authed ? null
229
+ : claudeDetail.inPath ? "found in PATH but not signed in — run: claude"
230
+ : "install from claude.ai/code, then sign in once",
231
+ },
232
+ {
233
+ id: "copilot-cli",
234
+ name: "Copilot CLI",
235
+ detected: copilotDetail.authed,
236
+ note: copilotDetail.authed ? null
237
+ : copilotDetail.inPath ? "found but no session — open VS Code and sign into Copilot Chat"
238
+ : "install VS Code + GitHub Copilot Chat extension",
239
+ },
240
+ {
241
+ id: "gemini-cli",
242
+ name: "Gemini CLI",
243
+ detected: geminiDetail.authed,
244
+ note: geminiDetail.authed ? null
245
+ : geminiDetail.inPath ? "found but not authenticated — run: gemini auth"
246
+ : "npm install -g @google/gemini-cli, then: gemini auth",
247
+ },
248
+ {
249
+ id: "ollama",
250
+ name: "Ollama",
251
+ detected: !!ollamaModel,
252
+ note: ollamaModel ?? "run: ollama serve (then: ollama pull llama3.2)",
253
+ },
254
+ ]
255
+ }
256
+
257
+ /**
258
+ * Probe a CLI binary — returns { inPath, authed }.
259
+ * authCheck is called only when the binary is found; errors are treated as "not authed".
260
+ */
261
+ async function probeCliDetail(binary, authCheck) {
262
+ let inPath = false
263
+ try { await execAsync(`which ${binary}`); inPath = true } catch { /**/ }
264
+ if (!inPath) return { inPath: false, authed: false }
265
+ try { const authed = await authCheck(); return { inPath: true, authed: !!authed } } catch { return { inPath: true, authed: false } }
266
+ }
267
+
142
268
  // ── Probe Ollama ───────────────────────────────────────────────────────────────
143
269
 
144
270
  async function probeOllama(host) {
@@ -236,6 +362,115 @@ function createGeminiProvider(apiKey) {
236
362
  }
237
363
  }
238
364
 
365
+ async function probeClaudeCLI() {
366
+ // Must be in PATH
367
+ try {
368
+ await execAsync("which claude")
369
+ } catch {
370
+ return false
371
+ }
372
+
373
+ // Must have an active session — Claude Code stores OAuth tokens in the macOS keychain.
374
+ // A quick smoke-test is cheaper than shelling out; check the keychain entry exists.
375
+ // On non-macOS, fall back to checking ~/.claude/sessions/ for any session file.
376
+ try {
377
+ const { platform } = await import("os")
378
+ if (platform() === "darwin") {
379
+ await execAsync("security find-generic-password -s 'Claude Code-credentials' 2>/dev/null")
380
+ return true
381
+ }
382
+ // Non-macOS: check for at least one session file
383
+ const { readdir } = await import("fs/promises")
384
+ const { homedir } = await import("os")
385
+ const sessions = await readdir(path.join(homedir(), ".claude", "sessions")).catch(() => [])
386
+ return sessions.length > 0
387
+ } catch {
388
+ return false
389
+ }
390
+ }
391
+
392
+ function createClaudeCLIProvider() {
393
+ return function llm(messages) {
394
+ return new Promise((resolve, reject) => {
395
+ const system = messages.find(m => m.role === "system")?.content ?? ""
396
+ const userContent = messages.filter(m => m.role !== "system").map(m => m.content).join("\n\n")
397
+
398
+ // Combine system + user into one prompt piped via stdin; --print for non-interactive output.
399
+ // Do NOT use --bare — that disables keychain OAuth and requires ANTHROPIC_API_KEY instead.
400
+ const fullPrompt = system ? `${system}\n\n${userContent}` : userContent
401
+ const child = spawn("claude", ["--print", "--output-format", "text"], { stdio: ["pipe", "pipe", "pipe"] })
402
+
403
+ let out = "", err = ""
404
+ child.stdout.on("data", d => { out += d })
405
+ child.stderr.on("data", d => { err += d })
406
+ child.on("error", reject)
407
+ child.on("close", code => {
408
+ if (code === 0) resolve(out.trim())
409
+ else reject(new Error(`claude CLI exited ${code}: ${err.slice(0, 200)}`))
410
+ })
411
+ child.stdin.write(fullPrompt)
412
+ child.stdin.end()
413
+ })
414
+ }
415
+ }
416
+
417
+ async function probeCopilotCLI() {
418
+ // Must be in PATH (VS Code installs a shell wrapper into PATH automatically)
419
+ try {
420
+ await execAsync("which copilot")
421
+ } catch {
422
+ return false
423
+ }
424
+
425
+ // Must have at least one session in ~/.copilot/session-state/ — created after first auth
426
+ try {
427
+ const { readdir } = await import("fs/promises")
428
+ const { homedir } = await import("os")
429
+ const sessions = await readdir(path.join(homedir(), ".copilot", "session-state")).catch(() => [])
430
+ return sessions.length > 0
431
+ } catch {
432
+ return false
433
+ }
434
+ }
435
+
436
+ function createCopilotCLIProvider() {
437
+ return function llm(messages) {
438
+ return new Promise((resolve, reject) => {
439
+ const system = messages.find(m => m.role === "system")?.content ?? ""
440
+ const userContent = messages.filter(m => m.role !== "system").map(m => m.content).join("\n\n")
441
+ const fullPrompt = system ? `${system}\n\n${userContent}` : userContent
442
+
443
+ // Use --output-format json so we can parse the JSONL event stream reliably
444
+ const child = spawn("copilot", ["-p", fullPrompt, "--output-format", "json"], {
445
+ stdio: ["ignore", "pipe", "pipe"],
446
+ })
447
+
448
+ let out = "", err = ""
449
+ child.stdout.on("data", d => { out += d })
450
+ child.stderr.on("data", d => { err += d })
451
+ child.on("error", reject)
452
+ child.on("close", code => {
453
+ if (code !== 0) {
454
+ return reject(new Error(`copilot CLI exited ${code}: ${err.slice(0, 200)}`))
455
+ }
456
+ // Parse JSONL stream — find the assistant.message event
457
+ const content = out.split("\n")
458
+ .filter(Boolean)
459
+ .reduce((found, line) => {
460
+ if (found) return found
461
+ try {
462
+ const evt = JSON.parse(line)
463
+ if (evt.type === "assistant.message" && evt.data?.content) return evt.data.content
464
+ } catch { /* skip non-JSON lines */ }
465
+ return null
466
+ }, null)
467
+ if (content == null) return reject(new Error("copilot CLI: no assistant.message event in output"))
468
+ resolve(content)
469
+ })
470
+ })
471
+ }
472
+ }
473
+
239
474
  async function probeGeminiCLI() {
240
475
  try {
241
476
  await execAsync("which gemini")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Git-backed memory and design review for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",