@agentskit/doc-bridge 0.1.0-alpha.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +139 -57
- package/action.yml +78 -0
- package/dist/cli/program.js +1000 -70
- package/dist/cli/program.js.map +1 -1
- package/dist/index.d.ts +238 -26
- package/dist/index.js +717 -23
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD-ROUND3.md +74 -0
- package/docs/DOGFOOD-V1.md +84 -0
- package/docs/POSITIONING.md +2 -0
- package/docs/RELEASE.md +5 -3
- package/docs/chat-and-rag.md +2 -0
- package/docs/getting-started.md +14 -1
- package/docs/landing/index.html +299 -0
- package/docs/mcp.md +10 -1
- package/docs/ollama-demo.md +64 -0
- package/docs/playbook/doc-bridge-pattern.md +114 -0
- package/docs/recipes/index-pipeline.md +89 -0
- package/docs/skills/doc-bridge.md +64 -0
- package/docs/spec/cli.md +6 -0
- package/docs/spec/playbook-feedback.md +12 -4
- package/examples/demo-example/doc-bridge.config.json +23 -0
- package/examples/demo-example/docs/for-agents/INDEX.md +3 -0
- package/examples/demo-example/docs/for-agents/packages/example.md +14 -0
- package/examples/demo-example/package.json +7 -0
- package/examples/demo-example/src/.gitkeep +0 -0
- package/examples/demo-monorepo/doc-bridge.config.json +37 -0
- package/examples/demo-monorepo/docs/for-agents/INDEX.md +4 -0
- package/examples/demo-monorepo/docs/for-agents/packages/auth.md +22 -0
- package/examples/demo-monorepo/docs/for-agents/packages/billing.md +19 -0
- package/examples/demo-monorepo/docs/human/guides/auth.md +7 -0
- package/examples/demo-monorepo/package.json +7 -0
- package/examples/demo-monorepo/packages/auth/package.json +8 -0
- package/examples/demo-monorepo/packages/billing/package.json +7 -0
- package/examples/demo-monorepo/pnpm-workspace.yaml +2 -0
- package/examples/ollama-chat.config.ts +42 -0
- package/package.json +7 -4
- package/src/cli/demo.ts +143 -0
- package/src/cli/program.ts +194 -14
- package/src/doctor/badge.ts +53 -0
- package/src/doctor/run-doctor.ts +286 -0
- package/src/index-builder/build-handoffs.ts +19 -1
- package/src/index-builder/watch-index.ts +94 -0
- package/src/index.ts +24 -0
- package/src/intelligence/adapter.ts +14 -1
- package/src/mcp/install.ts +84 -0
- package/src/memory/github-pr.ts +190 -0
- package/src/playbook/doc-bridge-pattern.ts +121 -0
- package/src/query/query.ts +19 -1
- package/src/schemas/agent-handoff.ts +11 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import { buildDocBridgeIndex } from '../index-builder/build-index.js'
|
|
3
|
+
import { scanAgentCorpus } from '../index-builder/scan-corpus.js'
|
|
4
|
+
import { runGates, type GateRunResult } from '../gates/run-gates.js'
|
|
5
|
+
import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
|
|
6
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
7
|
+
import { doctorBadgeMetrics, type DoctorBadgeMetrics } from './badge.js'
|
|
8
|
+
|
|
9
|
+
export type DoctorIssue = {
|
|
10
|
+
readonly severity: 'error' | 'warn' | 'info'
|
|
11
|
+
readonly code: string
|
|
12
|
+
readonly message: string
|
|
13
|
+
readonly action?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type DoctorCoverage = {
|
|
17
|
+
readonly packages: {
|
|
18
|
+
readonly total: number
|
|
19
|
+
readonly withAgentDoc: number
|
|
20
|
+
readonly withHumanDoc: number
|
|
21
|
+
readonly missingAgentDoc: readonly string[]
|
|
22
|
+
readonly missingHumanDoc: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
readonly agentDocs: {
|
|
25
|
+
readonly total: number
|
|
26
|
+
readonly indexed: number
|
|
27
|
+
readonly unindexed: readonly string[]
|
|
28
|
+
}
|
|
29
|
+
readonly freshness: {
|
|
30
|
+
readonly ok: boolean
|
|
31
|
+
readonly message: string
|
|
32
|
+
readonly hasIndex: boolean
|
|
33
|
+
}
|
|
34
|
+
readonly gates: GateRunResult
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DoctorReport = {
|
|
38
|
+
readonly ok: boolean
|
|
39
|
+
readonly score: number
|
|
40
|
+
readonly grade: 'A' | 'B' | 'C' | 'D' | 'F'
|
|
41
|
+
readonly coverage: DoctorCoverage
|
|
42
|
+
readonly badge: DoctorBadgeMetrics
|
|
43
|
+
readonly issues: readonly DoctorIssue[]
|
|
44
|
+
readonly nextActions: readonly string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const gradeForScore = (score: number): DoctorReport['grade'] => {
|
|
48
|
+
if (score >= 90) return 'A'
|
|
49
|
+
if (score >= 75) return 'B'
|
|
50
|
+
if (score >= 60) return 'C'
|
|
51
|
+
if (score >= 40) return 'D'
|
|
52
|
+
return 'F'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const agentDocPaths = (index: DocBridgeIndexV1): Set<string> => {
|
|
56
|
+
const paths = new Set<string>()
|
|
57
|
+
for (const owner of Object.values(index.lookup?.ownership ?? {})) {
|
|
58
|
+
if (owner.agentDoc) paths.add(owner.agentDoc)
|
|
59
|
+
}
|
|
60
|
+
for (const handoff of Object.values(index.handoffs ?? {})) {
|
|
61
|
+
if (handoff.startHere) paths.add(handoff.startHere)
|
|
62
|
+
}
|
|
63
|
+
return paths
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const computeScore = (coverage: DoctorCoverage): number => {
|
|
67
|
+
let score = 0
|
|
68
|
+
|
|
69
|
+
if (coverage.freshness.hasIndex) score += 15
|
|
70
|
+
if (coverage.freshness.ok) score += 15
|
|
71
|
+
|
|
72
|
+
const { total, withAgentDoc, withHumanDoc } = coverage.packages
|
|
73
|
+
if (total > 0) {
|
|
74
|
+
score += Math.round((withAgentDoc / total) * 35)
|
|
75
|
+
score += Math.round((withHumanDoc / total) * 20)
|
|
76
|
+
} else if (coverage.agentDocs.indexed > 0) {
|
|
77
|
+
score += 35
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (coverage.gates.ok) score += 15
|
|
81
|
+
else {
|
|
82
|
+
const passed = coverage.gates.results.filter((gate) => gate.ok).length
|
|
83
|
+
const totalGates = coverage.gates.results.length || 1
|
|
84
|
+
score += Math.round((passed / totalGates) * 10)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Math.min(100, Math.max(0, score))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const buildIssues = (coverage: DoctorCoverage): DoctorIssue[] => {
|
|
91
|
+
const issues: DoctorIssue[] = []
|
|
92
|
+
|
|
93
|
+
if (!coverage.freshness.hasIndex) {
|
|
94
|
+
issues.push({
|
|
95
|
+
severity: 'error',
|
|
96
|
+
code: 'index-missing',
|
|
97
|
+
message: 'No doc-bridge index found.',
|
|
98
|
+
action: 'ak-docs index',
|
|
99
|
+
})
|
|
100
|
+
} else if (!coverage.freshness.ok) {
|
|
101
|
+
issues.push({
|
|
102
|
+
severity: 'error',
|
|
103
|
+
code: 'index-stale',
|
|
104
|
+
message: coverage.freshness.message,
|
|
105
|
+
action: 'ak-docs index',
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const id of coverage.packages.missingAgentDoc) {
|
|
110
|
+
issues.push({
|
|
111
|
+
severity: 'warn',
|
|
112
|
+
code: 'missing-agent-doc',
|
|
113
|
+
message: `Package "${id}" has no dedicated agent doc.`,
|
|
114
|
+
action: `ak-docs init --scaffold-workspaces # or edit docs/for-agents/packages/${id}.md`,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const id of coverage.packages.missingHumanDoc) {
|
|
119
|
+
issues.push({
|
|
120
|
+
severity: 'info',
|
|
121
|
+
code: 'missing-human-doc',
|
|
122
|
+
message: `Package "${id}" has no linked human guide.`,
|
|
123
|
+
action: 'ak-docs bootstrap agent-docs',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const gate of coverage.gates.results.filter((result) => !result.ok)) {
|
|
128
|
+
issues.push({
|
|
129
|
+
severity: gate.id === 'index-freshness' ? 'error' : 'warn',
|
|
130
|
+
code: `gate-${gate.id}`,
|
|
131
|
+
message: gate.message,
|
|
132
|
+
action: gate.id === 'index-freshness' ? 'ak-docs index' : 'ak-docs gate run',
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return issues
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const buildNextActions = (issues: readonly DoctorIssue[], coverage: DoctorCoverage): string[] => {
|
|
140
|
+
const actions = new Set<string>()
|
|
141
|
+
for (const issue of issues) {
|
|
142
|
+
if (issue.action) actions.add(issue.action)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!coverage.freshness.hasIndex || !coverage.freshness.ok) {
|
|
146
|
+
actions.add('ak-docs index')
|
|
147
|
+
}
|
|
148
|
+
if (coverage.packages.missingHumanDoc.length) {
|
|
149
|
+
actions.add('ak-docs bootstrap agent-docs')
|
|
150
|
+
}
|
|
151
|
+
if (coverage.packages.total > 0) {
|
|
152
|
+
const sample =
|
|
153
|
+
coverage.packages.missingAgentDoc[0] ??
|
|
154
|
+
coverage.packages.missingHumanDoc[0]
|
|
155
|
+
if (sample) actions.add(`ak-docs query package ${sample} --agent`)
|
|
156
|
+
}
|
|
157
|
+
if (!actions.size) {
|
|
158
|
+
actions.add('ak-docs mcp install --cursor')
|
|
159
|
+
actions.add('ak-docs gate run')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [...actions].slice(0, 6)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const runDoctor = (root: string, config: DocBridgeConfigV1): DoctorReport => {
|
|
166
|
+
let index: DocBridgeIndexV1 | undefined
|
|
167
|
+
let hasIndex = true
|
|
168
|
+
let freshnessOk = false
|
|
169
|
+
let freshnessMessage = 'Index is fresh'
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
index = loadDocBridgeIndex(root, config)
|
|
173
|
+
const next = buildDocBridgeIndex({ root, config, write: false }).index.contentHash
|
|
174
|
+
freshnessOk = index.contentHash === next
|
|
175
|
+
freshnessMessage = freshnessOk ? 'Index is fresh' : 'Index is stale. Run: ak-docs index'
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof IndexNotFoundError) {
|
|
178
|
+
hasIndex = false
|
|
179
|
+
freshnessOk = false
|
|
180
|
+
freshnessMessage = error.message
|
|
181
|
+
index = buildDocBridgeIndex({ root, config, write: false }).index
|
|
182
|
+
} else {
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const ownership = Object.entries(index.lookup?.ownership ?? {})
|
|
188
|
+
const missingAgentDoc = ownership
|
|
189
|
+
.filter(([, owner]) => !owner.agentDoc || owner.agentDoc === config.corpus.agent.index)
|
|
190
|
+
.map(([id]) => id)
|
|
191
|
+
const missingHumanDoc = ownership.filter(([, owner]) => !owner.humanDoc).map(([id]) => id)
|
|
192
|
+
|
|
193
|
+
const indexedPaths = new Set(index.knowledge.map((entry) => entry.path))
|
|
194
|
+
const expectedAgentDocs = agentDocPaths(index)
|
|
195
|
+
const unindexed = [...expectedAgentDocs].filter((path) => !indexedPaths.has(path))
|
|
196
|
+
|
|
197
|
+
const corpusDocs = scanAgentCorpus(root, config).filter(
|
|
198
|
+
(doc) => doc.path !== config.corpus.agent.index,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
const gates = runGates(root, config)
|
|
202
|
+
|
|
203
|
+
const coverage: DoctorCoverage = {
|
|
204
|
+
packages: {
|
|
205
|
+
total: ownership.length,
|
|
206
|
+
withAgentDoc: ownership.length - missingAgentDoc.length,
|
|
207
|
+
withHumanDoc: ownership.length - missingHumanDoc.length,
|
|
208
|
+
missingAgentDoc,
|
|
209
|
+
missingHumanDoc,
|
|
210
|
+
},
|
|
211
|
+
agentDocs: {
|
|
212
|
+
total: corpusDocs.length,
|
|
213
|
+
indexed: corpusDocs.filter((doc) => indexedPaths.has(doc.path)).length,
|
|
214
|
+
unindexed: corpusDocs.filter((doc) => !indexedPaths.has(doc.path)).map((doc) => doc.path),
|
|
215
|
+
},
|
|
216
|
+
freshness: {
|
|
217
|
+
ok: freshnessOk,
|
|
218
|
+
message: freshnessMessage,
|
|
219
|
+
hasIndex,
|
|
220
|
+
},
|
|
221
|
+
gates,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const issues = buildIssues(coverage)
|
|
225
|
+
const score = computeScore(coverage)
|
|
226
|
+
const nextActions = buildNextActions(issues, coverage)
|
|
227
|
+
|
|
228
|
+
const report: DoctorReport = {
|
|
229
|
+
ok: issues.every((issue) => issue.severity !== 'error') && gates.ok,
|
|
230
|
+
score,
|
|
231
|
+
grade: gradeForScore(score),
|
|
232
|
+
coverage,
|
|
233
|
+
badge: { handoffPct: 0, bridgePct: 0, score, grade: gradeForScore(score), packages: 0 },
|
|
234
|
+
issues,
|
|
235
|
+
nextActions,
|
|
236
|
+
}
|
|
237
|
+
return { ...report, badge: doctorBadgeMetrics(report) }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const formatDoctorText = (report: DoctorReport): string[] => {
|
|
241
|
+
const { coverage } = report
|
|
242
|
+
const handoffPct =
|
|
243
|
+
coverage.packages.total > 0
|
|
244
|
+
? Math.round((coverage.packages.withAgentDoc / coverage.packages.total) * 100)
|
|
245
|
+
: 0
|
|
246
|
+
const humanPct =
|
|
247
|
+
coverage.packages.total > 0
|
|
248
|
+
? Math.round((coverage.packages.withHumanDoc / coverage.packages.total) * 100)
|
|
249
|
+
: 0
|
|
250
|
+
|
|
251
|
+
const lines = [
|
|
252
|
+
'doc-bridge doctor',
|
|
253
|
+
'─'.repeat(40),
|
|
254
|
+
`Score: ${report.score}/100 (${report.grade})`,
|
|
255
|
+
'',
|
|
256
|
+
'Coverage',
|
|
257
|
+
` Packages: ${coverage.packages.total}`,
|
|
258
|
+
` Agent docs: ${coverage.packages.withAgentDoc}/${coverage.packages.total} (${handoffPct}% handoff-ready)`,
|
|
259
|
+
` Human guides: ${coverage.packages.withHumanDoc}/${coverage.packages.total} (${humanPct}% bridged)`,
|
|
260
|
+
` Corpus indexed: ${coverage.agentDocs.indexed}/${coverage.agentDocs.total} agent docs`,
|
|
261
|
+
` Index freshness: ${coverage.freshness.ok ? 'fresh' : 'stale or missing'}`,
|
|
262
|
+
` Gates: ${coverage.gates.results.filter((g) => g.ok).length}/${coverage.gates.results.length} passing`,
|
|
263
|
+
` Badge: handoff ${report.badge.handoffPct}% · bridge ${report.badge.bridgePct}%`,
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
if (coverage.packages.missingHumanDoc.length) {
|
|
267
|
+
lines.push('', 'Missing humanDoc (bridge gap)', ...coverage.packages.missingHumanDoc.map((id) => ` • ${id}`))
|
|
268
|
+
}
|
|
269
|
+
if (coverage.packages.missingAgentDoc.length) {
|
|
270
|
+
lines.push('', 'Missing agent doc', ...coverage.packages.missingAgentDoc.map((id) => ` • ${id}`))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (report.issues.length) {
|
|
274
|
+
lines.push('', 'Issues')
|
|
275
|
+
for (const issue of report.issues.slice(0, 8)) {
|
|
276
|
+
const icon = issue.severity === 'error' ? '✗' : issue.severity === 'warn' ? '!' : '·'
|
|
277
|
+
lines.push(` ${icon} [${issue.code}] ${issue.message}`)
|
|
278
|
+
}
|
|
279
|
+
if (report.issues.length > 8) {
|
|
280
|
+
lines.push(` … +${report.issues.length - 8} more`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
lines.push('', 'Next actions', ...report.nextActions.map((action) => ` → ${action}`))
|
|
285
|
+
return lines
|
|
286
|
+
}
|
|
@@ -183,6 +183,18 @@ export const buildLookup = (
|
|
|
183
183
|
}
|
|
184
184
|
ownership[pkg.id] = record
|
|
185
185
|
|
|
186
|
+
const bridge = record.humanDoc
|
|
187
|
+
? /^https?:\/\//.test(record.humanDoc)
|
|
188
|
+
? { humanDoc: 'external' as const }
|
|
189
|
+
: { humanDoc: 'linked' as const }
|
|
190
|
+
: config.corpus.human
|
|
191
|
+
? {
|
|
192
|
+
humanDoc: 'missing' as const,
|
|
193
|
+
action: 'ak-docs bootstrap agent-docs',
|
|
194
|
+
bootstrap: `docs/for-agents/human/${pkg.id}.md`,
|
|
195
|
+
}
|
|
196
|
+
: undefined
|
|
197
|
+
|
|
186
198
|
handoffs[pkg.id] = {
|
|
187
199
|
type: 'agent-handoff',
|
|
188
200
|
schemaVersion: 1,
|
|
@@ -201,7 +213,13 @@ export const buildLookup = (
|
|
|
201
213
|
editRoots: [record.path],
|
|
202
214
|
checks: [...record.checks],
|
|
203
215
|
...(record.humanDoc ? { humanDoc: record.humanDoc } : {}),
|
|
204
|
-
|
|
216
|
+
...(bridge ? { bridge } : {}),
|
|
217
|
+
notes: [
|
|
218
|
+
...(record.purpose ? [record.purpose] : []),
|
|
219
|
+
...(!record.humanDoc && config.corpus.human
|
|
220
|
+
? [`Human guide missing for ${pkg.id}. Run: ak-docs bootstrap agent-docs`]
|
|
221
|
+
: []),
|
|
222
|
+
],
|
|
205
223
|
}
|
|
206
224
|
}
|
|
207
225
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, watch } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import { buildDocBridgeIndex } from './build-index.js'
|
|
6
|
+
|
|
7
|
+
export type WatchIndexOptions = {
|
|
8
|
+
readonly root: string
|
|
9
|
+
readonly config: DocBridgeConfigV1
|
|
10
|
+
readonly configPath?: string
|
|
11
|
+
readonly debounceMs?: number
|
|
12
|
+
readonly onRebuild?: (summary: { knowledgeCount: number; handoffCount: number; hash: string }) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WATCH_PATTERN = /\.(md|mdx|json|ya?ml|mdc)$/i
|
|
16
|
+
|
|
17
|
+
const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?: string): string[] => {
|
|
18
|
+
const roots = new Set<string>()
|
|
19
|
+
roots.add(resolve(root, config.corpus.agent.root))
|
|
20
|
+
const humanSources = config.corpus.human
|
|
21
|
+
? Array.isArray(config.corpus.human)
|
|
22
|
+
? config.corpus.human
|
|
23
|
+
: [config.corpus.human]
|
|
24
|
+
: []
|
|
25
|
+
for (const source of humanSources) {
|
|
26
|
+
const humanOpts = source.options ?? {}
|
|
27
|
+
for (const key of ['contentDir', 'docsDir', 'root']) {
|
|
28
|
+
const value = humanOpts[key]
|
|
29
|
+
if (typeof value === 'string' && value.length) roots.add(resolve(root, value))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (configPath) roots.add(dirname(resolve(configPath)))
|
|
33
|
+
return [...roots].filter((dir) => existsSync(dir))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> => {
|
|
37
|
+
const debounceMs = opts.debounceMs ?? 350
|
|
38
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
39
|
+
let running = false
|
|
40
|
+
|
|
41
|
+
const rebuild = (): void => {
|
|
42
|
+
if (timer) clearTimeout(timer)
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
if (running) return
|
|
45
|
+
running = true
|
|
46
|
+
try {
|
|
47
|
+
const result = buildDocBridgeIndex({ root: opts.root, config: opts.config })
|
|
48
|
+
const handoffCount = Object.keys(result.index.handoffs ?? {}).length
|
|
49
|
+
const summary = {
|
|
50
|
+
knowledgeCount: result.index.knowledge.length,
|
|
51
|
+
handoffCount,
|
|
52
|
+
hash: result.index.contentHash.slice(0, 8),
|
|
53
|
+
}
|
|
54
|
+
opts.onRebuild?.(summary)
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
`[ak-docs] indexed ${summary.knowledgeCount} docs, ${summary.handoffCount} handoffs (${summary.hash}…)\n`,
|
|
57
|
+
)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`[ak-docs] index failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
61
|
+
)
|
|
62
|
+
} finally {
|
|
63
|
+
running = false
|
|
64
|
+
}
|
|
65
|
+
}, debounceMs)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
rebuild()
|
|
69
|
+
|
|
70
|
+
for (const dir of collectWatchRoots(opts.root, opts.config, opts.configPath)) {
|
|
71
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
72
|
+
if (!filename || !WATCH_PATTERN.test(filename)) return
|
|
73
|
+
rebuild()
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const configDir = resolve(opts.root)
|
|
78
|
+
if (existsSync(configDir)) {
|
|
79
|
+
watch(configDir, (_event, filename) => {
|
|
80
|
+
if (!filename || !/doc-bridge\.config/.test(filename)) return
|
|
81
|
+
rebuild()
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.stdout.write(
|
|
86
|
+
`[ak-docs] watching ${collectWatchRoots(opts.root, opts.config, opts.configPath).join(', ') || opts.root} (Ctrl+C to stop)\n`,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return new Promise((resolvePromise) => {
|
|
90
|
+
const onSignal = () => resolvePromise(0)
|
|
91
|
+
process.once('SIGINT', onSignal)
|
|
92
|
+
process.once('SIGTERM', onSignal)
|
|
93
|
+
})
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ export {
|
|
|
20
20
|
HandoffTargetTypeSchema,
|
|
21
21
|
HANDOFF_SCHEMA_VERSION,
|
|
22
22
|
normalizeAgentHandoff,
|
|
23
|
+
HandoffBridgeSchema,
|
|
23
24
|
type AgentHandoffV1,
|
|
25
|
+
type HandoffBridge,
|
|
24
26
|
type AgentSearchV1,
|
|
25
27
|
type HandoffTarget,
|
|
26
28
|
type HandoffTargetType,
|
|
@@ -74,6 +76,22 @@ export {
|
|
|
74
76
|
type GateRunResult,
|
|
75
77
|
} from './gates/run-gates.js'
|
|
76
78
|
export { MCP_TOOLS, handleMcpRequest, startMcpStdioServer } from './mcp/server.js'
|
|
79
|
+
export { installMcpConfig, mcpSnippet, type McpInstallResult, type McpInstallTarget } from './mcp/install.js'
|
|
80
|
+
export { runDoctor, formatDoctorText, type DoctorReport, type DoctorIssue, type DoctorCoverage } from './doctor/run-doctor.js'
|
|
81
|
+
export {
|
|
82
|
+
doctorBadgeMetrics,
|
|
83
|
+
formatDoctorBadgeJson,
|
|
84
|
+
formatDoctorBadgeMarkdown,
|
|
85
|
+
type DoctorBadgeMetrics,
|
|
86
|
+
} from './doctor/badge.js'
|
|
87
|
+
export { watchDocBridgeIndex, type WatchIndexOptions } from './index-builder/watch-index.js'
|
|
88
|
+
export {
|
|
89
|
+
promoteMemoryToGithubPr,
|
|
90
|
+
writePromotionDraft,
|
|
91
|
+
defaultPromotionDraftPath,
|
|
92
|
+
type GithubPrOptions,
|
|
93
|
+
type GithubPrResult,
|
|
94
|
+
} from './memory/github-pr.js'
|
|
77
95
|
export { sha256NormalizedV1 } from './index-builder/content-hash.js'
|
|
78
96
|
export { IndexNotFoundError, indexFilePath, loadDocBridgeIndex, resolveRoot } from './query/load-index.js'
|
|
79
97
|
export { runQuery, type QueryKind, type QueryRequest, type QueryResult } from './query/query.js'
|
|
@@ -118,3 +136,9 @@ export { projectRootFromConfigPath } from './config/load-config.js'
|
|
|
118
136
|
export { createDocBridgeRag } from './intelligence/rag.js'
|
|
119
137
|
export { runChatOnce, startInkChat } from './intelligence/chat.js'
|
|
120
138
|
export { PeerMissingError, layer1InstallHint } from './intelligence/peers.js'
|
|
139
|
+
export {
|
|
140
|
+
DOC_BRIDGE_PATTERN_ID,
|
|
141
|
+
DOC_BRIDGE_PATTERN_META,
|
|
142
|
+
docBridgePatternMarkdown,
|
|
143
|
+
docBridgePatternPayload,
|
|
144
|
+
} from './playbook/doc-bridge-pattern.js'
|
|
@@ -9,6 +9,19 @@ const readEnv = (name: string | undefined): string | undefined => {
|
|
|
9
9
|
return value && value.length > 0 ? value : undefined
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const providerDefaultApiKeyEnv = (provider: string): string | undefined => {
|
|
13
|
+
switch (provider) {
|
|
14
|
+
case 'openai':
|
|
15
|
+
return 'OPENAI_API_KEY'
|
|
16
|
+
case 'anthropic':
|
|
17
|
+
return 'ANTHROPIC_API_KEY'
|
|
18
|
+
case 'openrouter':
|
|
19
|
+
return 'OPENROUTER_API_KEY'
|
|
20
|
+
default:
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export type ResolvedIntelligence = {
|
|
13
26
|
readonly adapter: unknown
|
|
14
27
|
readonly embed: unknown
|
|
@@ -30,7 +43,7 @@ export const resolveIntelligenceRuntime = async (
|
|
|
30
43
|
const adapters = await importPeer<typeof import('@agentskit/adapters')>('@agentskit/adapters')
|
|
31
44
|
const provider = adapterCfg.provider
|
|
32
45
|
const model = adapterCfg.model
|
|
33
|
-
const apiKey = readEnv(adapterCfg.apiKeyEnv) ?? readEnv(
|
|
46
|
+
const apiKey = readEnv(adapterCfg.apiKeyEnv) ?? readEnv(providerDefaultApiKeyEnv(provider))
|
|
34
47
|
const baseUrl = adapterCfg.baseUrl
|
|
35
48
|
|
|
36
49
|
let adapter: unknown
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export type McpInstallTarget = 'cursor' | 'claude'
|
|
6
|
+
|
|
7
|
+
export type McpInstallResult = {
|
|
8
|
+
readonly ok: boolean
|
|
9
|
+
readonly target: McpInstallTarget
|
|
10
|
+
readonly configPath: string
|
|
11
|
+
readonly created: boolean
|
|
12
|
+
readonly serverName: string
|
|
13
|
+
readonly nextSteps: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SERVER_NAME = 'ak-docs'
|
|
17
|
+
|
|
18
|
+
const mcpServerEntry = (root: string) => ({
|
|
19
|
+
command: 'npx',
|
|
20
|
+
args: ['ak-docs', 'mcp'],
|
|
21
|
+
cwd: root,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const readJson = (path: string): Record<string, unknown> => {
|
|
25
|
+
if (!existsSync(path)) return {}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown
|
|
28
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
29
|
+
? (parsed as Record<string, unknown>)
|
|
30
|
+
: {}
|
|
31
|
+
} catch {
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const writeJson = (path: string, value: Record<string, unknown>): void => {
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
38
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolveTargetPath = (target: McpInstallTarget, root: string): string => {
|
|
42
|
+
if (target === 'cursor') return resolve(root, '.cursor', 'mcp.json')
|
|
43
|
+
return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const installMcpConfig = (
|
|
47
|
+
root: string,
|
|
48
|
+
target: McpInstallTarget,
|
|
49
|
+
): McpInstallResult => {
|
|
50
|
+
const configPath = resolveTargetPath(target, root)
|
|
51
|
+
const created = !existsSync(configPath)
|
|
52
|
+
const existing = readJson(configPath)
|
|
53
|
+
const servers =
|
|
54
|
+
existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers)
|
|
55
|
+
? { ...(existing.mcpServers as Record<string, unknown>) }
|
|
56
|
+
: {}
|
|
57
|
+
|
|
58
|
+
servers[SERVER_NAME] = mcpServerEntry(root)
|
|
59
|
+
writeJson(configPath, { ...existing, mcpServers: servers })
|
|
60
|
+
|
|
61
|
+
const nextSteps =
|
|
62
|
+
target === 'cursor'
|
|
63
|
+
? [
|
|
64
|
+
'Restart Cursor or reload MCP servers',
|
|
65
|
+
'Paste the doc-bridge skill into Cursor rules (see docs/skills/doc-bridge.md)',
|
|
66
|
+
'Before editing packages/* call handoff.resolve',
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
'Restart Claude Desktop',
|
|
70
|
+
'Run ak-docs index after doc changes',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
target,
|
|
76
|
+
configPath,
|
|
77
|
+
created,
|
|
78
|
+
serverName: SERVER_NAME,
|
|
79
|
+
nextSteps,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const mcpSnippet = (root: string): string =>
|
|
84
|
+
JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpServerEntry(root) } }, null, 2)
|