@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.
- package/CHANGELOG.md +59 -0
- package/README.md +139 -57
- package/action.yml +78 -0
- package/dist/cli/program.js +1122 -114
- package/dist/cli/program.js.map +1 -1
- package/dist/index.d.ts +258 -28
- package/dist/index.js +824 -54
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD-ROUND2.md +142 -0
- package/docs/DOGFOOD-ROUND3.md +74 -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 +12 -9
- package/src/cli/demo.ts +143 -0
- package/src/cli/program.ts +220 -26
- package/src/doctor/badge.ts +53 -0
- package/src/doctor/run-doctor.ts +286 -0
- package/src/federation/llms.ts +20 -11
- package/src/index-builder/build-handoffs.ts +35 -2
- package/src/index-builder/scan-corpus.ts +5 -1
- package/src/index-builder/watch-index.ts +94 -0
- package/src/index.ts +24 -0
- package/src/lib/markdown.ts +31 -4
- 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/query/search.ts +85 -17
- package/src/schemas/agent-handoff.ts +11 -0
- package/src/schemas/doc-bridge-index.ts +3 -1
- package/src/schemas/json-schemas.ts +2 -1
- 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'
|
|
@@ -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.
|
|
164
|
-
|
|
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((
|
|
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
|
-
|
|
197
|
-
const owner =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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(
|
|
210
|
-
|
|
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(
|
|
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
|
-
|
|
630
|
-
|
|
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
|
+
``,
|
|
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
|
+
)
|