@agentskit/doc-bridge 0.1.0-alpha.3 → 1.0.1
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/CHANGELOG.md +46 -0
- package/README.md +139 -57
- package/action.yml +78 -0
- package/dist/cli/program.js +1000 -70
- package/dist/cli/program.js.map +1 -1
- package/dist/index.d.ts +238 -26
- package/dist/index.js +717 -23
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD-ROUND3.md +74 -0
- package/docs/DOGFOOD-V1.md +84 -0
- package/docs/POSITIONING.md +2 -0
- package/docs/RELEASE.md +5 -3
- package/docs/chat-and-rag.md +2 -0
- package/docs/getting-started.md +14 -1
- package/docs/landing/index.html +299 -0
- package/docs/mcp.md +10 -1
- package/docs/ollama-demo.md +64 -0
- package/docs/playbook/doc-bridge-pattern.md +114 -0
- package/docs/recipes/index-pipeline.md +89 -0
- package/docs/skills/doc-bridge.md +64 -0
- package/docs/spec/cli.md +6 -0
- package/docs/spec/playbook-feedback.md +12 -4
- package/examples/demo-example/doc-bridge.config.json +23 -0
- package/examples/demo-example/docs/for-agents/INDEX.md +3 -0
- package/examples/demo-example/docs/for-agents/packages/example.md +14 -0
- package/examples/demo-example/package.json +7 -0
- package/examples/demo-example/src/.gitkeep +0 -0
- package/examples/demo-monorepo/doc-bridge.config.json +37 -0
- package/examples/demo-monorepo/docs/for-agents/INDEX.md +4 -0
- package/examples/demo-monorepo/docs/for-agents/packages/auth.md +22 -0
- package/examples/demo-monorepo/docs/for-agents/packages/billing.md +19 -0
- package/examples/demo-monorepo/docs/human/guides/auth.md +7 -0
- package/examples/demo-monorepo/package.json +7 -0
- package/examples/demo-monorepo/packages/auth/package.json +8 -0
- package/examples/demo-monorepo/packages/billing/package.json +7 -0
- package/examples/demo-monorepo/pnpm-workspace.yaml +2 -0
- package/examples/ollama-chat.config.ts +42 -0
- package/package.json +7 -4
- package/src/cli/demo.ts +143 -0
- package/src/cli/program.ts +194 -14
- package/src/doctor/badge.ts +53 -0
- package/src/doctor/run-doctor.ts +286 -0
- package/src/index-builder/build-handoffs.ts +19 -1
- package/src/index-builder/watch-index.ts +94 -0
- package/src/index.ts +24 -0
- package/src/intelligence/adapter.ts +14 -1
- package/src/mcp/install.ts +84 -0
- package/src/memory/github-pr.ts +190 -0
- package/src/playbook/doc-bridge-pattern.ts +121 -0
- package/src/query/query.ts +19 -1
- package/src/schemas/agent-handoff.ts +11 -0
- package/src/version.ts +1 -1
package/src/cli/demo.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/program.ts
CHANGED
|
@@ -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
|
|
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
|
|
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'
|
|
@@ -201,23 +218,60 @@ const writeTextSearch = (
|
|
|
201
218
|
])
|
|
202
219
|
}
|
|
203
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
|
+
|
|
204
255
|
const writeAsk = (
|
|
205
256
|
question: string,
|
|
206
257
|
matches: ReturnType<typeof searchIndex>,
|
|
207
258
|
index: DocBridgeIndexV1,
|
|
259
|
+
config: DocBridgeConfigV1,
|
|
208
260
|
): void => {
|
|
209
261
|
// Prefer ownership match for routing questions
|
|
210
262
|
const owner =
|
|
211
263
|
matches.find((match) => match.type === 'ownership') ??
|
|
212
264
|
matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]))
|
|
213
265
|
const best = owner ?? matches[0]
|
|
214
|
-
const
|
|
215
|
-
best && (best.type === 'ownership' || index.lookup?.ownership?.[best.id])
|
|
216
|
-
|
|
217
|
-
|
|
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'
|
|
218
271
|
writeLines([
|
|
219
272
|
`Question: ${question}`,
|
|
220
273
|
best ? `Best match: ${best.type} ${best.id} (${best.path})` : 'Best match: none',
|
|
274
|
+
...(ownerId ? handoffSummaryLines(index, config, ownerId) : []),
|
|
221
275
|
'',
|
|
222
276
|
'Matches:',
|
|
223
277
|
...(
|
|
@@ -231,8 +285,9 @@ const writeAsk = (
|
|
|
231
285
|
? [
|
|
232
286
|
`ak-docs search "${question}" --agent`,
|
|
233
287
|
bestQuery,
|
|
288
|
+
...(ownerId ? [`ak-docs doctor --text`] : []),
|
|
234
289
|
]
|
|
235
|
-
: ['ak-docs list knowledge --text']),
|
|
290
|
+
: ['ak-docs list knowledge --text', 'ak-docs doctor --text']),
|
|
236
291
|
])
|
|
237
292
|
}
|
|
238
293
|
|
|
@@ -271,7 +326,7 @@ const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<numb
|
|
|
271
326
|
const gateId = value || undefined
|
|
272
327
|
writeJson(runGates(root, config, gateId ? [gateId as GateId] : undefined))
|
|
273
328
|
} else {
|
|
274
|
-
writeAsk(line, searchIndex(index, line, 8), index)
|
|
329
|
+
writeAsk(line, searchIndex(index, line, 8), index, config)
|
|
275
330
|
}
|
|
276
331
|
} catch (error) {
|
|
277
332
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
@@ -605,7 +660,9 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
605
660
|
|
|
606
661
|
if (command === 'memory') {
|
|
607
662
|
if (!['ingest', 'classify', 'promote'].includes(positional[1] ?? '')) {
|
|
608
|
-
process.stderr.write(
|
|
663
|
+
process.stderr.write(
|
|
664
|
+
'Usage: ak-docs memory <ingest|classify|promote> [--pr] [--dry-run] [--force] [--config <path>]\n',
|
|
665
|
+
)
|
|
609
666
|
return 1
|
|
610
667
|
}
|
|
611
668
|
try {
|
|
@@ -622,6 +679,23 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
622
679
|
return 0
|
|
623
680
|
}
|
|
624
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
|
+
}
|
|
625
699
|
writeJson(draft)
|
|
626
700
|
return draft.ok ? 0 : 1
|
|
627
701
|
} catch (error) {
|
|
@@ -640,8 +714,18 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
640
714
|
}
|
|
641
715
|
|
|
642
716
|
if (command === 'playbook') {
|
|
643
|
-
|
|
644
|
-
|
|
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')
|
|
645
729
|
return 1
|
|
646
730
|
}
|
|
647
731
|
try {
|
|
@@ -652,6 +736,8 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
652
736
|
...draft,
|
|
653
737
|
title: 'Draft Playbook feedback promotion',
|
|
654
738
|
pattern: 'Doc Bridge Pattern',
|
|
739
|
+
patternDoc: 'docs/playbook/doc-bridge-pattern.md',
|
|
740
|
+
exportCommand: 'ak-docs playbook pattern --text',
|
|
655
741
|
})
|
|
656
742
|
return 0
|
|
657
743
|
} catch (error) {
|
|
@@ -662,7 +748,14 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
662
748
|
|
|
663
749
|
if (command === 'index') {
|
|
664
750
|
try {
|
|
665
|
-
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
|
+
}
|
|
666
759
|
const result = buildDocBridgeIndex({ root, config })
|
|
667
760
|
const diagnostics = indexDiagnostics(config, result)
|
|
668
761
|
const handoffCount = Object.keys(result.index.handoffs ?? {}).length
|
|
@@ -710,6 +803,34 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
710
803
|
}
|
|
711
804
|
|
|
712
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
|
+
|
|
713
834
|
try {
|
|
714
835
|
const { config, root } = loadProject(configPath)
|
|
715
836
|
startMcpStdioServer({ root, config })
|
|
@@ -720,6 +841,65 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
720
841
|
}
|
|
721
842
|
}
|
|
722
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
|
+
|
|
723
903
|
if (command === 'query') {
|
|
724
904
|
const kind = positional[1] as QueryKind | undefined
|
|
725
905
|
const id = positional[2]
|
|
@@ -889,7 +1069,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
889
1069
|
return 1
|
|
890
1070
|
}
|
|
891
1071
|
const index = loadDocBridgeIndex(root, config)
|
|
892
|
-
writeAsk(question, searchIndex(index, question, 8), index)
|
|
1072
|
+
writeAsk(question, searchIndex(index, question, 8), index, config)
|
|
893
1073
|
return 0
|
|
894
1074
|
} catch (error) {
|
|
895
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
|
+
``,
|
|
38
|
+
``,
|
|
39
|
+
`}?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
|
+
)
|