@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,63 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from './schema.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_AGENT_EXCLUDE = ['**/node_modules/**', '**/.git/**'] as const
|
|
4
|
+
|
|
5
|
+
export const applyConfigDefaults = (config: DocBridgeConfigV1): DocBridgeConfigV1 => {
|
|
6
|
+
const agentRoot = config.corpus.agent.root
|
|
7
|
+
const agentIndex = config.corpus.agent.index ?? `${agentRoot}/INDEX.md`
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
...config,
|
|
11
|
+
corpus: {
|
|
12
|
+
...config.corpus,
|
|
13
|
+
agent: {
|
|
14
|
+
...config.corpus.agent,
|
|
15
|
+
index: agentIndex,
|
|
16
|
+
include: config.corpus.agent.include ?? ['**/*.{md,mdx}'],
|
|
17
|
+
exclude: config.corpus.agent.exclude ?? [...DEFAULT_AGENT_EXCLUDE],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
index: {
|
|
21
|
+
outFile: '.doc-bridge/index.json',
|
|
22
|
+
contentHash: 'sha256-normalized-v1',
|
|
23
|
+
llmsTxt: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
outFile: 'llms.txt',
|
|
26
|
+
...config.index?.llmsTxt,
|
|
27
|
+
},
|
|
28
|
+
capabilities: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
outFile: '.doc-bridge/capabilities.json',
|
|
31
|
+
...config.index?.capabilities,
|
|
32
|
+
},
|
|
33
|
+
...config.index,
|
|
34
|
+
},
|
|
35
|
+
gates: {
|
|
36
|
+
preset: 'minimal',
|
|
37
|
+
...config.gates,
|
|
38
|
+
},
|
|
39
|
+
surfaces: {
|
|
40
|
+
cli: {
|
|
41
|
+
bin: 'ak-docs',
|
|
42
|
+
defaultFormat: 'json',
|
|
43
|
+
...config.surfaces?.cli,
|
|
44
|
+
},
|
|
45
|
+
mcp: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
tools: ['handoff.resolve', 'doc.search', 'doc.get', 'gate.status'],
|
|
48
|
+
transport: 'stdio',
|
|
49
|
+
...config.surfaces?.mcp,
|
|
50
|
+
},
|
|
51
|
+
...config.surfaces,
|
|
52
|
+
},
|
|
53
|
+
intelligence: config.intelligence
|
|
54
|
+
? {
|
|
55
|
+
enabled: false,
|
|
56
|
+
...config.intelligence,
|
|
57
|
+
chat: config.intelligence.chat
|
|
58
|
+
? { handoffFirst: true, ...config.intelligence.chat }
|
|
59
|
+
: undefined,
|
|
60
|
+
}
|
|
61
|
+
: { enabled: false },
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { applyConfigDefaults } from './defaults.js'
|
|
2
|
+
import { DocBridgeConfigV1Schema, type DocBridgeConfigV1 } from './schema.js'
|
|
3
|
+
|
|
4
|
+
/** Type-safe config helper for `doc-bridge.config.ts`. */
|
|
5
|
+
export const defineConfig = (config: DocBridgeConfigV1): DocBridgeConfigV1 =>
|
|
6
|
+
applyConfigDefaults(DocBridgeConfigV1Schema.parse(config))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { defineConfig } from './define-config.js'
|
|
2
|
+
export { applyConfigDefaults } from './defaults.js'
|
|
3
|
+
export {
|
|
4
|
+
loadConfig,
|
|
5
|
+
resolveProjectRoot,
|
|
6
|
+
projectRootFromConfigPath,
|
|
7
|
+
ConfigNotFoundError,
|
|
8
|
+
} from './load-config.js'
|
|
9
|
+
export {
|
|
10
|
+
DocBridgeConfigV1Schema,
|
|
11
|
+
AgentCorpusConfigSchema,
|
|
12
|
+
HumanCorpusConfigSchema,
|
|
13
|
+
type DocBridgeConfigV1,
|
|
14
|
+
type AgentCorpusConfig,
|
|
15
|
+
} from './schema.js'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
|
|
5
|
+
import { applyConfigDefaults } from './defaults.js'
|
|
6
|
+
import { DocBridgeConfigV1Schema, type DocBridgeConfigV1 } from './schema.js'
|
|
7
|
+
|
|
8
|
+
const CONFIG_CANDIDATES = [
|
|
9
|
+
'doc-bridge.config.ts',
|
|
10
|
+
'doc-bridge.config.mts',
|
|
11
|
+
'doc-bridge.config.js',
|
|
12
|
+
'doc-bridge.config.mjs',
|
|
13
|
+
'doc-bridge.config.json',
|
|
14
|
+
'doc-bridge.config.yaml',
|
|
15
|
+
'doc-bridge.config.yml',
|
|
16
|
+
'package.json',
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
export type LoadConfigOptions = {
|
|
20
|
+
readonly cwd?: string
|
|
21
|
+
readonly explicitPath?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LoadConfigResult = {
|
|
25
|
+
readonly config: DocBridgeConfigV1
|
|
26
|
+
readonly path: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ConfigNotFoundError extends Error {
|
|
30
|
+
constructor(readonly cwd: string) {
|
|
31
|
+
super(
|
|
32
|
+
`No doc-bridge config found in ${cwd}. Run: ak-docs init — or pass --config <path>.`,
|
|
33
|
+
)
|
|
34
|
+
this.name = 'ConfigNotFoundError'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const findConfigPath = (cwd: string, explicitPath?: string): string | null => {
|
|
39
|
+
if (explicitPath) {
|
|
40
|
+
const abs = resolve(cwd, explicitPath)
|
|
41
|
+
return existsSync(abs) ? abs : null
|
|
42
|
+
}
|
|
43
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
44
|
+
const candidate = join(cwd, name)
|
|
45
|
+
if (!existsSync(candidate)) continue
|
|
46
|
+
if (name !== 'package.json') return candidate
|
|
47
|
+
const pkg = parseJsonConfig(readFileSync(candidate, 'utf8'), candidate) as { docBridge?: unknown }
|
|
48
|
+
if (pkg.docBridge) return candidate
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parseJsonConfig = (raw: string, path: string): unknown => {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw) as unknown
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
58
|
+
throw new Error(`Failed to parse JSON config at ${path}: ${message}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parseCodeConfig = (raw: string, path: string): unknown => {
|
|
63
|
+
const code = raw
|
|
64
|
+
.replace(/import\s+type\s+[\s\S]*?;?\n/g, '')
|
|
65
|
+
.replace(/import\s+\{\s*defineConfig\s*\}\s+from\s+['"]@agentskit\/doc-bridge(?:\/config)?['"];?\n?/g, '')
|
|
66
|
+
.replace(/\s+satisfies\s+[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*(?:;|\)|$))/g, '')
|
|
67
|
+
.replace(/export\s+default/, 'module.exports.default =')
|
|
68
|
+
|
|
69
|
+
if (/\bimport\b/.test(code)) {
|
|
70
|
+
throw new Error(`Unsupported import in ${path}. Static config only supports defineConfig imports.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sandbox = {
|
|
74
|
+
module: { exports: {} as { default?: unknown } },
|
|
75
|
+
exports: {},
|
|
76
|
+
defineConfig: (config: unknown) => config,
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
vm.runInNewContext(code, sandbox, { timeout: 250, filename: path })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
82
|
+
throw new Error(`Failed to load config at ${path}: ${message}`)
|
|
83
|
+
}
|
|
84
|
+
return sandbox.module.exports.default
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const parseConfig = (input: unknown): DocBridgeConfigV1 => {
|
|
88
|
+
const result = DocBridgeConfigV1Schema.safeParse(input)
|
|
89
|
+
if (result.success) return result.data
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Invalid doc-bridge config:\n${result.error.issues.map((issue) =>
|
|
92
|
+
` - ${issue.path.join('.') || '(root)'}: ${issue.message}`,
|
|
93
|
+
).join('\n')}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** v0.1 — static JS/TS config, JSON config files, and package.json#docBridge. */
|
|
98
|
+
export const loadConfig = (opts: LoadConfigOptions = {}): LoadConfigResult => {
|
|
99
|
+
const cwd = resolve(opts.cwd ?? process.cwd())
|
|
100
|
+
const path = findConfigPath(cwd, opts.explicitPath)
|
|
101
|
+
if (!path) throw new ConfigNotFoundError(cwd)
|
|
102
|
+
|
|
103
|
+
const raw = readFileSync(path, 'utf8')
|
|
104
|
+
const ext = path.split('.').pop()?.toLowerCase()
|
|
105
|
+
if (ext === 'yaml' || ext === 'yml') {
|
|
106
|
+
throw new Error(`YAML config is not supported yet (${path}). Use doc-bridge.config.json for now.`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsed =
|
|
110
|
+
basename(path) === 'package.json'
|
|
111
|
+
? (parseJsonConfig(raw, path) as { docBridge?: unknown }).docBridge
|
|
112
|
+
: ext === 'ts' || ext === 'mts' || ext === 'js' || ext === 'mjs'
|
|
113
|
+
? parseCodeConfig(raw, path)
|
|
114
|
+
: parseJsonConfig(raw, path)
|
|
115
|
+
const config = applyConfigDefaults(parseConfig(parsed))
|
|
116
|
+
return { config, path }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const resolveProjectRoot = (start = process.cwd()): string => {
|
|
120
|
+
let cur = resolve(start)
|
|
121
|
+
for (let i = 0; i < 12; i += 1) {
|
|
122
|
+
if (findConfigPath(cur)) return cur
|
|
123
|
+
const parent = dirname(cur)
|
|
124
|
+
if (parent === cur) break
|
|
125
|
+
cur = parent
|
|
126
|
+
}
|
|
127
|
+
return resolve(start)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Project root for a loaded config file path (directory of the config). */
|
|
131
|
+
export const projectRootFromConfigPath = (
|
|
132
|
+
configFilePath: string,
|
|
133
|
+
projectRootField?: string,
|
|
134
|
+
): string => {
|
|
135
|
+
const configDir = dirname(resolve(configFilePath))
|
|
136
|
+
if (projectRootField) return resolve(configDir, projectRootField)
|
|
137
|
+
return configDir
|
|
138
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const CONFIG_SCHEMA_VERSION = 1 as const
|
|
4
|
+
|
|
5
|
+
export const HumanCorpusPluginIdSchema = z.enum([
|
|
6
|
+
'plain-markdown',
|
|
7
|
+
'fumadocs',
|
|
8
|
+
'docusaurus',
|
|
9
|
+
'mkdocs',
|
|
10
|
+
'vitepress',
|
|
11
|
+
'custom',
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
export const AgentCorpusConfigSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
root: z.string().min(1).max(512),
|
|
17
|
+
index: z.string().min(1).max(512).optional(),
|
|
18
|
+
include: z.array(z.string().min(1).max(256)).max(64).optional(),
|
|
19
|
+
exclude: z.array(z.string().min(1).max(256)).max(64).optional(),
|
|
20
|
+
okf: z
|
|
21
|
+
.object({
|
|
22
|
+
requireType: z.boolean().optional(),
|
|
23
|
+
allowedTypes: z.array(z.string().min(1).max(128)).max(64).optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict()
|
|
26
|
+
.optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict()
|
|
29
|
+
|
|
30
|
+
export const HumanCorpusConfigSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
plugin: HumanCorpusPluginIdSchema,
|
|
33
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict()
|
|
36
|
+
|
|
37
|
+
export const IndexConfigSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
outFile: z.string().min(1).max(512).optional(),
|
|
40
|
+
llmsTxt: z
|
|
41
|
+
.object({
|
|
42
|
+
enabled: z.boolean().optional(),
|
|
43
|
+
outFile: z.string().min(1).max(512).optional(),
|
|
44
|
+
preamble: z.string().max(4_000).optional(),
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.optional(),
|
|
48
|
+
capabilities: z
|
|
49
|
+
.object({
|
|
50
|
+
enabled: z.boolean().optional(),
|
|
51
|
+
outFile: z.string().min(1).max(512).optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict()
|
|
54
|
+
.optional(),
|
|
55
|
+
contentHash: z.literal('sha256-normalized-v1').optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
|
|
59
|
+
export const OwnershipEntrySchema = z
|
|
60
|
+
.object({
|
|
61
|
+
path: z.string().min(1).max(512),
|
|
62
|
+
group: z.string().min(1).max(128).optional(),
|
|
63
|
+
layer: z.string().min(1).max(32).optional(),
|
|
64
|
+
purpose: z.string().max(1024).optional(),
|
|
65
|
+
checks: z.array(z.string().min(1).max(256)).max(32).optional(),
|
|
66
|
+
agentDoc: z.string().min(1).max(512).optional(),
|
|
67
|
+
humanDoc: z.string().min(1).max(512).optional(),
|
|
68
|
+
})
|
|
69
|
+
.strict()
|
|
70
|
+
|
|
71
|
+
export const RoutingConfigSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
plugin: z
|
|
74
|
+
.enum(['pnpm-monorepo', 'npm-workspaces', 'yarn-workspaces', 'pattern-files', 'custom'])
|
|
75
|
+
.optional(),
|
|
76
|
+
options: z
|
|
77
|
+
.object({
|
|
78
|
+
packages: z.array(z.string().min(1).max(256)).max(128).optional(),
|
|
79
|
+
/** Infer ownership from corpus paths/frontmatter (default true). */
|
|
80
|
+
ownershipFromCorpus: z.boolean().optional(),
|
|
81
|
+
ownership: z.record(z.string().min(1).max(256), OwnershipEntrySchema).optional(),
|
|
82
|
+
intents: z
|
|
83
|
+
.array(
|
|
84
|
+
z
|
|
85
|
+
.object({
|
|
86
|
+
id: z.string().min(1).max(128),
|
|
87
|
+
title: z.string().min(1).max(256),
|
|
88
|
+
paths: z.array(z.string().min(1).max(512)).max(32),
|
|
89
|
+
})
|
|
90
|
+
.strict(),
|
|
91
|
+
)
|
|
92
|
+
.max(128)
|
|
93
|
+
.optional(),
|
|
94
|
+
changes: z
|
|
95
|
+
.array(
|
|
96
|
+
z
|
|
97
|
+
.object({
|
|
98
|
+
id: z.string().min(1).max(128),
|
|
99
|
+
title: z.string().min(1).max(256),
|
|
100
|
+
startHere: z.string().min(1).max(512),
|
|
101
|
+
relatedPackages: z.array(z.string().min(1).max(256)).max(32).optional(),
|
|
102
|
+
})
|
|
103
|
+
.strict(),
|
|
104
|
+
)
|
|
105
|
+
.max(128)
|
|
106
|
+
.optional(),
|
|
107
|
+
})
|
|
108
|
+
.strict()
|
|
109
|
+
.optional(),
|
|
110
|
+
})
|
|
111
|
+
.strict()
|
|
112
|
+
|
|
113
|
+
export const GatesConfigSchema = z
|
|
114
|
+
.object({
|
|
115
|
+
/** minimal: freshness · standard: + human links · strict: + okf-type · playbook: freshness + okf soft style */
|
|
116
|
+
preset: z.enum(['minimal', 'standard', 'strict', 'playbook']).optional(),
|
|
117
|
+
include: z
|
|
118
|
+
.array(
|
|
119
|
+
z.enum([
|
|
120
|
+
'index-freshness',
|
|
121
|
+
'human-guide-links',
|
|
122
|
+
'link-rot',
|
|
123
|
+
'okf-type',
|
|
124
|
+
'docs-style',
|
|
125
|
+
'routing-currency',
|
|
126
|
+
'bootstrap-size',
|
|
127
|
+
]),
|
|
128
|
+
)
|
|
129
|
+
.max(16)
|
|
130
|
+
.optional(),
|
|
131
|
+
exclude: z
|
|
132
|
+
.array(
|
|
133
|
+
z.enum([
|
|
134
|
+
'index-freshness',
|
|
135
|
+
'human-guide-links',
|
|
136
|
+
'link-rot',
|
|
137
|
+
'okf-type',
|
|
138
|
+
'docs-style',
|
|
139
|
+
'routing-currency',
|
|
140
|
+
'bootstrap-size',
|
|
141
|
+
]),
|
|
142
|
+
)
|
|
143
|
+
.max(16)
|
|
144
|
+
.optional(),
|
|
145
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
146
|
+
})
|
|
147
|
+
.strict()
|
|
148
|
+
|
|
149
|
+
export const SurfacesConfigSchema = z
|
|
150
|
+
.object({
|
|
151
|
+
cli: z
|
|
152
|
+
.object({
|
|
153
|
+
bin: z.string().min(1).max(64).optional(),
|
|
154
|
+
defaultFormat: z.enum(['json', 'text']).optional(),
|
|
155
|
+
})
|
|
156
|
+
.strict()
|
|
157
|
+
.optional(),
|
|
158
|
+
mcp: z
|
|
159
|
+
.object({
|
|
160
|
+
enabled: z.boolean().optional(),
|
|
161
|
+
tools: z
|
|
162
|
+
.array(
|
|
163
|
+
z.enum([
|
|
164
|
+
'handoff.resolve',
|
|
165
|
+
'doc.search',
|
|
166
|
+
'doc.get',
|
|
167
|
+
'gate.status',
|
|
168
|
+
'playbook.pattern.get',
|
|
169
|
+
'retriever.query',
|
|
170
|
+
'memory.classify',
|
|
171
|
+
'memory.promoteDraft',
|
|
172
|
+
'registry.topology',
|
|
173
|
+
]),
|
|
174
|
+
)
|
|
175
|
+
.max(16)
|
|
176
|
+
.optional(),
|
|
177
|
+
transport: z.enum(['stdio', 'http']).optional(),
|
|
178
|
+
http: z
|
|
179
|
+
.object({
|
|
180
|
+
port: z.number().int().min(1).max(65_535).optional(),
|
|
181
|
+
path: z.string().min(1).max(256).optional(),
|
|
182
|
+
})
|
|
183
|
+
.strict()
|
|
184
|
+
.optional(),
|
|
185
|
+
})
|
|
186
|
+
.strict()
|
|
187
|
+
.optional(),
|
|
188
|
+
})
|
|
189
|
+
.strict()
|
|
190
|
+
|
|
191
|
+
export const IntelligenceConfigSchema = z
|
|
192
|
+
.object({
|
|
193
|
+
enabled: z.boolean().optional(),
|
|
194
|
+
adapter: z
|
|
195
|
+
.object({
|
|
196
|
+
provider: z.enum(['openai', 'anthropic', 'ollama', 'openrouter', 'custom']),
|
|
197
|
+
model: z.string().min(1).max(128).optional(),
|
|
198
|
+
apiKeyEnv: z.string().min(0).max(128).optional(),
|
|
199
|
+
baseUrl: z.string().url().optional(),
|
|
200
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
201
|
+
})
|
|
202
|
+
.strict()
|
|
203
|
+
.optional(),
|
|
204
|
+
chat: z
|
|
205
|
+
.object({
|
|
206
|
+
enabled: z.boolean().optional(),
|
|
207
|
+
sources: z.array(z.enum(['agent', 'human', 'federation'])).max(8).optional(),
|
|
208
|
+
handoffFirst: z.boolean().optional(),
|
|
209
|
+
})
|
|
210
|
+
.strict()
|
|
211
|
+
.optional(),
|
|
212
|
+
retriever: z
|
|
213
|
+
.object({
|
|
214
|
+
enabled: z.boolean().optional(),
|
|
215
|
+
mode: z.enum(['local', 'remote', 'bm25', 'agentskit-rag']).optional(),
|
|
216
|
+
embedModel: z.string().min(1).max(128).optional(),
|
|
217
|
+
chunkSize: z.number().int().min(128).max(16_384).optional(),
|
|
218
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
219
|
+
})
|
|
220
|
+
.strict()
|
|
221
|
+
.optional(),
|
|
222
|
+
memory: z
|
|
223
|
+
.object({
|
|
224
|
+
enabled: z.boolean().optional(),
|
|
225
|
+
adapters: z
|
|
226
|
+
.array(z.enum(['playbook-memory', 'cursor-rules', 'session-export', 'bootstrap-delta']))
|
|
227
|
+
.max(8)
|
|
228
|
+
.optional(),
|
|
229
|
+
ingestDir: z.string().min(1).max(512).optional(),
|
|
230
|
+
classify: z.boolean().optional(),
|
|
231
|
+
promote: z
|
|
232
|
+
.object({
|
|
233
|
+
enabled: z.boolean().optional(),
|
|
234
|
+
targets: z.array(z.enum(['agent', 'human', 'agents-md'])).max(8).optional(),
|
|
235
|
+
requireApproval: z.boolean().optional(),
|
|
236
|
+
})
|
|
237
|
+
.strict()
|
|
238
|
+
.optional(),
|
|
239
|
+
})
|
|
240
|
+
.strict()
|
|
241
|
+
.optional(),
|
|
242
|
+
runtime: z.enum(['agentskit', 'custom']).optional(),
|
|
243
|
+
runtimeModule: z.string().min(1).max(512).optional(),
|
|
244
|
+
})
|
|
245
|
+
.strict()
|
|
246
|
+
|
|
247
|
+
export const FederationConfigSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
enabled: z.boolean().optional(),
|
|
250
|
+
sources: z
|
|
251
|
+
.array(
|
|
252
|
+
z
|
|
253
|
+
.object({
|
|
254
|
+
id: z.string().min(1).max(64),
|
|
255
|
+
llmsTxt: z.string().min(1).max(512).optional(),
|
|
256
|
+
rawBaseUrl: z.string().url().optional(),
|
|
257
|
+
includeInRetriever: z.boolean().optional(),
|
|
258
|
+
includeInChat: z.boolean().optional(),
|
|
259
|
+
})
|
|
260
|
+
.strict(),
|
|
261
|
+
)
|
|
262
|
+
.max(16)
|
|
263
|
+
.optional(),
|
|
264
|
+
})
|
|
265
|
+
.strict()
|
|
266
|
+
|
|
267
|
+
export const DocBridgeConfigV1Schema = z
|
|
268
|
+
.object({
|
|
269
|
+
schemaVersion: z.literal(CONFIG_SCHEMA_VERSION),
|
|
270
|
+
project: z
|
|
271
|
+
.object({
|
|
272
|
+
name: z.string().min(1).max(128).optional(),
|
|
273
|
+
root: z.string().min(1).max(512).optional(),
|
|
274
|
+
})
|
|
275
|
+
.strict()
|
|
276
|
+
.optional(),
|
|
277
|
+
corpus: z
|
|
278
|
+
.object({
|
|
279
|
+
agent: AgentCorpusConfigSchema,
|
|
280
|
+
human: z.union([HumanCorpusConfigSchema, z.array(HumanCorpusConfigSchema).max(8)]).optional(),
|
|
281
|
+
})
|
|
282
|
+
.strict(),
|
|
283
|
+
index: IndexConfigSchema.optional(),
|
|
284
|
+
routing: RoutingConfigSchema.optional(),
|
|
285
|
+
gates: GatesConfigSchema.optional(),
|
|
286
|
+
surfaces: SurfacesConfigSchema.optional(),
|
|
287
|
+
intelligence: IntelligenceConfigSchema.optional(),
|
|
288
|
+
federation: FederationConfigSchema.optional(),
|
|
289
|
+
})
|
|
290
|
+
.strict()
|
|
291
|
+
|
|
292
|
+
export type DocBridgeConfigV1 = z.infer<typeof DocBridgeConfigV1Schema>
|
|
293
|
+
export type AgentCorpusConfig = z.infer<typeof AgentCorpusConfigSchema>
|
|
294
|
+
export type HumanCorpusConfig = z.infer<typeof HumanCorpusConfigSchema>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import { slugFromPath } from '../lib/markdown.js'
|
|
6
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
7
|
+
import {
|
|
8
|
+
retrieveDocBridgeChunks,
|
|
9
|
+
type DocBridgeRetrievedChunk,
|
|
10
|
+
} from '../retriever/doc-bridge-retriever.js'
|
|
11
|
+
|
|
12
|
+
export type FetchText = (url: string) => Promise<string>
|
|
13
|
+
|
|
14
|
+
export type FederatedRetrieverOptions = {
|
|
15
|
+
readonly fetchText?: FetchText
|
|
16
|
+
readonly limit?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const tokenize = (value: string): string[] =>
|
|
20
|
+
value.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 2)
|
|
21
|
+
|
|
22
|
+
const scoreText = (query: string, text: string): number => {
|
|
23
|
+
const hay = text.toLowerCase()
|
|
24
|
+
return tokenize(query).reduce((score, token) => score + (hay.includes(token) ? token.length : 0), 0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultFetchText: FetchText = async (url) => {
|
|
28
|
+
const signal = AbortSignal.timeout(5_000)
|
|
29
|
+
const res = await fetch(url, { signal })
|
|
30
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`)
|
|
31
|
+
return res.text()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sourceText = async (
|
|
35
|
+
root: string,
|
|
36
|
+
source: string,
|
|
37
|
+
fetchText: FetchText,
|
|
38
|
+
): Promise<string | null> => {
|
|
39
|
+
try {
|
|
40
|
+
if (/^https?:\/\//.test(source)) return await fetchText(source)
|
|
41
|
+
const path = resolve(root, source)
|
|
42
|
+
if (!existsSync(path)) return null
|
|
43
|
+
return readFileSync(path, 'utf8')
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sameOrigin = (base: string, target: string): boolean => {
|
|
50
|
+
if (!/^https?:\/\//.test(base) || !/^https?:\/\//.test(target)) return true
|
|
51
|
+
return new URL(base).origin === new URL(target).origin
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const parseLlmsTxtLinks = (raw: string): { title: string; url: string; description?: string }[] => {
|
|
55
|
+
const links = [...raw.matchAll(/\[([^\]]+)\]\(([^)]+)\)(?::\s*([^\n]+))?/g)].map((match) => ({
|
|
56
|
+
title: match[1] ?? match[2] ?? 'link',
|
|
57
|
+
url: match[2] ?? '',
|
|
58
|
+
...(match[3]?.trim() ? { description: match[3].trim() } : {}),
|
|
59
|
+
})).filter((link) => link.url)
|
|
60
|
+
|
|
61
|
+
for (const match of raw.matchAll(/(?:^|\s)(?:Raw|llms\.txt|Full bundle|ZIP bundle)?:?\s*(https?:\/\/\S+)/gi)) {
|
|
62
|
+
const url = match[1]?.replace(/[),.;]+$/, '')
|
|
63
|
+
if (url && !links.some((link) => link.url === url)) links.push({ title: slugFromPath(url), url })
|
|
64
|
+
}
|
|
65
|
+
return links
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const chunksFromMarkdown = (
|
|
69
|
+
property: string,
|
|
70
|
+
raw: string,
|
|
71
|
+
sourceUrl: string,
|
|
72
|
+
): DocBridgeRetrievedChunk[] => {
|
|
73
|
+
const searchable = raw.includes('\n==== ') ? raw : raw.replace(/^---\n[\s\S]*?\n---\n?/, '')
|
|
74
|
+
const sections = searchable.includes('\n==== ')
|
|
75
|
+
? searchable.split(/\n====\s+/).filter((section) => section.trim())
|
|
76
|
+
: searchable.split(/\n(?=##?\s+)/)
|
|
77
|
+
return sections.map((section, index) => {
|
|
78
|
+
const title =
|
|
79
|
+
/^title:\s*(.+)$/m.exec(section)?.[1]?.trim() ??
|
|
80
|
+
/^https?:\/\/\S+\/([^/\s]+)$/m.exec(section)?.[1]?.trim() ??
|
|
81
|
+
/^#+\s+(.+)$/m.exec(section)?.[1]?.trim() ??
|
|
82
|
+
slugFromPath(sourceUrl)
|
|
83
|
+
const id = slugFromPath(title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) || `${index}`
|
|
84
|
+
return {
|
|
85
|
+
chunkKey: `${property}:federated:${id}`,
|
|
86
|
+
property,
|
|
87
|
+
type: 'federated',
|
|
88
|
+
id,
|
|
89
|
+
path: sourceUrl,
|
|
90
|
+
title,
|
|
91
|
+
summary: section.trim().slice(0, 500),
|
|
92
|
+
score: 0,
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const loadFederatedChunks = async (
|
|
98
|
+
root: string,
|
|
99
|
+
config: DocBridgeConfigV1,
|
|
100
|
+
options: FederatedRetrieverOptions = {},
|
|
101
|
+
): Promise<DocBridgeRetrievedChunk[]> => {
|
|
102
|
+
const fetchText = options.fetchText ?? defaultFetchText
|
|
103
|
+
const chunks: DocBridgeRetrievedChunk[] = []
|
|
104
|
+
const warnings: string[] = []
|
|
105
|
+
for (const source of config.federation?.sources ?? []) {
|
|
106
|
+
if (source.includeInRetriever === false || !source.llmsTxt) continue
|
|
107
|
+
const llms = await sourceText(root, source.llmsTxt, fetchText)
|
|
108
|
+
if (!llms) {
|
|
109
|
+
warnings.push(`federation source skipped (unavailable): ${source.id} → ${source.llmsTxt}`)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
chunks.push(...chunksFromMarkdown(source.id, llms, source.llmsTxt))
|
|
113
|
+
const links = parseLlmsTxtLinks(llms)
|
|
114
|
+
for (const link of links) {
|
|
115
|
+
const url = !/^https?:\/\//.test(link.url) && source.rawBaseUrl
|
|
116
|
+
? `${source.rawBaseUrl.replace(/\/$/, '')}/${link.url.replace(/^\//, '')}`
|
|
117
|
+
: link.url
|
|
118
|
+
if (!/\.(md|txt)(?:$|\?)/.test(url)) continue
|
|
119
|
+
if (!sameOrigin(source.llmsTxt, url)) continue
|
|
120
|
+
const raw = await sourceText(root, url, fetchText)
|
|
121
|
+
if (raw) chunks.push(...chunksFromMarkdown(source.id, raw, url))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Soft-fail: never throw for missing remote sources; one-line warn for agents/humans.
|
|
125
|
+
if (warnings.length && process.stderr.isTTY) {
|
|
126
|
+
for (const w of warnings) process.stderr.write(`${w}\n`)
|
|
127
|
+
}
|
|
128
|
+
return chunks
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const retrieveHybridChunks = async (
|
|
132
|
+
root: string,
|
|
133
|
+
config: DocBridgeConfigV1,
|
|
134
|
+
index: DocBridgeIndexV1,
|
|
135
|
+
query: string,
|
|
136
|
+
options: FederatedRetrieverOptions = {},
|
|
137
|
+
): Promise<DocBridgeRetrievedChunk[]> => {
|
|
138
|
+
const limit = options.limit ?? 8
|
|
139
|
+
const local = retrieveDocBridgeChunks(index, query, {
|
|
140
|
+
property: config.project?.name ?? index.project?.name ?? 'local',
|
|
141
|
+
limit,
|
|
142
|
+
})
|
|
143
|
+
const federated = (await loadFederatedChunks(root, config, options)).map((chunk) => ({
|
|
144
|
+
...chunk,
|
|
145
|
+
score: scoreText(query, `${chunk.title ?? ''} ${chunk.summary ?? ''} ${chunk.path}`),
|
|
146
|
+
})).filter((chunk) => chunk.score > 0)
|
|
147
|
+
|
|
148
|
+
const byKey = new Map<string, DocBridgeRetrievedChunk>()
|
|
149
|
+
for (const chunk of [...local, ...federated]) {
|
|
150
|
+
const existing = byKey.get(chunk.chunkKey)
|
|
151
|
+
if (!existing || chunk.score > existing.score) byKey.set(chunk.chunkKey, chunk)
|
|
152
|
+
}
|
|
153
|
+
return [...byKey.values()].sort((a, b) => b.score - a.score).slice(0, limit)
|
|
154
|
+
}
|