@agentskit/doc-bridge 0.1.0-alpha.1 → 0.1.0-alpha.2

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 (61) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/cli/program.js +283 -105
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +6 -3
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-CD_zmKXf.d.ts → index-CPUJbTbg.d.ts} +19 -10
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +278 -100
  11. package/dist/index.js.map +1 -1
  12. package/docs/DOGFOOD.md +92 -0
  13. package/package.json +27 -9
  14. package/scripts/prepare.mjs +44 -0
  15. package/src/cli/program.ts +933 -0
  16. package/src/config/defaults.ts +63 -0
  17. package/src/config/define-config.ts +6 -0
  18. package/src/config/index.ts +15 -0
  19. package/src/config/load-config.ts +138 -0
  20. package/src/config/schema.ts +294 -0
  21. package/src/federation/llms.ts +145 -0
  22. package/src/gates/run-gates.ts +274 -0
  23. package/src/index-builder/build-handoffs.ts +217 -0
  24. package/src/index-builder/build-index.ts +137 -0
  25. package/src/index-builder/capabilities.ts +37 -0
  26. package/src/index-builder/content-hash.ts +19 -0
  27. package/src/index-builder/human-adapters/core.ts +100 -0
  28. package/src/index-builder/human-adapters/docusaurus.ts +113 -0
  29. package/src/index-builder/human-adapters/fumadocs.ts +70 -0
  30. package/src/index-builder/human-adapters/index.ts +52 -0
  31. package/src/index-builder/human-adapters/plain-markdown.ts +10 -0
  32. package/src/index-builder/llms-txt.ts +19 -0
  33. package/src/index-builder/plugins/human-markdown.ts +7 -0
  34. package/src/index-builder/plugins/pnpm-monorepo.ts +72 -0
  35. package/src/index-builder/scan-corpus.ts +181 -0
  36. package/src/index.ts +120 -0
  37. package/src/intelligence/adapter.ts +90 -0
  38. package/src/intelligence/chat.ts +148 -0
  39. package/src/intelligence/peers.ts +59 -0
  40. package/src/intelligence/rag.ts +98 -0
  41. package/src/lib/glob-expand.ts +33 -0
  42. package/src/lib/markdown.ts +90 -0
  43. package/src/lib/package-manager.ts +104 -0
  44. package/src/lib/paths.ts +6 -0
  45. package/src/lib/walk.ts +45 -0
  46. package/src/mcp/server.ts +276 -0
  47. package/src/memory/ingest.ts +51 -0
  48. package/src/memory/pipeline.ts +119 -0
  49. package/src/query/load-index.ts +25 -0
  50. package/src/query/query.ts +142 -0
  51. package/src/query/search.ts +58 -0
  52. package/src/retriever/doc-bridge-retriever.ts +62 -0
  53. package/src/schemas/agent-handoff.ts +82 -0
  54. package/src/schemas/doc-bridge-index.ts +106 -0
  55. package/src/schemas/json-schemas.ts +156 -0
  56. package/src/schemas/memory-candidate.ts +37 -0
  57. package/src/shims/agentskit-peers.d.ts +80 -0
  58. package/src/validate.ts +67 -0
  59. package/src/version.ts +1 -0
  60. package/tsconfig.json +21 -0
  61. package/tsup.config.ts +15 -0
