@agentskit/doc-bridge 0.1.0-alpha.2 → 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 (57) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +139 -57
  3. package/action.yml +78 -0
  4. package/dist/cli/program.js +1122 -114
  5. package/dist/cli/program.js.map +1 -1
  6. package/dist/index.d.ts +258 -28
  7. package/dist/index.js +824 -54
  8. package/dist/index.js.map +1 -1
  9. package/docs/DOGFOOD-ROUND2.md +142 -0
  10. package/docs/DOGFOOD-ROUND3.md +74 -0
  11. package/docs/POSITIONING.md +2 -0
  12. package/docs/RELEASE.md +5 -3
  13. package/docs/chat-and-rag.md +2 -0
  14. package/docs/getting-started.md +14 -1
  15. package/docs/landing/index.html +299 -0
  16. package/docs/mcp.md +10 -1
  17. package/docs/ollama-demo.md +64 -0
  18. package/docs/playbook/doc-bridge-pattern.md +114 -0
  19. package/docs/recipes/index-pipeline.md +89 -0
  20. package/docs/skills/doc-bridge.md +64 -0
  21. package/docs/spec/cli.md +6 -0
  22. package/docs/spec/playbook-feedback.md +12 -4
  23. package/examples/demo-example/doc-bridge.config.json +23 -0
  24. package/examples/demo-example/docs/for-agents/INDEX.md +3 -0
  25. package/examples/demo-example/docs/for-agents/packages/example.md +14 -0
  26. package/examples/demo-example/package.json +7 -0
  27. package/examples/demo-example/src/.gitkeep +0 -0
  28. package/examples/demo-monorepo/doc-bridge.config.json +37 -0
  29. package/examples/demo-monorepo/docs/for-agents/INDEX.md +4 -0
  30. package/examples/demo-monorepo/docs/for-agents/packages/auth.md +22 -0
  31. package/examples/demo-monorepo/docs/for-agents/packages/billing.md +19 -0
  32. package/examples/demo-monorepo/docs/human/guides/auth.md +7 -0
  33. package/examples/demo-monorepo/package.json +7 -0
  34. package/examples/demo-monorepo/packages/auth/package.json +8 -0
  35. package/examples/demo-monorepo/packages/billing/package.json +7 -0
  36. package/examples/demo-monorepo/pnpm-workspace.yaml +2 -0
  37. package/examples/ollama-chat.config.ts +42 -0
  38. package/package.json +12 -9
  39. package/src/cli/demo.ts +143 -0
  40. package/src/cli/program.ts +220 -26
  41. package/src/doctor/badge.ts +53 -0
  42. package/src/doctor/run-doctor.ts +286 -0
  43. package/src/federation/llms.ts +20 -11
  44. package/src/index-builder/build-handoffs.ts +35 -2
  45. package/src/index-builder/scan-corpus.ts +5 -1
  46. package/src/index-builder/watch-index.ts +94 -0
  47. package/src/index.ts +24 -0
  48. package/src/lib/markdown.ts +31 -4
  49. package/src/mcp/install.ts +84 -0
  50. package/src/memory/github-pr.ts +190 -0
  51. package/src/playbook/doc-bridge-pattern.ts +121 -0
  52. package/src/query/query.ts +19 -1
  53. package/src/query/search.ts +85 -17
  54. package/src/schemas/agent-handoff.ts +11 -0
  55. package/src/schemas/doc-bridge-index.ts +3 -1
  56. package/src/schemas/json-schemas.ts +2 -1
  57. package/src/version.ts +1 -1
