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