@agentskit/doc-bridge 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +6 -0
- package/PRIVACY.md +38 -0
- package/README.md +30 -3
- package/SECURITY.md +7 -2
- package/action.yml +1 -1
- package/dist/cli/program.js +400 -156
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DGI9TBLE.d.ts → index-DAeq_OIi.d.ts} +22 -22
- package/dist/index.d.ts +47 -12
- package/dist/index.js +376 -131
- package/dist/index.js.map +1 -1
- package/docs/POSITIONING.md +1 -1
- package/docs/examples.md +4 -0
- package/docs/getting-started.md +1 -1
- package/docs/landing/index.html +1 -1
- package/docs/mcp.md +16 -0
- package/docs/qa/vitepress-starlight-adapters.md +19 -0
- package/docs/spec/config-v1.md +33 -2
- package/examples/nextra-only.config.ts +17 -0
- package/examples/nx-monorepo.config.ts +11 -0
- package/examples/starlight-only.config.ts +17 -0
- package/examples/vitepress-only.config.ts +19 -0
- package/mcpb/.mcpbignore +8 -0
- package/mcpb/icon.png +0 -0
- package/mcpb/manifest.json +96 -0
- package/package.json +14 -11
- package/src/config/schema.ts +3 -1
- package/src/index-builder/build-handoffs.ts +2 -0
- package/src/index-builder/build-index.ts +7 -1
- package/src/index-builder/human-adapters/index.ts +6 -0
- package/src/index-builder/human-adapters/nextra.ts +43 -0
- package/src/index-builder/human-adapters/starlight.ts +40 -0
- package/src/index-builder/human-adapters/vitepress.ts +43 -0
- package/src/index-builder/plugins/nx.ts +161 -0
- package/src/index-builder/plugins/pnpm-monorepo.ts +1 -0
- package/src/index-builder/watch-index.ts +11 -2
- package/src/index.ts +1 -0
- package/src/mcp/server.ts +58 -26
- package/src/version.ts +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../../config/schema.js'
|
|
5
|
+
import { detectPackageManager } from '../../lib/package-manager.js'
|
|
6
|
+
import { toPosix } from '../../lib/paths.js'
|
|
7
|
+
import { walkFiles } from '../../lib/walk.js'
|
|
8
|
+
import type { DiscoveredPackage } from './pnpm-monorepo.js'
|
|
9
|
+
|
|
10
|
+
const NX_SCAN_SKIP = new Set([
|
|
11
|
+
'node_modules',
|
|
12
|
+
'.git',
|
|
13
|
+
'.nx',
|
|
14
|
+
'dist',
|
|
15
|
+
'coverage',
|
|
16
|
+
'.doc-bridge',
|
|
17
|
+
])
|
|
18
|
+
const NX_PROJECT_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
19
|
+
|
|
20
|
+
type JsonRecord = Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
const isRecord = (value: unknown): value is JsonRecord =>
|
|
23
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
24
|
+
|
|
25
|
+
const readJsonRecord = (path: string): JsonRecord | undefined => {
|
|
26
|
+
try {
|
|
27
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
28
|
+
return isRecord(parsed) ? parsed : undefined
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const safeProjectPath = (
|
|
35
|
+
root: string,
|
|
36
|
+
manifestDir: string,
|
|
37
|
+
declaredRoot?: unknown,
|
|
38
|
+
): string | undefined => {
|
|
39
|
+
const candidate = typeof declaredRoot === 'string' ? resolve(root, declaredRoot) : manifestDir
|
|
40
|
+
const rel = toPosix(relative(root, candidate)) || '.'
|
|
41
|
+
if (isAbsolute(rel) || rel === '..' || rel.startsWith('../')) return undefined
|
|
42
|
+
try {
|
|
43
|
+
if (!lstatSync(candidate).isDirectory()) return undefined
|
|
44
|
+
const canonicalRoot = realpathSync.native(root)
|
|
45
|
+
const canonicalCandidate = realpathSync.native(candidate)
|
|
46
|
+
const canonicalRel = relative(canonicalRoot, canonicalCandidate)
|
|
47
|
+
if (
|
|
48
|
+
isAbsolute(canonicalRel) ||
|
|
49
|
+
canonicalRel === '..' ||
|
|
50
|
+
canonicalRel.startsWith(`..${sep}`)
|
|
51
|
+
) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
return rel
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const packageId = (name: string): string =>
|
|
61
|
+
name.startsWith('@') ? (name.split('/').at(-1) ?? name) : name
|
|
62
|
+
|
|
63
|
+
const commandPrefix = (root: string): string => {
|
|
64
|
+
const manager = detectPackageManager(root)
|
|
65
|
+
if (manager === 'pnpm') return 'pnpm exec nx run'
|
|
66
|
+
if (manager === 'yarn') return 'yarn nx run'
|
|
67
|
+
if (manager === 'bun') return 'bunx nx run'
|
|
68
|
+
return 'npx nx run'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inferredChecks = (
|
|
72
|
+
root: string,
|
|
73
|
+
config: DocBridgeConfigV1,
|
|
74
|
+
projectName: string,
|
|
75
|
+
targets: ReadonlySet<string>,
|
|
76
|
+
): string[] | undefined => {
|
|
77
|
+
const prefix = commandPrefix(root)
|
|
78
|
+
const strict = (config.gates?.preset ?? 'minimal') !== 'minimal'
|
|
79
|
+
const checks = [
|
|
80
|
+
...(targets.has('test') ? [`${prefix} ${projectName}:test`] : []),
|
|
81
|
+
...(strict && targets.has('lint') ? [`${prefix} ${projectName}:lint`] : []),
|
|
82
|
+
]
|
|
83
|
+
return checks.length ? checks : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type RankedProject = DiscoveredPackage & {
|
|
87
|
+
readonly rank: number
|
|
88
|
+
readonly targets: readonly string[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const discoverNxProjects = (
|
|
92
|
+
root: string,
|
|
93
|
+
config: DocBridgeConfigV1,
|
|
94
|
+
): DiscoveredPackage[] => {
|
|
95
|
+
if (!existsSync(resolve(root, 'nx.json'))) return []
|
|
96
|
+
|
|
97
|
+
const manifests = walkFiles(root, {
|
|
98
|
+
extensions: ['project.json', 'package.json'],
|
|
99
|
+
skipDirs: NX_SCAN_SKIP,
|
|
100
|
+
maxFiles: 10_000,
|
|
101
|
+
})
|
|
102
|
+
const projects = new Map<string, RankedProject>()
|
|
103
|
+
|
|
104
|
+
for (const manifest of manifests) {
|
|
105
|
+
const json = readJsonRecord(manifest)
|
|
106
|
+
if (!json) continue
|
|
107
|
+
const manifestDir = dirname(manifest)
|
|
108
|
+
const manifestName = basename(manifest)
|
|
109
|
+
const isProjectJson = manifestName === 'project.json'
|
|
110
|
+
if (!isProjectJson && manifestName !== 'package.json') continue
|
|
111
|
+
const nx = json.nx
|
|
112
|
+
if (!isProjectJson && !isRecord(nx)) continue
|
|
113
|
+
|
|
114
|
+
const path = safeProjectPath(root, manifestDir, isProjectJson ? json.root : undefined)
|
|
115
|
+
if (!path) continue
|
|
116
|
+
const projectPackage = isProjectJson
|
|
117
|
+
? readJsonRecord(resolve(root, path, 'package.json'))
|
|
118
|
+
: undefined
|
|
119
|
+
const projectName =
|
|
120
|
+
typeof json.name === 'string'
|
|
121
|
+
? json.name
|
|
122
|
+
: typeof projectPackage?.name === 'string'
|
|
123
|
+
? projectPackage.name
|
|
124
|
+
: basename(path === '.' ? root : path)
|
|
125
|
+
if (!NX_PROJECT_NAME.test(projectName)) continue
|
|
126
|
+
|
|
127
|
+
const targets = new Set<string>()
|
|
128
|
+
const targetRecord = isProjectJson ? json.targets : isRecord(nx) ? nx.targets : undefined
|
|
129
|
+
if (isRecord(targetRecord)) {
|
|
130
|
+
for (const target of Object.keys(targetRecord)) targets.add(target)
|
|
131
|
+
}
|
|
132
|
+
if (!isProjectJson && isRecord(json.scripts)) {
|
|
133
|
+
for (const script of Object.keys(json.scripts)) targets.add(script)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const id = packageId(projectName)
|
|
137
|
+
const rank = isProjectJson ? 2 : 1
|
|
138
|
+
const existing = projects.get(id)
|
|
139
|
+
if (existing && existing.path !== path) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Nx project identity collision for "${id}": "${existing.name ?? id}" at "${existing.path}" and "${projectName}" at "${path}". Use unique Nx project names.`,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
const mergedTargets = [...new Set([...(existing?.targets ?? []), ...targets])]
|
|
145
|
+
const projectJsonWins = rank >= (existing?.rank ?? 0)
|
|
146
|
+
const resolvedName = projectJsonWins ? projectName : (existing?.name ?? projectName)
|
|
147
|
+
const checks = inferredChecks(root, config, resolvedName, new Set(mergedTargets))
|
|
148
|
+
projects.set(id, {
|
|
149
|
+
id,
|
|
150
|
+
path,
|
|
151
|
+
name: resolvedName,
|
|
152
|
+
...(checks ? { checks } : {}),
|
|
153
|
+
rank: projectJsonWins ? rank : (existing?.rank ?? rank),
|
|
154
|
+
targets: mergedTargets,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [...projects.values()]
|
|
159
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
160
|
+
.map(({ rank: _rank, targets: _targets, ...project }) => project)
|
|
161
|
+
}
|
|
@@ -13,6 +13,7 @@ export type WatchIndexOptions = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const WATCH_PATTERN = /\.(md|mdx|json|ya?ml|mdc)$/i
|
|
16
|
+
const NX_MANIFEST_PATTERN = /(^|[/\\])(project|package)\.json$/i
|
|
16
17
|
|
|
17
18
|
const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?: string): string[] => {
|
|
18
19
|
const roots = new Set<string>()
|
|
@@ -24,7 +25,7 @@ const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?:
|
|
|
24
25
|
: []
|
|
25
26
|
for (const source of humanSources) {
|
|
26
27
|
const humanOpts = source.options ?? {}
|
|
27
|
-
for (const key of ['contentDir', 'docsDir', 'root']) {
|
|
28
|
+
for (const key of ['contentDir', 'docsDir', 'root', 'srcDir']) {
|
|
28
29
|
const value = humanOpts[key]
|
|
29
30
|
if (typeof value === 'string' && value.length) roots.add(resolve(root, value))
|
|
30
31
|
}
|
|
@@ -74,6 +75,14 @@ export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> =>
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const nxRoot = resolve(opts.root)
|
|
79
|
+
if (opts.config.routing?.plugin === 'nx' && existsSync(nxRoot)) {
|
|
80
|
+
watch(nxRoot, { recursive: true }, (_event, filename) => {
|
|
81
|
+
if (!filename || !NX_MANIFEST_PATTERN.test(filename)) return
|
|
82
|
+
rebuild()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
const configDir = resolve(opts.root)
|
|
78
87
|
if (existsSync(configDir)) {
|
|
79
88
|
watch(configDir, (_event, filename) => {
|
|
@@ -91,4 +100,4 @@ export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> =>
|
|
|
91
100
|
process.once('SIGINT', onSignal)
|
|
92
101
|
process.once('SIGTERM', onSignal)
|
|
93
102
|
})
|
|
94
|
-
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,7 @@ export {
|
|
|
155
155
|
export { PACKAGE_VERSION } from './version.js'
|
|
156
156
|
|
|
157
157
|
export { collectPackages, buildLookup } from './index-builder/build-handoffs.js'
|
|
158
|
+
export { discoverNxProjects } from './index-builder/plugins/nx.js'
|
|
158
159
|
export { projectRootFromConfigPath } from './config/load-config.js'
|
|
159
160
|
export { createDocBridgeRag } from './intelligence/rag.js'
|
|
160
161
|
export { runChatOnce, startInkChat } from './intelligence/chat.js'
|
package/src/mcp/server.ts
CHANGED
|
@@ -30,7 +30,9 @@ type McpContext = {
|
|
|
30
30
|
export const MCP_TOOLS = [
|
|
31
31
|
{
|
|
32
32
|
name: 'handoff.resolve',
|
|
33
|
-
|
|
33
|
+
title: 'Resolve repository handoff',
|
|
34
|
+
description: 'Resolve a package or ownership id to its deterministic AgentHandoff.',
|
|
35
|
+
annotations: { readOnlyHint: true },
|
|
34
36
|
inputSchema: {
|
|
35
37
|
type: 'object',
|
|
36
38
|
properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['package', 'ownership'] } },
|
|
@@ -39,7 +41,9 @@ export const MCP_TOOLS = [
|
|
|
39
41
|
},
|
|
40
42
|
{
|
|
41
43
|
name: 'doc.search',
|
|
42
|
-
|
|
44
|
+
title: 'Search repository documentation',
|
|
45
|
+
description: 'Search the deterministic Doc Bridge index for repository documentation.',
|
|
46
|
+
annotations: { readOnlyHint: true },
|
|
43
47
|
inputSchema: {
|
|
44
48
|
type: 'object',
|
|
45
49
|
properties: { term: { type: 'string' }, limit: { type: 'number' } },
|
|
@@ -48,7 +52,9 @@ export const MCP_TOOLS = [
|
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
name: 'doc.get',
|
|
51
|
-
|
|
55
|
+
title: 'Read indexed documentation',
|
|
56
|
+
description: 'Read one indexed agent documentation file by id or indexed path.',
|
|
57
|
+
annotations: { readOnlyHint: true },
|
|
52
58
|
inputSchema: {
|
|
53
59
|
type: 'object',
|
|
54
60
|
properties: { id: { type: 'string' }, path: { type: 'string' } },
|
|
@@ -56,12 +62,16 @@ export const MCP_TOOLS = [
|
|
|
56
62
|
},
|
|
57
63
|
{
|
|
58
64
|
name: 'gate.status',
|
|
59
|
-
|
|
65
|
+
title: 'Check documentation gates',
|
|
66
|
+
description: 'Evaluate documentation gates without writing files.',
|
|
67
|
+
annotations: { readOnlyHint: true },
|
|
60
68
|
inputSchema: { type: 'object', properties: {} },
|
|
61
69
|
},
|
|
62
70
|
{
|
|
63
71
|
name: 'retriever.query',
|
|
64
|
-
|
|
72
|
+
title: 'Retrieve documentation context',
|
|
73
|
+
description: 'Return relevant local Doc Bridge index chunks for a query.',
|
|
74
|
+
annotations: { readOnlyHint: true },
|
|
65
75
|
inputSchema: {
|
|
66
76
|
type: 'object',
|
|
67
77
|
properties: { query: { type: 'string' }, limit: { type: 'number' } },
|
|
@@ -70,17 +80,23 @@ export const MCP_TOOLS = [
|
|
|
70
80
|
},
|
|
71
81
|
{
|
|
72
82
|
name: 'memory.classify',
|
|
73
|
-
|
|
83
|
+
title: 'Classify memory candidates',
|
|
84
|
+
description: 'Classify local memory candidates into agent, human, playbook, or discard routes.',
|
|
85
|
+
annotations: { readOnlyHint: true },
|
|
74
86
|
inputSchema: { type: 'object', properties: {} },
|
|
75
87
|
},
|
|
76
88
|
{
|
|
77
89
|
name: 'memory.promoteDraft',
|
|
78
|
-
|
|
90
|
+
title: 'Draft memory promotion',
|
|
91
|
+
description: 'Build a reviewable draft promotion body from local memory candidates without publishing it.',
|
|
92
|
+
annotations: { readOnlyHint: true },
|
|
79
93
|
inputSchema: { type: 'object', properties: {} },
|
|
80
94
|
},
|
|
81
95
|
{
|
|
82
96
|
name: 'registry.topology',
|
|
83
|
-
|
|
97
|
+
title: 'Inspect registry topology',
|
|
98
|
+
description: 'Return the static Doc Bridge curator and delegate topology.',
|
|
99
|
+
annotations: { readOnlyHint: true },
|
|
84
100
|
inputSchema: { type: 'object', properties: {} },
|
|
85
101
|
},
|
|
86
102
|
] as const
|
|
@@ -223,12 +239,14 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
|
|
|
223
239
|
throw new Error(`Unsupported MCP method "${request.method ?? ''}"`)
|
|
224
240
|
}
|
|
225
241
|
|
|
226
|
-
|
|
242
|
+
type StdioFraming = 'content-length' | 'json-line'
|
|
243
|
+
|
|
244
|
+
const writeFrame = (payload: unknown, framing: StdioFraming): void => {
|
|
227
245
|
const body = JSON.stringify(payload)
|
|
228
|
-
process.stdout.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`)
|
|
246
|
+
process.stdout.write(framing === 'json-line' ? `${body}\n` : `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`)
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
const respond = (ctx: McpContext, request: JsonRpcRequest): void => {
|
|
249
|
+
const respond = (ctx: McpContext, request: JsonRpcRequest, framing: StdioFraming): void => {
|
|
232
250
|
if (request.id === undefined) {
|
|
233
251
|
try {
|
|
234
252
|
handleMcpRequest(ctx, request)
|
|
@@ -240,13 +258,13 @@ const respond = (ctx: McpContext, request: JsonRpcRequest): void => {
|
|
|
240
258
|
|
|
241
259
|
try {
|
|
242
260
|
const result = handleMcpRequest(ctx, request)
|
|
243
|
-
writeFrame({ jsonrpc: '2.0', id: request.id, result: result ?? {} })
|
|
261
|
+
writeFrame({ jsonrpc: '2.0', id: request.id, result: result ?? {} }, framing)
|
|
244
262
|
} catch (error) {
|
|
245
263
|
writeFrame({
|
|
246
264
|
jsonrpc: '2.0',
|
|
247
265
|
id: request.id,
|
|
248
266
|
error: { code: -32000, message: error instanceof Error ? error.message : String(error) },
|
|
249
|
-
})
|
|
267
|
+
}, framing)
|
|
250
268
|
}
|
|
251
269
|
}
|
|
252
270
|
|
|
@@ -255,21 +273,35 @@ export const startMcpStdioServer = (ctx: McpContext): void => {
|
|
|
255
273
|
process.stdin.on('data', (chunk: Buffer) => {
|
|
256
274
|
buffer = Buffer.concat([buffer, chunk])
|
|
257
275
|
while (true) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
276
|
+
if (/^content-length:/i.test(buffer.subarray(0, Math.min(buffer.length, 32)).toString('utf8'))) {
|
|
277
|
+
const headerEnd = buffer.indexOf('\r\n\r\n')
|
|
278
|
+
if (headerEnd === -1) return
|
|
279
|
+
const header = buffer.subarray(0, headerEnd).toString('utf8')
|
|
280
|
+
const match = /content-length:\s*(\d+)/i.exec(header)
|
|
281
|
+
if (!match?.[1]) {
|
|
282
|
+
buffer = buffer.subarray(headerEnd + 4)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
const length = Number(match[1])
|
|
286
|
+
const bodyStart = headerEnd + 4
|
|
287
|
+
const bodyEnd = bodyStart + length
|
|
288
|
+
if (buffer.length < bodyEnd) return
|
|
289
|
+
const raw = buffer.subarray(bodyStart, bodyEnd).toString('utf8')
|
|
290
|
+
buffer = buffer.subarray(bodyEnd)
|
|
291
|
+
respond(ctx, JSON.parse(raw) as JsonRpcRequest, 'content-length')
|
|
264
292
|
continue
|
|
265
293
|
}
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
|
|
295
|
+
const lineEnd = buffer.indexOf('\n')
|
|
296
|
+
if (lineEnd === -1) return
|
|
297
|
+
const raw = buffer.subarray(0, lineEnd).toString('utf8').trim()
|
|
298
|
+
buffer = buffer.subarray(lineEnd + 1)
|
|
299
|
+
if (!raw) continue
|
|
300
|
+
try {
|
|
301
|
+
respond(ctx, JSON.parse(raw) as JsonRpcRequest, 'json-line')
|
|
302
|
+
} catch {
|
|
303
|
+
writeFrame({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }, 'json-line')
|
|
304
|
+
}
|
|
273
305
|
}
|
|
274
306
|
})
|
|
275
307
|
process.stdin.resume()
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '1.
|
|
1
|
+
export const PACKAGE_VERSION = '1.3.0'
|