@@ -0,0 +1,143 @@
1
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
7
+ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
8
+ import { runGates } from '../gates/run-gates.js'
9
+ import { mcpSnippet } from '../mcp/install.js'
10
+ import { loadDocBridgeIndex } from '../query/load-index.js'
11
+ import { runQuery } from '../query/query.js'
12
+ import type { AgentHandoffV1 } from '../schemas/agent-handoff.js'
13
+
14
+ export type DemoFixture = 'example' | 'monorepo'
15
+
16
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
17
+
18
+ const fixturePath = (fixture: DemoFixture): string => {
19
+ if (fixture === 'monorepo') {
20
+ return join(packageRoot, 'examples', 'demo-monorepo')
21
+ }
22
+ return join(packageRoot, 'examples', 'demo-example')
23
+ }
24
+
25
+ const formatHandoffText = (handoff: AgentHandoffV1): string[] => {
26
+ const bridge =
27
+ handoff.bridge?.humanDoc === 'missing'
28
+ ? `human guide: missing → ${handoff.bridge.action ?? 'ak-docs bootstrap agent-docs'}`
29
+ : handoff.humanDoc
30
+ ? `human guide: ${handoff.humanDoc}`
31
+ : 'human guide: (none)'
32
+
33
+ return [
34
+ `target: ${handoff.target.id} (${handoff.target.path ?? 'n/a'})`,
35
+ `start: ${handoff.startHere}`,
36
+ `edit: ${handoff.editRoots.join(', ')}`,
37
+ `checks: ${handoff.checks.length ? handoff.checks.join(' · ') : '(none)'}`,
38
+ bridge,
39
+ ...(handoff.notes.length ? [`notes: ${handoff.notes[0]}`] : []),
40
+ ]
41
+ }
42
+
43
+ export type DemoResult = {
44
+ readonly ok: boolean
45
+ readonly fixture: DemoFixture
46
+ readonly handoff: AgentHandoffV1
47
+ readonly gateBefore: { readonly ok: boolean; readonly message: string }
48
+ readonly gateAfter: { readonly ok: boolean; readonly message: string }
49
+ readonly mcpSnippet: string
50
+ readonly nextCommands: readonly string[]
51
+ }
52
+
53
+ export const runDemo = (
54
+ root: string,
55
+ config: DocBridgeConfigV1,
56
+ fixture: DemoFixture = 'example',
57
+ options: { readonly copyFixture?: boolean } = {},
58
+ ): DemoResult => {
59
+ const targetPackage = fixture === 'monorepo' ? 'auth' : 'example'
60
+ const fixtureDir = fixturePath(fixture)
61
+
62
+ if (options.copyFixture && existsSync(fixtureDir)) {
63
+ for (const rel of ['doc-bridge.config.json', 'docs', 'packages', 'pnpm-workspace.yaml', 'package.json']) {
64
+ const src = join(fixtureDir, rel)
65
+ if (!existsSync(src)) continue
66
+ const dest = join(root, rel)
67
+ cpSync(src, dest, { recursive: true })
68
+ }
69
+ }
70
+
71
+ const gateBeforeStale = runGates(root, config, ['index-freshness'])
72
+ const beforeGate = gateBeforeStale.results[0] ?? { ok: false, message: 'index-freshness unavailable' }
73
+
74
+ buildDocBridgeIndex({ root, config })
75
+ const index = loadDocBridgeIndex(root, config)
76
+ const handoff = runQuery(index, config, {
77
+ kind: 'package',
78
+ id: targetPackage,
79
+ agent: true,
80
+ }) as AgentHandoffV1
81
+
82
+ const gateAfter = runGates(root, config, ['index-freshness'])
83
+ const afterGate = gateAfter.results[0] ?? { ok: true, message: 'Index is fresh' }
84
+
85
+ return {
86
+ ok: afterGate.ok,
87
+ fixture,
88
+ handoff,
89
+ gateBefore: { ok: beforeGate.ok, message: beforeGate.message },
90
+ gateAfter: { ok: afterGate.ok, message: afterGate.message },
91
+ mcpSnippet: mcpSnippet(root),
92
+ nextCommands: [
93
+ `ak-docs query package ${targetPackage} --agent`,
94
+ 'ak-docs doctor --text',
95
+ 'ak-docs mcp install --cursor',
96
+ 'ak-docs ask "where do I change auth?"',
97
+ ],
98
+ }
99
+ }
100
+
101
+ export const formatDemoText = (result: DemoResult): string[] => {
102
+ const lines = [
103
+ 'ak-docs demo — AgentHandoff in 60s',
104
+ '═'.repeat(44),
105
+ '',
106
+ 'Before (agent guesses package)',
107
+ ' ✗ edits packages/billing when task mentions "auth"',
108
+ ' ✗ runs repo-wide test instead of package checks',
109
+ '',
110
+ 'After (handoff.resolve / query --agent)',
111
+ ...formatHandoffText(result.handoff).map((line) => ` ✓ ${line}`),
112
+ '',
113
+ `Gate: ${result.gateBefore.ok ? 'green' : 'red'} → ${result.gateAfter.ok ? 'green' : 'red'}`,
114
+ ` before: ${result.gateBefore.message}`,
115
+ ` after: ${result.gateAfter.message}`,
116
+ '',
117
+ 'MCP snippet (.cursor/mcp.json)',
118
+ ...result.mcpSnippet.split('\n').map((line) => ` ${line}`),
119
+ '',
120
+ 'Next',
121
+ ...result.nextCommands.map((cmd) => ` → ${cmd}`),
122
+ ]
123
+ return lines
124
+ }
125
+
126
+ /** Ephemeral demo workspace for `ak-docs demo` without polluting cwd. */
127
+ export const withDemoWorkspace = (
128
+ fixture: DemoFixture,
129
+ fn: (root: string, config: DocBridgeConfigV1) => DemoResult,
130
+ ): DemoResult => {
131
+ const dir = mkdtempSync(join(tmpdir(), 'ak-docs-demo-'))
132
+ try {
133
+ const fixtureDir = fixturePath(fixture)
134
+ if (!existsSync(fixtureDir)) {
135
+ throw new Error(`Demo fixture "${fixture}" not found at ${fixtureDir}`)
136
+ }
137
+ cpSync(fixtureDir, dir, { recursive: true })
138
+ const config = JSON.parse(readFileSync(join(dir, 'doc-bridge.config.json'), 'utf8')) as DocBridgeConfigV1
139
+ return fn(dir, config)
140
+ } finally {
141
+ rmSync(dir, { recursive: true, force: true })
142
+ }
143
+ }
@@ -15,6 +15,16 @@ import { createDocBridgeRag } from '../intelligence/rag.js'
15
15
  import { firstHeading, firstParagraph } from '../lib/markdown.js'