@@ -0,0 +1,933 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { createInterface } from 'node:readline/promises'
4
+
5
+ import { loadConfig, projectRootFromConfigPath } from '../config/load-config.js'
6
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
7
+ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
8
+ import { discoverPnpmPackages } from '../index-builder/plugins/pnpm-monorepo.js'
9
+ import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
10
+ import { retrieveHybridChunks } from '../federation/llms.js'
11
+ import { runGates, type GateId } from '../gates/run-gates.js'
12
+ import { runChatOnce, startInkChat } from '../intelligence/chat.js'
13
+ import { PeerMissingError, layer1InstallHint } from '../intelligence/peers.js'
14
+ import { createDocBridgeRag } from '../intelligence/rag.js'
15
+ import { firstHeading, firstParagraph } from '../lib/markdown.js'
16
+ import { ingestMemoryCandidates } from '../memory/ingest.js'
17
+ import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
18
+ import { startMcpStdioServer } from '../mcp/server.js'
19
+ import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
20
+ import { runQuery, type QueryKind } from '../query/query.js'
21
+ import { searchIndex } from '../query/search.js'
22
+ import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
23
+ import { parseAgentHandoff, parseDocBridgeConfig } from '../validate.js'
24
+ import { PACKAGE_VERSION } from '../version.js'
25
+
26
+ type Command =
27
+ | 'help'
28
+ | 'version'
29
+ | 'validate-config'
30
+ | 'validate-handoff'
31
+ | 'init'
32
+ | 'bootstrap'
33
+ | 'memory'
34
+ | 'playbook'
35
+ | 'registry'
36
+ | 'index'
37
+ | 'gate'
38
+ | 'mcp'
39
+ | 'query'
40
+ | 'search'
41
+ | 'retrieve'
42
+ | 'ask'
43
+ | 'chat'
44
+ | 'rag'
45
+ | 'list'
46
+
47
+ const usage = `ak-docs — human↔agent documentation bridge (@agentskit/doc-bridge)
48
+
49
+ Core (no API key):
50
+ ak-docs init [--demo] [--scaffold-workspaces]
51
+ ak-docs index
52
+ ak-docs query <package|ownership|intent|change> <id> [--agent] [--text]
53
+ ak-docs search <term> [--agent] [--text]
54
+ ak-docs list <packages|intents|changes|knowledge> [--text]
55
+ ak-docs ask [question] local consult (no LLM)
56
+ ak-docs gate run [gate-id]
57
+ ak-docs mcp
58
+ ak-docs memory ingest|classify|promote
59
+ ak-docs bootstrap agent-docs
60
+ ak-docs validate-config | validate-handoff <file>
61
+
62
+ Intelligence (optional AgentsKit peers):
63
+ ak-docs rag ingest|search <query>
64
+ ak-docs chat terminal chat (Ink + RAG)
65
+ ak-docs ask <question> --chat one-shot grounded answer
66
+
67
+ Advanced / ecosystem:
68
+ ak-docs retrieve <query>
69
+ ak-docs registry topology
70
+ ak-docs playbook draft
71
+
72
+ Global flags:
73
+ -h, --help --version
74
+ --config <path> (project root = config file directory)
75
+ --agent --json --text --chat --demo
76
+ `
77
+
78
+ const QUERY_KINDS = new Set<QueryKind>(['package', 'ownership', 'intent', 'change', 'search'])
79
+ const LIST_KINDS = new Set(['packages', 'intents', 'changes', 'knowledge'])
80
+ const GATE_IDS = new Set<GateId>(['index-freshness', 'human-guide-links', 'okf-type', 'docs-style'])
81
+
82
+ const parseArgs = (argv: readonly string[]) => {
83
+ const flags = new Set<string>()
84
+ let configPath: string | undefined
85
+ const positional: string[] = []
86
+
87
+ for (let i = 0; i < argv.length; i += 1) {
88
+ const arg = argv[i]
89
+ if (!arg) continue
90
+ if (arg === '--config') {
91
+ configPath = argv[i + 1]
92
+ i += 1
93
+ continue
94
+ }
95
+ if (arg.startsWith('-')) {
96
+ flags.add(arg)
97
+ continue
98
+ }
99
+ positional.push(arg)
100
+ }
101
+
102
+ let command: Command = 'help'
103
+ if (flags.has('--version') || flags.has('-V')) command = 'version'
104
+ else if (flags.has('--help') || flags.has('-h')) command = 'help'
105
+ else if (positional[0] === 'validate-config') command = 'validate-config'
106
+ else if (positional[0] === 'validate-handoff') command = 'validate-handoff'
107
+ else if (positional[0] === 'init') command = 'init'
108
+ else if (positional[0] === 'bootstrap') command = 'bootstrap'
109
+ else if (positional[0] === 'memory') command = 'memory'
110
+ else if (positional[0] === 'playbook') command = 'playbook'
111
+ else if (positional[0] === 'registry') command = 'registry'
112
+ else if (positional[0] === 'index') command = 'index'
113
+ else if (positional[0] === 'gate') command = 'gate'
114
+ else if (positional[0] === 'mcp') command = 'mcp'
115
+ else if (positional[0] === 'query') command = 'query'
116
+ else if (positional[0] === 'search') command = 'search'
117
+ else if (positional[0] === 'retrieve') command = 'retrieve'
118
+ else if (positional[0] === 'ask') command = 'ask'
119
+ else if (positional[0] === 'chat') command = 'chat'
120
+ else if (positional[0] === 'rag') command = 'rag'
121
+ else if (positional[0] === 'list') command = 'list'
122
+
123
+ return { command, flags, configPath, positional }
124
+ }
125
+
126
+ const writeJson = (payload: unknown): void => {
127
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
128
+ }
129
+
130
+ const writeLines = (lines: readonly string[]): void => {
131
+ process.stdout.write(lines.length ? `${lines.join('\n')}\n` : '')
132
+ }
133
+
134
+ const textValue = (value: unknown): string => {
135
+ if (value === null || value === undefined) return 'Not found'
136
+ if (typeof value === 'string') return value
137
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
138
+ return JSON.stringify(value, null, 2)
139
+ }
140
+
141
+ const writeTextQuery = (payload: unknown): void => {
142
+ if (!payload || typeof payload !== 'object' || !('data' in payload)) {
143
+ writeLines([textValue(payload)])
144
+ return
145
+ }
146
+
147
+ const result = payload as { readonly type?: unknown; readonly data?: unknown }
148
+ if (result.type === 'search') {
149
+ const data = result.data as {
150
+ readonly term?: unknown
151
+ readonly count?: unknown
152
+ readonly matches?: readonly {
153
+ readonly type: string
154
+ readonly id: string
155
+ readonly path: string
156
+ readonly summary?: string
157
+ }[]
158
+ }
159
+ const matches = data.matches ?? []
160
+ writeLines([
161
+ `Search: ${String(data.term ?? '')}`,
162
+ `Matches: ${String(data.count ?? matches.length)}`,
163
+ ...matches.map((match) =>
164
+ [match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
165
+ ),
166
+ ])
167
+ return
168
+ }
169
+
170
+ writeLines([textValue(result.data)])
171
+ }
172
+
173
+ const writeTextSearch = (
174
+ term: string,
175
+ matches: readonly {
176
+ readonly type: string
177
+ readonly id: string
178
+ readonly path: string
179
+ readonly summary?: string
180
+ }[],
181
+ ): void => {
182
+ writeLines([
183
+ `Search: ${term}`,
184
+ `Matches: ${matches.length}`,
185
+ ...matches.map((match) =>
186
+ [match.type, match.id, match.path, match.summary].filter(Boolean).join('\t'),
187
+ ),
188
+ ])
189
+ }
190
+
191
+ const writeAsk = (
192
+ question: string,
193
+ matches: ReturnType<typeof searchIndex>,
194
+ index: DocBridgeIndexV1,
195
+ ): 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'
202
+ writeLines([
203
+ `Question: ${question}`,
204
+ best ? `Best match: ${best.type} ${best.id} (${best.path})` : 'Best match: none',
205
+ '',
206
+ 'Matches:',
207
+ ...(
208
+ 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>']
213
+ ),
214
+ '',
215
+ 'Next commands:',
216
+ ...(best
217
+ ? [
218
+ `ak-docs search "${question}" --agent`,
219
+ bestQuery,
220
+ ]
221
+ : ['ak-docs list knowledge --text']),
222
+ ])
223
+ }
224
+
225
+ const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: string): string => {
226
+ const index = loadDocBridgeIndex(root, config)
227
+ const entry = index.knowledge.find((doc) => doc.id === idOrPath || doc.path === idOrPath)
228
+ if (!entry) throw new Error(`Unknown indexed doc "${idOrPath}". Try: search ${idOrPath}`)
229
+
230
+ const abs = resolve(root, entry.path)
231
+ const rootAbs = resolve(root)
232
+ if (abs !== rootAbs && !abs.startsWith(`${rootAbs}/`)) {
233
+ throw new Error(`Indexed doc escapes project root: ${entry.path}`)
234
+ }
235
+ return readFileSync(abs, 'utf8')
236
+ }
237
+
238
+ const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<number> => {
239
+ const index = loadDocBridgeIndex(root, config)
240
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
241
+ try {
242
+ for (;;) {
243
+ const line = (await rl.question('ak-docs> ')).trim()
244
+ if (!line) continue
245
+ if (line === 'exit' || line === 'quit') return 0
246
+
247
+ const [command, ...rest] = line.split(/\s+/)
248
+ const value = rest.join(' ').trim()
249
+ try {
250
+ if (command === 'search') {
251
+ writeTextSearch(value, searchIndex(index, value))
252
+ } else if (command === 'read' || command === 'open') {
253
+ process.stdout.write(`${readIndexedDoc(root, config, value).slice(0, 8000)}\n`)
254
+ } else if (command === 'resolve') {
255
+ writeJson(runQuery(index, config, { kind: 'ownership', id: value, agent: true }))
256
+ } else if (command === 'gate') {
257
+ const gateId = value || undefined
258
+ writeJson(runGates(root, config, gateId ? [gateId as GateId] : undefined))
259
+ } else {
260
+ writeAsk(line, searchIndex(index, line, 8), index)
261
+ }
262
+ } catch (error) {
263
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
264
+ }
265
+ }
266
+ } finally {
267
+ rl.close()
268
+ }
269
+ }
270
+
271
+ const wantsTextOutput = (flags: ReadonlySet<string>, config: DocBridgeConfigV1): boolean =>
272
+ !flags.has('--agent') &&
273
+ (flags.has('--text') || (!flags.has('--json') && config.surfaces?.cli?.defaultFormat === 'text'))
274
+
275
+ const loadProject = (configPath?: string) => {
276
+ const loadOpts = configPath ? { explicitPath: configPath } : {}
277
+ const { config, path } = loadConfig(loadOpts)
278
+ parseDocBridgeConfig(config)
279
+ const root = projectRootFromConfigPath(path, config.project?.root)
280
+ return { config, configPath: path, root }
281
+ }
282
+
283
+ const indexDiagnostics = (config: DocBridgeConfigV1, result: ReturnType<typeof buildDocBridgeIndex>): string[] => {
284
+ const diagnostics: string[] = []
285
+ const onlyDoc = result.index.knowledge.length === 1 ? result.index.knowledge[0] : undefined
286
+ if (onlyDoc?.path === config.corpus.agent.index) {
287
+ diagnostics.push(
288
+ `Only the starter ${config.corpus.agent.index} was indexed.`,
289
+ `Add agent docs under ${config.corpus.agent.root}/, then run ak-docs index again.`,
290
+ )
291
+ }
292
+ const handoffCount = Object.keys(result.index.handoffs ?? {}).length
293
+ if (handoffCount === 0) {
294
+ diagnostics.push(
295
+ 'No ownership handoffs yet. Add routing.options.ownership, package frontmatter (package + editRoot), or a monorepo plugin.',
296
+ )
297
+ }
298
+ return diagnostics
299
+ }
300
+
301
+ const diagnosticNextCommands = (
302
+ config: DocBridgeConfigV1,
303
+ result: ReturnType<typeof buildDocBridgeIndex>,
304
+ ): string[] => {
305
+ const handoffIds = Object.keys(result.index.handoffs ?? {})
306
+ if (handoffIds[0]) {
307
+ return [
308
+ `ak-docs query package ${handoffIds[0]} --agent`,
309
+ `ak-docs list packages --text`,
310
+ 'ak-docs mcp',
311
+ ]
312
+ }
313
+ return [
314
+ `mkdir -p ${config.corpus.agent.root}/packages`,
315
+ `edit ${config.corpus.agent.root}/packages/<module>.md # frontmatter: package + editRoot`,
316
+ 'ak-docs index',
317
+ ]
318
+ }
319
+
320
+ const writeIfMissing = (path: string, contents: string): boolean => {
321
+ if (existsSync(path)) return false
322
+ mkdirSync(dirname(path), { recursive: true })
323
+ writeFileSync(path, contents, 'utf8')
324
+ return true
325
+ }
326
+
327
+ const demoOwnership = {
328
+ example: {
329
+ path: 'src',
330
+ purpose: 'Starter ownership target — replace with your real modules',
331
+ checks: ['npm test'],
332
+ agentDoc: 'docs/for-agents/packages/example.md',
333
+ },
334
+ }
335
+
336
+ const initConfigObject = (withDemo: boolean) => ({
337
+ schemaVersion: 1 as const,
338
+ corpus: { agent: { root: 'docs/for-agents' } },
339
+ ...(withDemo
340
+ ? {
341
+ routing: {
342
+ options: {
343
+ ownership: demoOwnership,
344
+ },
345
+ },
346
+ }
347
+ : {}),
348
+ gates: { preset: 'minimal' as const },
349
+ })
350
+
351
+ const initConfigContents = (path: string, withDemo: boolean): string => {
352
+ if (path.endsWith('.ts') || path.endsWith('.mts')) {
353
+ return [
354
+ "import { defineConfig } from '@agentskit/doc-bridge/config'",
355
+ '',
356
+ 'export default defineConfig({',
357
+ ' schemaVersion: 1,',
358
+ " corpus: { agent: { root: 'docs/for-agents' } },",
359
+ ...(withDemo
360
+ ? [
361
+ ' routing: {',
362
+ ' options: {',
363
+ ' ownership: {',
364
+ " example: { path: 'src', purpose: 'Starter ownership target', checks: ['npm test'], agentDoc: 'docs/for-agents/packages/example.md' },",
365
+ ' },',
366
+ ' },',
367
+ ' },',
368
+ ]
369
+ : []),
370
+ " gates: { preset: 'minimal' },",
371
+ '})',
372
+ '',
373
+ ].join('\n')
374
+ }
375
+
376
+ return `${JSON.stringify(initConfigObject(withDemo), null, 2)}\n`
377
+ }
378
+
379
+ const exampleAgentDoc = `---
380
+ type: package
381
+ package: example
382
+ editRoot: src
383
+ checks: [npm test]
384
+ ---
385
+
386
+ # example
387
+
388
+ Starter agent doc generated by \`ak-docs init --demo\`.
389
+
390
+ Describe ownership, boundaries, and how agents should change this module.
391
+
392
+ ## Checks
393
+
394
+ - \`npm test\`
395
+ `
396
+
397
+ const agentsMdSnippet = `# AGENTS.md
398
+
399
+ ## Documentation routing (doc-bridge)
400
+
401
+ Before editing a package or module:
402
+
403
+ 1. Run \`ak-docs query ownership <id> --agent\` (or MCP tool \`handoff.resolve\`)
404
+ 2. Read \`startHere\` and respect \`editRoots\` + \`checks\`
405
+ 3. Prefer agent docs under \`docs/for-agents/\`; human site links appear as \`humanDoc\`
406
+
407
+ Local consult without LLM: \`ak-docs ask "<question>"\`
408
+ Optional grounded chat (AgentsKit peers): \`ak-docs chat\`
409
+ `
410
+
411
+ const workspaceDocDraft = (id: string, path: string): string => [
412
+ '---',
413
+ 'type: package',
414
+ 'draft: true',
415
+ `package: ${id}`,
416
+ `editRoot: ${path}`,
417
+ '---',
418
+ '',
419
+ `# ${id}`,
420
+ '',
421
+ 'Draft generated by `ak-docs init --scaffold-workspaces`.',
422
+ '',
423
+ '## Ownership',
424
+ '',
425
+ `- Package: \`${path}\``,
426
+ '',
427
+ '## Notes',
428
+ '',
429
+ '- TODO: describe responsibility, boundaries, and checks.',
430
+ '',
431
+ ].join('\n')
432
+
433
+ const scaffoldWorkspaceDocs = (
434
+ root: string,
435
+ config: DocBridgeConfigV1,
436
+ ): { created: string[]; skipped: string[] } => {
437
+ const created: string[] = []
438
+ const skipped: string[] = []
439
+ for (const pkg of discoverPnpmPackages(root, config)) {
440
+ const path = resolve(root, config.corpus.agent.root, 'packages', `${pkg.id}.md`)
441
+ if (writeIfMissing(path, workspaceDocDraft(pkg.id, pkg.path))) created.push(path)
442
+ else skipped.push(path)
443
+ }
444
+ return { created, skipped }
445
+ }
446
+
447
+ const bootstrapAgentDocs = (
448
+ root: string,
449
+ config: DocBridgeConfigV1,
450
+ ): { created: string[]; skipped: string[] } => {
451
+ const created: string[] = []
452
+ const skipped: string[] = []
453
+ for (const doc of scanHumanDocRecords(root, config)) {
454
+ const raw = readFileSync(doc.path, 'utf8')
455
+ const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '')
456
+ const title = firstHeading(body) ?? doc.id
457
+ const description = firstParagraph(body)
458
+ const draftPath = resolve(root, config.corpus.agent.root, 'human', `${doc.id}.md`)
459
+ const draft = [
460
+ '---',
461
+ 'type: knowledge',
462
+ 'draft: true',
463
+ `id: ${doc.id}`,
464
+ `humanDoc: ${doc.url}`,
465
+ '---',
466
+ '',
467
+ `# ${title}`,
468
+ '',
469
+ 'Draft generated by `ak-docs bootstrap agent-docs` from existing human docs.',
470
+ '',
471
+ ...(description ? ['## Source summary', '', description, ''] : []),
472
+ '## Review checklist',
473
+ '',
474
+ '- TODO: confirm ownership, edit roots, and checks.',
475
+ '- TODO: move this draft to the right agent-doc location if needed.',
476
+ '',
477
+ ].join('\n')
478
+ if (writeIfMissing(draftPath, draft)) created.push(draftPath)
479
+ else skipped.push(draftPath)
480
+ }
481
+ return { created, skipped }
482
+ }
483
+
484
+ const registryTopology = () => ({
485
+ id: 'doc-curator',
486
+ delegates: ['docs-chat', 'knowledge-promoter', 'code-review'],
487
+ tools: ['handoff.resolve', 'doc.search', 'doc.get', 'gate.status', 'retriever.query'],
488
+ steps: ['classify', 'draft', 'verify', 'review'],
489
+ mergePolicy: { autoMerge: false, requiresHuman: true },
490
+ })
491
+
492
+ export const runCli = (argv: readonly string[]): number | undefined | Promise<number> => {
493
+ const { command, flags, configPath, positional } = parseArgs(argv)
494
+
495
+ if (command === 'help') {
496
+ process.stdout.write(usage)
497
+ return 0
498
+ }
499
+
500
+ if (command === 'version') {
501
+ process.stdout.write(`ak-docs ${PACKAGE_VERSION} (@agentskit/doc-bridge)\n`)
502
+ return 0
503
+ }
504
+
505
+ if (command === 'validate-config') {
506
+ try {
507
+ const { config, path } = loadConfig(
508
+ configPath ? { explicitPath: configPath } : {},
509
+ )
510
+ parseDocBridgeConfig(config)
511
+ writeJson({ ok: true, path, schemaVersion: config.schemaVersion })
512
+ return 0
513
+ } catch (error) {
514
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
515
+ return 1
516
+ }
517
+ }
518
+
519
+ if (command === 'validate-handoff') {
520
+ const file = positional[1]
521
+ if (!file) {
522
+ process.stderr.write('Missing handoff JSON file path.\n')
523
+ return 1
524
+ }
525
+ try {
526
+ const abs = resolve(file)
527
+ const raw = readFileSync(abs, 'utf8')
528
+ const handoff = parseAgentHandoff(JSON.parse(raw) as unknown)
529
+ writeJson({ ok: true, handoff })
530
+ return 0
531
+ } catch (error) {
532
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
533
+ return 1
534
+ }
535
+ }
536
+
537
+ if (command === 'init') {
538
+ const root = process.cwd()
539
+ const withDemo = flags.has('--demo') || !flags.has('--no-demo')
540
+ const configFile = resolve(root, configPath ?? 'doc-bridge.config.json')
541
+ const docsIndex = resolve(root, 'docs/for-agents/INDEX.md')
542
+ const exampleDoc = resolve(root, 'docs/for-agents/packages/example.md')
543
+ const agentsMd = resolve(root, 'AGENTS.md')
544
+ const configWritten = writeIfMissing(configFile, initConfigContents(configFile, withDemo))
545
+ const indexWritten = writeIfMissing(
546
+ docsIndex,
547
+ withDemo
548
+ ? '# Agent docs index\n\n- [example](./packages/example.md) — starter ownership target\n'
549
+ : '# Agent docs index\n\nStart here for ownership, architecture, and task handoffs.\n',
550
+ )
551
+ const exampleWritten = withDemo ? writeIfMissing(exampleDoc, exampleAgentDoc) : false
552
+ const agentsWritten = writeIfMissing(agentsMd, agentsMdSnippet)
553
+ if (withDemo) writeIfMissing(resolve(root, 'src/.gitkeep'), '')
554
+ const scaffold = flags.has('--scaffold-workspaces')
555
+ ? scaffoldWorkspaceDocs(root, loadProject(configFile).config)
556
+ : undefined
557
+ writeJson({
558
+ ok: true,
559
+ configPath: configFile,
560
+ demo: withDemo,
561
+ created: {
562
+ config: configWritten,
563
+ index: indexWritten,
564
+ ...(withDemo ? { exampleDoc: exampleWritten, srcStub: true } : {}),
565
+ agentsMd: agentsWritten,
566
+ ...(scaffold ? { workspaceDocs: scaffold.created } : {}),
567
+ },
568
+ ...(scaffold ? { skipped: { workspaceDocs: scaffold.skipped } } : {}),
569
+ nextCommands: withDemo
570
+ ? ['ak-docs index', 'ak-docs query package example --agent', 'ak-docs list packages --text']
571
+ : ['ak-docs index', 'ak-docs list knowledge --text'],
572
+ })
573
+ return 0
574
+ }
575
+
576
+ if (command === 'bootstrap') {
577
+ if (positional[1] !== 'agent-docs') {
578
+ process.stderr.write('Usage: ak-docs bootstrap agent-docs [--config <path>]\n')
579
+ return 1
580
+ }
581
+ try {
582
+ const { config, root } = loadProject(configPath)
583
+ const result = bootstrapAgentDocs(root, config)
584
+ writeJson({ ok: true, ...result })
585
+ return 0
586
+ } catch (error) {
587
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
588
+ return 1
589
+ }
590
+ }
591
+
592
+ if (command === 'memory') {
593
+ if (!['ingest', 'classify', 'promote'].includes(positional[1] ?? '')) {
594
+ process.stderr.write('Usage: ak-docs memory <ingest|classify|promote> [--config <path>]\n')
595
+ return 1
596
+ }
597
+ try {
598
+ const { config, root } = loadProject(configPath)
599
+ const candidates = ingestMemoryCandidates(root)
600
+ if (positional[1] === 'ingest') {
601
+ writeJson({ ok: true, count: candidates.length, candidates })
602
+ return 0
603
+ }
604
+ const index = loadDocBridgeIndex(root, config)
605
+ const classifications = classifyMemoryCandidates(candidates, index)
606
+ if (positional[1] === 'classify') {
607
+ writeJson({ ok: true, count: classifications.length, classifications })
608
+ return 0
609
+ }
610
+ const draft = draftMemoryPromotion(classifications)
611
+ writeJson(draft)
612
+ return draft.ok ? 0 : 1
613
+ } catch (error) {
614
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
615
+ return 1
616
+ }
617
+ }
618
+
619
+ if (command === 'registry') {
620
+ if (positional[1] !== 'topology') {
621
+ process.stderr.write('Usage: ak-docs registry topology\n')
622
+ return 1
623
+ }
624
+ writeJson(registryTopology())
625
+ return 0
626
+ }
627
+
628
+ if (command === 'playbook') {
629
+ if (positional[1] !== 'draft') {
630
+ process.stderr.write('Usage: ak-docs playbook draft [--config <path>]\n')
631
+ return 1
632
+ }
633
+ try {
634
+ const { config, root } = loadProject(configPath)
635
+ const index = loadDocBridgeIndex(root, config)
636
+ const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index))
637
+ writeJson({
638
+ ...draft,
639
+ title: 'Draft Playbook feedback promotion',
640
+ pattern: 'Doc Bridge Pattern',
641
+ })
642
+ return 0
643
+ } catch (error) {
644
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
645
+ return 1
646
+ }
647
+ }
648
+
649
+ if (command === 'index') {
650
+ try {
651
+ const { config, root } = loadProject(configPath)
652
+ const result = buildDocBridgeIndex({ root, config })
653
+ const diagnostics = indexDiagnostics(config, result)
654
+ const handoffCount = Object.keys(result.index.handoffs ?? {}).length
655
+ writeJson({
656
+ ok: true,
657
+ indexPath: result.indexPath,
658
+ ...(result.llmsTxtPath ? { llmsTxtPath: result.llmsTxtPath } : {}),
659
+ ...(result.capabilitiesPath ? { capabilitiesPath: result.capabilitiesPath } : {}),
660
+ contentHash: result.index.contentHash,
661
+ knowledgeCount: result.index.knowledge.length,
662
+ packageCount: result.index.lookup?.packages.length ?? 0,
663
+ handoffCount,
664
+ ...(diagnostics.length ? { diagnostics } : {}),
665
+ nextCommands: diagnosticNextCommands(config, result),
666
+ })
667
+ return 0
668
+ } catch (error) {
669
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
670
+ return 1
671
+ }
672
+ }
673
+
674
+ if (command === 'gate') {
675
+ const action = positional[1]
676
+ const gateId = positional[2] as GateId | undefined
677
+ if (action !== 'run') {
678
+ process.stderr.write('Usage: ak-docs gate run [index-freshness]\n')
679
+ return 1
680
+ }
681
+ if (gateId && !GATE_IDS.has(gateId)) {
682
+ process.stderr.write(
683
+ `Unsupported gate "${gateId}". Supported gates: index-freshness, human-guide-links, okf-type, docs-style\n`,
684
+ )
685
+ return 1
686
+ }
687
+ try {
688
+ const { config, root } = loadProject(configPath)
689
+ const result = runGates(root, config, gateId ? [gateId] : undefined)
690
+ writeJson(result)
691
+ return result.ok ? 0 : 1
692
+ } catch (error) {
693
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
694
+ return 1
695
+ }
696
+ }
697
+
698
+ if (command === 'mcp') {
699
+ try {
700
+ const { config, root } = loadProject(configPath)
701
+ startMcpStdioServer({ root, config })
702
+ return undefined
703
+ } catch (error) {
704
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
705
+ return 1
706
+ }
707
+ }
708
+
709
+ if (command === 'query') {
710
+ const kind = positional[1] as QueryKind | undefined
711
+ const id = positional[2]
712
+ if (!kind || !QUERY_KINDS.has(kind) || kind === 'search') {
713
+ process.stderr.write('Usage: ak-docs query <package|ownership|intent|change> <id> [--agent]\n')
714
+ return 1
715
+ }
716
+ if (!id) {
717
+ process.stderr.write(`Missing id for query kind "${kind}".\n`)
718
+ return 1
719
+ }
720
+ try {
721
+ const { config, root } = loadProject(configPath)
722
+ const index = loadDocBridgeIndex(root, config)
723
+ const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') })
724
+ if (wantsTextOutput(flags, config)) writeTextQuery(result)
725
+ else writeJson(result)
726
+ return 0
727
+ } catch (error) {
728
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
729
+ return 1
730
+ }
731
+ }
732
+
733
+ if (command === 'search') {
734
+ const term = positional.slice(1).join(' ').trim()
735
+ if (!term) {
736
+ process.stderr.write('Usage: ak-docs search <term> [--agent]\n')
737
+ return 1
738
+ }
739
+ try {
740
+ const { config, root } = loadProject(configPath)
741
+ const index = loadDocBridgeIndex(root, config)
742
+ if (flags.has('--agent')) {
743
+ const result = runQuery(index, config, { kind: 'search', term, agent: true })
744
+ writeJson(result)
745
+ } else {
746
+ const matches = searchIndex(index, term)
747
+ if (wantsTextOutput(flags, config)) writeTextSearch(term, matches)
748
+ else writeJson({ term, count: matches.length, matches })
749
+ }
750
+ return 0
751
+ } catch (error) {
752
+ if (error instanceof IndexNotFoundError) {
753
+ process.stderr.write(`${error.message}\n`)
754
+ return 1
755
+ }
756
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
757
+ return 1
758
+ }
759
+ }
760
+
761
+ if (command === 'retrieve') {
762
+ const query = positional.slice(1).join(' ').trim()
763
+ if (!query) {
764
+ process.stderr.write('Usage: ak-docs retrieve <query> [--config <path>]\n')
765
+ return 1
766
+ }
767
+ return (async () => {
768
+ try {
769
+ const { config, root } = loadProject(configPath)
770
+ const index = loadDocBridgeIndex(root, config)
771
+ writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) })
772
+ return 0
773
+ } catch (error) {
774
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
775
+ return 1
776
+ }
777
+ })()
778
+ }
779
+
780
+ if (command === 'rag') {
781
+ const action = positional[1]
782
+ if (action !== 'ingest' && action !== 'search') {
783
+ process.stderr.write('Usage: ak-docs rag ingest | ak-docs rag search <query>\n')
784
+ return 1
785
+ }
786
+ return (async () => {
787
+ try {
788
+ const { config, root } = loadProject(configPath)
789
+ const index = loadDocBridgeIndex(root, config)
790
+ const rag = await createDocBridgeRag(root, config, index)
791
+ if (action === 'ingest') {
792
+ const result = await rag.ingest()
793
+ writeJson({ ok: true, ...result })
794
+ return 0
795
+ }
796
+ const query = positional.slice(2).join(' ').trim()
797
+ if (!query) {
798
+ process.stderr.write('Usage: ak-docs rag search <query>\n')
799
+ return 1
800
+ }
801
+ const hits = await rag.search(query)
802
+ writeJson({ query, count: hits.length, hits })
803
+ return 0
804
+ } catch (error) {
805
+ if (error instanceof PeerMissingError) {
806
+ process.stderr.write(`${error.message}\n`)
807
+ return 1
808
+ }
809
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
810
+ return 1
811
+ }
812
+ })()
813
+ }
814
+
815
+ if (command === 'chat') {
816
+ return (async () => {
817
+ try {
818
+ const { config, root } = loadProject(configPath)
819
+ if (!config.intelligence?.enabled || !config.intelligence.adapter) {
820
+ process.stderr.write(
821
+ 'Chat requires intelligence.enabled and intelligence.adapter in doc-bridge config.\n' +
822
+ `Layer 1 peers: ${layer1InstallHint()}\n`,
823
+ )
824
+ return 1
825
+ }
826
+ const index = loadDocBridgeIndex(root, config)
827
+ await startInkChat(root, config, index)
828
+ return 0
829
+ } catch (error) {
830
+ if (error instanceof PeerMissingError) {
831
+ process.stderr.write(`${error.message}\n`)
832
+ return 1
833
+ }
834
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
835
+ return 1
836
+ }
837
+ })()
838
+ }
839
+
840
+ if (command === 'ask') {
841
+ const question = positional.slice(1).join(' ').trim()
842
+ try {
843
+ const { config, root } = loadProject(configPath)
844
+ if (flags.has('--chat')) {
845
+ if (!config.intelligence?.enabled || !config.intelligence.adapter) {
846
+ process.stderr.write(
847
+ 'Chat mode requires intelligence.enabled and intelligence.adapter in doc-bridge config.\n' +
848
+ `Install peers: ${layer1InstallHint()}\n`,
849
+ )
850
+ return 1
851
+ }
852
+ if (!question) {
853
+ process.stderr.write('Usage: ak-docs ask <question> --chat\n')
854
+ return 1
855
+ }
856
+ return (async () => {
857
+ try {
858
+ const index = loadDocBridgeIndex(root, config)
859
+ const result = await runChatOnce(root, config, index, question)
860
+ writeLines([result.content])
861
+ return 0
862
+ } catch (error) {
863
+ if (error instanceof PeerMissingError) {
864
+ process.stderr.write(`${error.message}\n`)
865
+ return 1
866
+ }
867
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
868
+ return 1
869
+ }
870
+ })()
871
+ }
872
+ if (!question) {
873
+ if (process.stdin.isTTY) return runAskRepl(root, config)
874
+ process.stderr.write('Usage: ak-docs ask <question>, or run ak-docs ask in an interactive terminal.\n')
875
+ return 1
876
+ }
877
+ const index = loadDocBridgeIndex(root, config)
878
+ writeAsk(question, searchIndex(index, question, 8), index)
879
+ return 0
880
+ } catch (error) {
881
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
882
+ return 1
883
+ }
884
+ }
885
+
886
+ if (command === 'list') {
887
+ const kind = positional[1]
888
+ if (!kind || !LIST_KINDS.has(kind)) {
889
+ process.stderr.write('Usage: ak-docs list <packages|intents|changes|knowledge>\n')
890
+ return 1
891
+ }
892
+ try {
893
+ const { config, root } = loadProject(configPath)
894
+ const index = loadDocBridgeIndex(root, config)
895
+
896
+ if (kind === 'packages') {
897
+ const items = index.lookup?.packages ?? []
898
+ if (wantsTextOutput(flags, config)) writeLines(items)
899
+ else writeJson({ kind, items })
900
+ return 0
901
+ }
902
+ if (kind === 'intents') {
903
+ const items = Object.keys(index.lookup?.intents ?? {})
904
+ if (wantsTextOutput(flags, config)) writeLines(items)
905
+ else writeJson({ kind, items })
906
+ return 0
907
+ }
908
+ if (kind === 'changes') {
909
+ const items = Object.keys(index.lookup?.changes ?? {})
910
+ if (wantsTextOutput(flags, config)) writeLines(items)
911
+ else writeJson({ kind, items })
912
+ return 0
913
+ }
914
+ const items = index.knowledge.map((entry) => ({
915
+ id: entry.id,
916
+ title: entry.title,
917
+ path: entry.path,
918
+ }))
919
+ if (wantsTextOutput(flags, config)) {
920
+ writeLines(items.map((item) => [item.id, item.path, item.title].join('\t')))
921
+ } else {
922
+ writeJson({ kind, items })
923
+ }
924
+ return 0
925
+ } catch (error) {
926
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
927
+ return 1
928
+ }
929
+ }
930
+
931
+ process.stdout.write(usage)
932
+ return 1
933
+ }