16
16
  import { ingestMemoryCandidates } from '../memory/ingest.js'
17
17
  import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
18
+ import { promoteMemoryToGithubPr } from '../memory/github-pr.js'
19
+ import { watchDocBridgeIndex } from '../index-builder/watch-index.js'
20
+ import {
21
+ formatDoctorBadgeJson,
22
+ formatDoctorBadgeMarkdown,
23
+ } from '../doctor/badge.js'
24
+ import { docBridgePatternMarkdown, docBridgePatternPayload } from '../playbook/doc-bridge-pattern.js'
25
+ import { formatDemoText, runDemo, withDemoWorkspace, type DemoFixture } from './demo.js'
26
+ import { formatDoctorText, runDoctor } from '../doctor/run-doctor.js'
27
+ import { installMcpConfig, mcpSnippet } from '../mcp/install.js'
18
28
  import { startMcpStdioServer } from '../mcp/server.js'
19
29
  import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
20
30
  import { runQuery, type QueryKind } from '../query/query.js'
@@ -36,6 +46,8 @@ type Command =
36
46
  | 'index'
37
47
  | 'gate'
38
48
  | 'mcp'
49
+ | 'doctor'
50
+ | 'demo'
39
51
  | 'query'
40
52
  | 'search'
41
53
  | 'retrieve'
@@ -48,14 +60,17 @@ const usage = `ak-docs — human↔agent documentation bridge (@agentskit/doc-br
48
60
 
49
61
  Core (no API key):
50
62
  ak-docs init [--demo] [--scaffold-workspaces]
51
- ak-docs index
63
+ ak-docs demo [--fixture example|monorepo] [--text] [--in-project]
64
+ ak-docs doctor [--text] [--badge] [--write-badge]
65
+ ak-docs index [--watch]
52
66
  ak-docs query <package|ownership|intent|change> <id> [--agent] [--text]
53
67
  ak-docs search <term> [--agent] [--text]
54
68
  ak-docs list <packages|intents|changes|knowledge> [--text]
55
69
  ak-docs ask [question] local consult (no LLM)
56
70
  ak-docs gate run [gate-id]
57
71
  ak-docs mcp
58
- ak-docs memory ingest|classify|promote
72
+ ak-docs mcp install --cursor | --claude
73
+ ak-docs memory ingest|classify|promote [--pr] [--dry-run]
59
74
  ak-docs bootstrap agent-docs
60
75
  ak-docs validate-config | validate-handoff <file>
61
76
 
@@ -67,7 +82,7 @@ Intelligence (optional AgentsKit peers):
67
82
  Advanced / ecosystem:
68
83
  ak-docs retrieve <query>
69
84
  ak-docs registry topology
70
- ak-docs playbook draft
85
+ ak-docs playbook draft | pattern [--text]
71
86
 
72
87
  Global flags:
73
88
  -h, --help --version
@@ -112,6 +127,8 @@ const parseArgs = (argv: readonly string[]) => {
112
127
  else if (positional[0] === 'index') command = 'index'
113
128
  else if (positional[0] === 'gate') command = 'gate'
114
129
  else if (positional[0] === 'mcp') command = 'mcp'
130
+ else if (positional[0] === 'doctor') command = 'doctor'
131
+ else if (positional[0] === 'demo') command = 'demo'
115
132
  else if (positional[0] === 'query') command = 'query'
116
133
  else if (positional[0] === 'search') command = 'search'
117
134
  else if (positional[0] === 'retrieve') command = 'retrieve'
@@ -160,9 +177,11 @@ const writeTextQuery = (payload: unknown): void => {
160
177
  writeLines([
161
178
  `Search: ${String(data.term ?? '')}`,
162
179
  `Matches: ${String(data.count ?? matches.length)}`,
163
- ...matches.map((match) =>
164
- [match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
165
- ),
180
+ ...(matches.length
181
+ ? matches.map((match) =>
182
+ formatSearchMatch(match as { type: string; id: string; path: string; summary?: string }),
183
+ )
184
+ : [' (none)']),
166
185
  ])
167
186
  return
168
187
  }
@@ -170,6 +189,18 @@ const writeTextQuery = (payload: unknown): void => {
170
189
  writeLines([textValue(result.data)])
171
190
  }
172
191
 
192
+ const formatSearchMatch = (match: {
193
+ readonly type: string
194
+ readonly id: string
195
+ readonly path: string
196
+ readonly summary?: string
197
+ readonly score?: number
198
+ }): string => {
199
+ const summary = match.summary ? match.summary.replace(/\s+/g, ' ').slice(0, 100) : ''
200
+ const score = typeof match.score === 'number' ? ` score=${match.score}` : ''
201
+ return ` [${match.type}] ${match.id}${score}\n ${match.path}${summary ? `\n ${summary}` : ''}`
202
+ }
203
+
173
204
  const writeTextSearch = (
174
205
  term: string,
175
206
  matches: readonly {
@@ -177,39 +208,76 @@ const writeTextSearch = (
177
208
  readonly id: string
178
209
  readonly path: string
179
210
  readonly summary?: string
211
+ readonly score?: number
180
212
  }[],
181
213
  ): void => {
182
214
  writeLines([
183
215
  `Search: ${term}`,
184
216
  `Matches: ${matches.length}`,
185
- ...matches.map((match) =>
186
- [match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
187
- ),
217
+ ...(matches.length ? matches.map(formatSearchMatch) : [' (none)']),
188
218
  ])
189
219
  }
190
220
 
221
+ const handoffSummaryLines = (
222
+ index: DocBridgeIndexV1,
223
+ config: DocBridgeConfigV1,
224
+ ownerId: string,
225
+ ): string[] => {
226
+ try {
227
+ const handoff = runQuery(index, config, { kind: 'ownership', id: ownerId, agent: true })
228
+ if (!handoff || typeof handoff !== 'object' || !('editRoots' in handoff)) return []
229
+ const payload = handoff as {
230
+ startHere?: string
231
+ editRoots?: string[]
232
+ checks?: string[]
233
+ humanDoc?: string | null
234
+ bridge?: { humanDoc?: string; action?: string }
235
+ }
236
+ const bridgeLine =
237
+ payload.bridge?.humanDoc === 'missing'
238
+ ? `Bridge: human guide missing → ${payload.bridge.action ?? 'ak-docs bootstrap agent-docs'}`
239
+ : payload.humanDoc
240
+ ? `Bridge: ${payload.humanDoc}`
241
+ : 'Bridge: (no human plugin configured)'
242
+ return [
243
+ '',
244
+ 'Handoff preview',
245
+ ` start: ${payload.startHere ?? '(unknown)'}`,
246
+ ` edit: ${(payload.editRoots ?? []).join(', ') || '(none)'}`,
247
+ ` checks: ${(payload.checks ?? []).join(' · ') || '(none)'}`,
248
+ ` ${bridgeLine}`,
249
+ ]
250
+ } catch {
251
+ return []
252
+ }
253
+ }
254
+
191
255
  const writeAsk = (
192
256
  question: string,
193
257
  matches: ReturnType<typeof searchIndex>,
194
258
  index: DocBridgeIndexV1,
259
+ config: DocBridgeConfigV1,
195
260
  ): void => {
196
- const best = matches[0]
197
- const owner = matches.find((match) => match.type === 'ownership' || index.lookup?.ownership?.[match.id])
198
- const bestQuery =
199
- owner
200
- ? `ak-docs query ownership ${owner.id} --agent`
201
- : 'ak-docs list knowledge --text'
261
+ // Prefer ownership match for routing questions
262
+ const owner =
263
+ matches.find((match) => match.type === 'ownership') ??
264
+ matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]))
265
+ const best = owner ?? matches[0]
266
+ const ownerId =
267
+ best && (best.type === 'ownership' || index.lookup?.ownership?.[best.id]) ? best.id : undefined
268
+ const bestQuery = ownerId
269
+ ? `ak-docs query ownership ${ownerId} --agent`
270
+ : 'ak-docs list knowledge --text'
202
271
  writeLines([
203
272
  `Question: ${question}`,
204
273
  best ? `Best match: ${best.type} ${best.id} (${best.path})` : 'Best match: none',
274
+ ...(ownerId ? handoffSummaryLines(index, config, ownerId) : []),
205
275
  '',
206
276
  'Matches:',
207
277
  ...(
208
278
  matches.length
209
- ? matches.slice(0, 5).map((match) =>
210
- [match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
211
- )
212
- : ['No local matches. Try: ak-docs search <term>']
279
+ ? matches.slice(0, 5).map(formatSearchMatch)
280
+ : [' No local matches. Try: ak-docs search <term>']
213
281
  ),
214
282
  '',
215
283
  'Next commands:',
@@ -217,8 +285,9 @@ const writeAsk = (
217
285
  ? [
218
286
  `ak-docs search "${question}" --agent`,
219
287
  bestQuery,
288
+ ...(ownerId ? [`ak-docs doctor --text`] : []),
220
289
  ]
221
- : ['ak-docs list knowledge --text']),
290
+ : ['ak-docs list knowledge --text', 'ak-docs doctor --text']),
222
291
  ])
223
292
  }
224
293
 
@@ -257,7 +326,7 @@ const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<numb
257
326
  const gateId = value || undefined
258
327
  writeJson(runGates(root, config, gateId ? [gateId as GateId] : undefined))
259
328
  } else {
260
- writeAsk(line, searchIndex(index, line, 8), index)
329
+ writeAsk(line, searchIndex(index, line, 8), index, config)
261
330
  }
262
331
  } catch (error) {
263
332
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -591,7 +660,9 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
591
660
 
592
661
  if (command === 'memory') {
593
662
  if (!['ingest', 'classify', 'promote'].includes(positional[1] ?? '')) {
594
- process.stderr.write('Usage: ak-docs memory <ingest|classify|promote> [--config <path>]\n')
663
+ process.stderr.write(
664
+ 'Usage: ak-docs memory <ingest|classify|promote> [--pr] [--dry-run] [--force] [--config <path>]\n',
665
+ )
595
666
  return 1
596
667
  }
597
668
  try {
@@ -608,6 +679,23 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
608
679
  return 0
609
680
  }
610
681
  const draft = draftMemoryPromotion(classifications)
682
+ if (flags.has('--pr') || flags.has('--github')) {
683
+ const pr = promoteMemoryToGithubPr(root, draft, {
684
+ dryRun: flags.has('--dry-run'),
685
+ force: flags.has('--force'),
686
+ })
687
+ if (flags.has('--text')) {
688
+ writeLines([
689
+ pr.message,
690
+ ...(pr.draftPath ? [`Draft: ${pr.draftPath}`] : []),
691
+ ...(pr.prUrl ? [`PR: ${pr.prUrl}`] : []),
692
+ ...(pr.commands.length ? ['', 'Commands:', ...pr.commands.map((cmd) => ` ${cmd}`)] : []),
693
+ ])
694
+ } else {
695
+ writeJson({ ...draft, pr })
696
+ }
697
+ return pr.ok ? 0 : 1
698
+ }
611
699
  writeJson(draft)
612
700
  return draft.ok ? 0 : 1
613
701
  } catch (error) {
@@ -626,8 +714,18 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
626
714
  }
627
715
 
628
716
  if (command === 'playbook') {
629
- if (positional[1] !== 'draft') {
630
- process.stderr.write('Usage: ak-docs playbook draft [--config <path>]\n')
717
+ const action = positional[1]
718
+ if (action === 'pattern') {
719
+ const payload = docBridgePatternPayload()
720
+ if (flags.has('--text') || wantsTextOutput(flags, { schemaVersion: 1, corpus: { agent: { root: 'docs' } } } as DocBridgeConfigV1)) {
721
+ process.stdout.write(`${docBridgePatternMarkdown()}\n`)
722
+ } else {
723
+ writeJson(payload)
724
+ }
725
+ return 0
726
+ }
727
+ if (action !== 'draft') {
728
+ process.stderr.write('Usage: ak-docs playbook draft | pattern [--text] [--config <path>]\n')
631
729
  return 1
632
730
  }
633
731
  try {
@@ -638,6 +736,8 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
638
736
  ...draft,
639
737
  title: 'Draft Playbook feedback promotion',
640
738
  pattern: 'Doc Bridge Pattern',
739
+ patternDoc: 'docs/playbook/doc-bridge-pattern.md',
740
+ exportCommand: 'ak-docs playbook pattern --text',
641
741
  })
642
742
  return 0
643
743
  } catch (error) {
@@ -648,7 +748,14 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
648
748
 
649
749
  if (command === 'index') {
650
750
  try {
651
- const { config, root } = loadProject(configPath)
751
+ const { config, root, configPath: loadedConfigPath } = loadProject(configPath)
752
+ if (flags.has('--watch')) {
753
+ return watchDocBridgeIndex({
754
+ root,
755
+ config,
756
+ configPath: loadedConfigPath,
757
+ })
758
+ }
652
759
  const result = buildDocBridgeIndex({ root, config })
653
760
  const diagnostics = indexDiagnostics(config, result)
654
761
  const handoffCount = Object.keys(result.index.handoffs ?? {}).length
@@ -696,6 +803,34 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
696
803
  }
697
804
 
698
805
  if (command === 'mcp') {
806
+ if (positional[1] === 'install') {
807
+ const target = flags.has('--claude') ? 'claude' : flags.has('--cursor') ? 'cursor' : undefined
808
+ if (!target) {
809
+ process.stderr.write('Usage: ak-docs mcp install --cursor | --claude\n')
810
+ return 1
811
+ }
812
+ try {
813
+ const { config, root } = loadProject(configPath)
814
+ const result = installMcpConfig(root, target)
815
+ if (wantsTextOutput(flags, config)) {
816
+ writeLines([
817
+ `Installed MCP server "${result.serverName}" for ${result.target}`,
818
+ `Config: ${result.configPath}`,
819
+ ...(result.created ? ['Created new config file'] : ['Merged into existing config']),
820
+ '',
821
+ 'Next steps:',
822
+ ...result.nextSteps.map((step) => ` → ${step}`),
823
+ ])
824
+ } else {
825
+ writeJson({ ...result, snippet: mcpSnippet(root) })
826
+ }
827
+ return 0
828
+ } catch (error) {
829
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
830
+ return 1
831
+ }
832
+ }
833
+
699
834
  try {
700
835
  const { config, root } = loadProject(configPath)
701
836
  startMcpStdioServer({ root, config })
@@ -706,6 +841,65 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
706
841
  }
707
842
  }
708
843
 
844
+ if (command === 'doctor') {
845
+ try {
846
+ const { config, root } = loadProject(configPath)
847
+ const report = runDoctor(root, config)
848
+ if (flags.has('--write-badge')) {
849
+ const badgePath = resolve(root, '.doc-bridge', 'coverage-badge.json')
850
+ mkdirSync(dirname(badgePath), { recursive: true })
851
+ writeFileSync(badgePath, `${formatDoctorBadgeJson(report.badge)}\n`, 'utf8')
852
+ }
853
+ if (flags.has('--badge')) {
854
+ writeLines([formatDoctorBadgeMarkdown(report.badge)])
855
+ return 0
856
+ }
857
+ if (wantsTextOutput(flags, config)) writeLines(formatDoctorText(report))
858
+ else writeJson(report)
859
+ return report.ok ? 0 : 1
860
+ } catch (error) {
861
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
862
+ return 1
863
+ }
864
+ }
865
+
866
+ if (command === 'demo') {
867
+ const resolveDemoFixture = (): DemoFixture => {
868
+ const fixtureFlagIndex = positional.indexOf('--fixture')
869
+ if (fixtureFlagIndex >= 0) {
870
+ const value = positional[fixtureFlagIndex + 1]
871
+ if (value === 'monorepo' || value === 'example') return value
872
+ }
873
+ if (flags.has('--monorepo') || positional.includes('monorepo')) return 'monorepo'
874
+ if (positional[1] === 'monorepo') return 'monorepo'
875
+ return 'example'
876
+ }
877
+ const resolvedFixture = resolveDemoFixture()
878
+
879
+ try {
880
+ const useProject = flags.has('--in-project') || flags.has('--copy-fixture')
881
+ if (!useProject) {
882
+ const result = withDemoWorkspace(resolvedFixture, (root, config) =>
883
+ runDemo(root, config, resolvedFixture),
884
+ )
885
+ const textMode =
886
+ flags.has('--text') || (!flags.has('--json') && !flags.has('--agent'))
887
+ if (textMode) writeLines(formatDemoText(result))
888
+ else writeJson(result)
889
+ return result.ok ? 0 : 1
890
+ }
891
+
892
+ const { config, root } = loadProject(configPath)
893
+ const result = runDemo(root, config, resolvedFixture, { copyFixture: flags.has('--copy-fixture') })
894
+ if (wantsTextOutput(flags, config)) writeLines(formatDemoText(result))
895
+ else writeJson(result)
896
+ return result.ok ? 0 : 1
897
+ } catch (error) {
898
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
899
+ return 1
900
+ }
901
+ }
902
+
709
903
  if (command === 'query') {
710
904
  const kind = positional[1] as QueryKind | undefined
711
905
  const id = positional[2]
@@ -875,7 +1069,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
875
1069
  return 1
876
1070
  }
877
1071
  const index = loadDocBridgeIndex(root, config)
878
- writeAsk(question, searchIndex(index, question, 8), index)
1072
+ writeAsk(question, searchIndex(index, question, 8), index, config)
879
1073
  return 0
880
1074
  } catch (error) {
881
1075
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -0,0 +1,53 @@
1
+ import type { DoctorReport } from './run-doctor.js'
2
+
3
+ export type DoctorBadgeMetrics = {
4
+ readonly handoffPct: number
5
+ readonly bridgePct: number
6
+ readonly score: number
7
+ readonly grade: DoctorReport['grade']
8
+ readonly packages: number
9
+ }
10
+
11
+ export const doctorBadgeMetrics = (report: DoctorReport): DoctorBadgeMetrics => {
12
+ const { packages } = report.coverage
13
+ const handoffPct =
14
+ packages.total > 0 ? Math.round((packages.withAgentDoc / packages.total) * 100) : 0
15
+ const bridgePct =
16
+ packages.total > 0 ? Math.round((packages.withHumanDoc / packages.total) * 100) : 0
17
+
18
+ return {
19
+ handoffPct,
20
+ bridgePct,
21
+ score: report.score,
22
+ grade: report.grade,
23
+ packages: packages.total,
24
+ }
25
+ }
26
+
27
+ const badgeColor = (pct: number): string => {
28
+ if (pct >= 80) return '2ea44f'
29
+ if (pct >= 50) return 'dbab09'
30
+ return 'cb2431'
31
+ }
32
+
33
+ export const formatDoctorBadgeMarkdown = (metrics: DoctorBadgeMetrics): string => {
34
+ const handoff = `handoff_coverage-${metrics.handoffPct}%25-${badgeColor(metrics.handoffPct)}`
35
+ const bridge = `human_bridge-${metrics.bridgePct}%25-${badgeColor(metrics.bridgePct)}`
36
+ return [
37
+ `![handoff coverage](https://img.shields.io/badge/${handoff}?style=flat-square)`,
38
+ `![human bridge](https://img.shields.io/badge/${bridge}?style=flat-square)`,
39
+ `![doc-bridge score](https://img.shields.io/badge/doc--bridge_score-${metrics.score}%2F100-${badgeColor(metrics.score)}?style=flat-square)`,
40
+ ].join(' ')
41
+ }
42
+
43
+ export const formatDoctorBadgeJson = (metrics: DoctorBadgeMetrics): string =>
44
+ JSON.stringify(
45
+ {
46
+ schemaVersion: 1,
47
+ ...metrics,
48
+ markdown: formatDoctorBadgeMarkdown(metrics),
49
+ updatedAt: new Date().toISOString(),
50
+ },
51
+ null,
52
+ 2,
53
+ )