@agentskit/doc-bridge 0.1.0-alpha.1 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/cli/program.js +283 -105
- 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 +5 -4
- package/dist/index.js +278 -100
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD.md +92 -0
- package/package.json +27 -9
- package/scripts/prepare.mjs +44 -0
- package/src/cli/program.ts +933 -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 +145 -0
- package/src/gates/run-gates.ts +274 -0
- package/src/index-builder/build-handoffs.ts +217 -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 +181 -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 +90 -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 +58 -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 +106 -0
- package/src/schemas/json-schemas.ts +156 -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,100 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { HumanCorpusConfig } from '../../config/schema.js'
|
|
5
|
+
import { slugFromPath } from '../../lib/markdown.js'
|
|
6
|
+
import { toPosix } from '../../lib/paths.js'
|
|
7
|
+
import { walkFiles } from '../../lib/walk.js'
|
|
8
|
+
|
|
9
|
+
export type HumanDocRecord = {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly url: string
|
|
12
|
+
readonly path: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type HumanDocMap = Record<string, string>
|
|
16
|
+
|
|
17
|
+
export type HumanAdapterContext = {
|
|
18
|
+
readonly root: string
|
|
19
|
+
readonly config: HumanCorpusConfig
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type HumanAdapter = {
|
|
23
|
+
readonly plugin: HumanCorpusConfig['plugin']
|
|
24
|
+
readonly scan: (ctx: HumanAdapterContext) => HumanDocRecord[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const optionString = (
|
|
28
|
+
options: Record<string, unknown> | undefined,
|
|
29
|
+
keys: readonly string[],
|
|
30
|
+
): string | undefined => {
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
const value = options?.[key]
|
|
33
|
+
if (typeof value === 'string' && value) return value
|
|
34
|
+
}
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const parseFrontmatter = (raw: string): Record<string, string> => {
|
|
39
|
+
if (!raw.startsWith('---\n')) return {}
|
|
40
|
+
const end = raw.indexOf('\n---', 4)
|
|
41
|
+
if (end === -1) return {}
|
|
42
|
+
|
|
43
|
+
const out: Record<string, string> = {}
|
|
44
|
+
for (const line of raw.slice(4, end).split('\n')) {
|
|
45
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.+?)\s*$/.exec(line)
|
|
46
|
+
if (match?.[1] && match[2]) out[match[1]] = match[2].replace(/^['"]|['"]$/g, '')
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const docId = (relToHumanRoot: string, raw: string): string => {
|
|
52
|
+
const meta = parseFrontmatter(raw)
|
|
53
|
+
return meta.package ?? meta.module ?? meta.id ?? slugFromPath(relToHumanRoot)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const routeSlug = (relToHumanRoot: string, opts?: { stripGroups?: boolean }): string =>
|
|
57
|
+
relToHumanRoot
|
|
58
|
+
.split('/')
|
|
59
|
+
.filter((part) => !opts?.stripGroups || !/^\(.+\)$/.test(part))
|
|
60
|
+
.join('/')
|
|
61
|
+
.replace(/(?:^|\/)index\.mdx?$/, '')
|
|
62
|
+
.replace(/\.mdx?$/, '')
|
|
63
|
+
|
|
64
|
+
export const humanUrl = (slug: string, urlPrefix: unknown): string => {
|
|
65
|
+
if (typeof urlPrefix !== 'string' || !urlPrefix) return slug
|
|
66
|
+
const prefix = urlPrefix.replace(/\/$/, '')
|
|
67
|
+
return slug ? `${prefix}/${slug.replace(/^\//, '')}` : prefix
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const scanMarkdownDocs = (
|
|
71
|
+
root: string,
|
|
72
|
+
humanRoot: string,
|
|
73
|
+
options?: {
|
|
74
|
+
readonly includeRelPath?: (relToHumanRoot: string, raw: string) => boolean
|
|
75
|
+
readonly idForDoc?: (relToHumanRoot: string, raw: string) => string
|
|
76
|
+
readonly slugForDoc?: (relToHumanRoot: string, raw: string) => string
|
|
77
|
+
readonly urlPrefix?: unknown
|
|
78
|
+
readonly stripGroups?: boolean
|
|
79
|
+
},
|
|
80
|
+
): HumanDocRecord[] => {
|
|
81
|
+
const out: HumanDocRecord[] = []
|
|
82
|
+
const absRoot = join(root, humanRoot)
|
|
83
|
+
|
|
84
|
+
for (const abs of walkFiles(absRoot, { extensions: ['.md', '.mdx'] })) {
|
|
85
|
+
const relToHumanRoot = toPosix(abs.replace(`${toPosix(absRoot)}/`, ''))
|
|
86
|
+
const raw = readFileSync(abs, 'utf8')
|
|
87
|
+
if (options?.includeRelPath && !options.includeRelPath(relToHumanRoot, raw)) continue
|
|
88
|
+
out.push({
|
|
89
|
+
id: options?.idForDoc?.(relToHumanRoot, raw) ?? docId(relToHumanRoot, raw),
|
|
90
|
+
url: humanUrl(
|
|
91
|
+
options?.slugForDoc?.(relToHumanRoot, raw) ??
|
|
92
|
+
routeSlug(relToHumanRoot, options?.stripGroups ? { stripGroups: true } : undefined),
|
|
93
|
+
options?.urlPrefix,
|
|
94
|
+
),
|
|
95
|
+
path: abs,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
optionString,
|
|
7
|
+
parseFrontmatter,
|
|
8
|
+
routeSlug,
|
|
9
|
+
scanMarkdownDocs,
|
|
10
|
+
type HumanAdapter,
|
|
11
|
+
} from './core.js'
|
|
12
|
+
|
|
13
|
+
type SidebarDocFilter = {
|
|
14
|
+
readonly ids: ReadonlySet<string>
|
|
15
|
+
readonly autogenDirs: readonly string[]
|
|
16
|
+
readonly enabled: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const docusaurusFileSlug = (relPath: string): string => {
|
|
20
|
+
const parts = relPath.split('/')
|
|
21
|
+
const file = parts.at(-1) ?? ''
|
|
22
|
+
const base = file.replace(/\.mdx?$/, '')
|
|
23
|
+
const parent = parts.at(-2)
|
|
24
|
+
if (/^(index|readme)$/i.test(base) || base === parent) return parts.slice(0, -1).join('/')
|
|
25
|
+
return routeSlug(relPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const docusaurusSlug = (relPath: string, raw: string): string => {
|
|
29
|
+
const slug = parseFrontmatter(raw).slug
|
|
30
|
+
if (!slug) return docusaurusFileSlug(relPath)
|
|
31
|
+
return slug.replace(/^\.\//, '').replace(/^\//, '').replace(/\/$/, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const docusaurusSidebarId = (relPath: string, raw: string): string => {
|
|
35
|
+
const id = parseFrontmatter(raw).id
|
|
36
|
+
if (!id) return routeSlug(relPath)
|
|
37
|
+
const parts = relPath.split('/').slice(0, -1)
|
|
38
|
+
return [...parts, id].filter(Boolean).join('/')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const docusaurusRecordId = (relPath: string, raw: string): string => {
|
|
42
|
+
const frontmatter = parseFrontmatter(raw)
|
|
43
|
+
return frontmatter.package ?? frontmatter.module ?? docusaurusSidebarId(relPath, raw)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sidebarsValue = (file: string): unknown => {
|
|
47
|
+
if (!existsSync(file)) return undefined
|
|
48
|
+
const raw = readFileSync(file, 'utf8')
|
|
49
|
+
.replace(/import\s+type\s+[\s\S]*?;?\n/g, '')
|
|
50
|
+
.replace(/:\s*[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*=)/g, '')
|
|
51
|
+
.replace(/\s+satisfies\s+[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*(?:;|\n|$))/g, '')
|
|
52
|
+
.replace(/export\s+default/, 'module.exports =')
|
|
53
|
+
const sandbox = { module: { exports: {} as unknown }, exports: {} }
|
|
54
|
+
vm.runInNewContext(raw, sandbox, { timeout: 250 })
|
|
55
|
+
return sandbox.module.exports
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const visitSidebar = (value: unknown, filter: { ids: Set<string>; autogenDirs: string[] }): void => {
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
filter.ids.add(value)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
for (const item of value) visitSidebar(item, filter)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (!value || typeof value !== 'object') return
|
|
68
|
+
|
|
69
|
+
const item = value as Record<string, unknown>
|
|
70
|
+
if ((item.type === 'doc' || item.type === 'ref') && typeof item.id === 'string') {
|
|
71
|
+
filter.ids.add(item.id)
|
|
72
|
+
}
|
|
73
|
+
if (item.type === 'autogenerated' && typeof item.dirName === 'string') {
|
|
74
|
+
filter.autogenDirs.push(item.dirName)
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(item.items)) visitSidebar(item.items, filter)
|
|
77
|
+
if (item.link && typeof item.link === 'object') visitSidebar(item.link, filter)
|
|
78
|
+
for (const child of Object.values(item)) {
|
|
79
|
+
if (Array.isArray(child)) visitSidebar(child, filter)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const readSidebars = (root: string, sidebarsFile: string | undefined): SidebarDocFilter => {
|
|
84
|
+
if (!sidebarsFile) return { enabled: false, ids: new Set(), autogenDirs: [] }
|
|
85
|
+
const value = sidebarsValue(join(root, sidebarsFile))
|
|
86
|
+
const filter = { ids: new Set<string>(), autogenDirs: [] as string[] }
|
|
87
|
+
visitSidebar(value, filter)
|
|
88
|
+
return { enabled: true, ...filter }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isIncludedBySidebar = (filter: SidebarDocFilter, id: string): boolean => {
|
|
92
|
+
if (!filter.enabled) return true
|
|
93
|
+
if (filter.ids.has(id)) return true
|
|
94
|
+
return filter.autogenDirs.some((dir) => {
|
|
95
|
+
const prefix = dir === '.' ? '' : dir.replace(/\/$/, '')
|
|
96
|
+
return prefix ? id === prefix || id.startsWith(`${prefix}/`) : true
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const docusaurusAdapter: HumanAdapter = {
|
|
101
|
+
plugin: 'docusaurus',
|
|
102
|
+
scan: ({ root, config }) => {
|
|
103
|
+
const docsDir = optionString(config.options, ['docsDir', 'root'])
|
|
104
|
+
if (!docsDir) return []
|
|
105
|
+
const sidebarFilter = readSidebars(root, optionString(config.options, ['sidebarsFile']))
|
|
106
|
+
return scanMarkdownDocs(root, docsDir, {
|
|
107
|
+
includeRelPath: (relPath, raw) => isIncludedBySidebar(sidebarFilter, docusaurusSidebarId(relPath, raw)),
|
|
108
|
+
idForDoc: docusaurusRecordId,
|
|
109
|
+
slugForDoc: docusaurusSlug,
|
|
110
|
+
urlPrefix: config.options?.urlPrefix,
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { optionString, scanMarkdownDocs, type HumanAdapter } from './core.js'
|
|
5
|
+
|
|
6
|
+
type FumadocsMeta = {
|
|
7
|
+
readonly pages?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const readMetaPages = (dir: string): string[] | undefined => {
|
|
11
|
+
const file = join(dir, 'meta.json')
|
|
12
|
+
if (!existsSync(file)) return undefined
|
|
13
|
+
try {
|
|
14
|
+
const meta = JSON.parse(readFileSync(file, 'utf8')) as FumadocsMeta
|
|
15
|
+
return Array.isArray(meta.pages) ? meta.pages.filter((page): page is string => typeof page === 'string') : undefined
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pageKey = (part: string): string => part.replace(/\.mdx?$/, '')
|
|
22
|
+
|
|
23
|
+
const isDotFile = (relPath: string): boolean => {
|
|
24
|
+
const file = relPath.split('/').at(-1) ?? ''
|
|
25
|
+
return file.startsWith('.')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isListedByMeta = (contentRoot: string, relPath: string): boolean => {
|
|
29
|
+
if (isDotFile(relPath)) return false
|
|
30
|
+
|
|
31
|
+
const parts = relPath.split('/')
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
34
|
+
const dir = join(contentRoot, ...parts.slice(0, i))
|
|
35
|
+
const pages = readMetaPages(dir)
|
|
36
|
+
if (!pages?.length || pages.includes('...')) continue
|
|
37
|
+
|
|
38
|
+
const key = pageKey(parts[i] ?? '')
|
|
39
|
+
if (!pages.includes(key) && !pages.includes(`./${key}`)) return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const fumadocsAdapter: HumanAdapter = {
|
|
46
|
+
plugin: 'fumadocs',
|
|
47
|
+
scan: ({ root, config }) => {
|
|
48
|
+
const contentDir = optionString(config.options, ['contentDir', 'root'])
|
|
49
|
+
if (!contentDir) return []
|
|
50
|
+
const contentRoot = join(root, contentDir)
|
|
51
|
+
const excludePrefixes = Array.isArray(config.options?.excludePrefixes)
|
|
52
|
+
? config.options.excludePrefixes.filter((v): v is string => typeof v === 'string')
|
|
53
|
+
: typeof config.options?.excludePrefix === 'string'
|
|
54
|
+
? [config.options.excludePrefix]
|
|
55
|
+
: []
|
|
56
|
+
// Nested agent corpora under Fumadocs (e.g. content/docs/for-agents) should not be human docs
|
|
57
|
+
if (!excludePrefixes.includes('for-agents')) excludePrefixes.push('for-agents')
|
|
58
|
+
|
|
59
|
+
return scanMarkdownDocs(root, contentDir, {
|
|
60
|
+
includeRelPath: (relPath) => {
|
|
61
|
+
if (excludePrefixes.some((prefix) => relPath === prefix || relPath.startsWith(`${prefix}/`))) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
return isListedByMeta(contentRoot, relPath)
|
|
65
|
+
},
|
|
66
|
+
urlPrefix: config.options?.urlPrefix,
|
|
67
|
+
stripGroups: true,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1, HumanCorpusConfig } from '../../config/schema.js'
|
|
2
|
+
import { docusaurusAdapter } from './docusaurus.js'
|
|
3
|
+
import { fumadocsAdapter } from './fumadocs.js'
|
|
4
|
+
import type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
|
|
5
|
+
import { plainMarkdownAdapter } from './plain-markdown.js'
|
|
6
|
+
|
|
7
|
+
export type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
|
|
8
|
+
|
|
9
|
+
const ADAPTERS: readonly HumanAdapter[] = [
|
|
10
|
+
plainMarkdownAdapter,
|
|
11
|
+
fumadocsAdapter,
|
|
12
|
+
docusaurusAdapter,
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const humanConfigs = (config: DocBridgeConfigV1): HumanCorpusConfig[] => {
|
|
16
|
+
const human = config.corpus.human
|
|
17
|
+
if (!human) return []
|
|
18
|
+
return Array.isArray(human) ? human : [human]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const scanHumanDocRecords = (
|
|
22
|
+
root: string,
|
|
23
|
+
config: DocBridgeConfigV1,
|
|
24
|
+
): HumanDocRecord[] => {
|
|
25
|
+
const out: HumanDocRecord[] = []
|
|
26
|
+
const seen = new Set<string>()
|
|
27
|
+
const agentRoot = config.corpus.agent.root.replace(/\/$/, '')
|
|
28
|
+
|
|
29
|
+
for (const human of humanConfigs(config)) {
|
|
30
|
+
const adapter = ADAPTERS.find((candidate) => candidate.plugin === human.plugin)
|
|
31
|
+
if (!adapter) continue
|
|
32
|
+
for (const record of adapter.scan({ root, config: human })) {
|
|
33
|
+
// Never treat agent-corpus files as human docs (nested for-agents, etc.)
|
|
34
|
+
if (
|
|
35
|
+
record.path === agentRoot ||
|
|
36
|
+
record.path.startsWith(`${agentRoot}/`) ||
|
|
37
|
+
record.path.includes('/for-agents/') ||
|
|
38
|
+
record.path.endsWith('/for-agents')
|
|
39
|
+
) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (seen.has(record.id)) continue
|
|
43
|
+
seen.add(record.id)
|
|
44
|
+
out.push(record)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const scanHumanDocs = (root: string, config: DocBridgeConfigV1): HumanDocMap =>
|
|
52
|
+
Object.fromEntries(scanHumanDocRecords(root, config).map((doc) => [doc.id, doc.url]))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { optionString, scanMarkdownDocs, type HumanAdapter } from './core.js'
|
|
2
|
+
|
|
3
|
+
export const plainMarkdownAdapter: HumanAdapter = {
|
|
4
|
+
plugin: 'plain-markdown',
|
|
5
|
+
scan: ({ root, config }) => {
|
|
6
|
+
const humanRoot =
|
|
7
|
+
optionString(config.options, ['contentDir', 'root', 'docsDir']) ?? 'docs'
|
|
8
|
+
return scanMarkdownDocs(root, humanRoot, { urlPrefix: config.options?.urlPrefix })
|
|
9
|
+
},
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
|
|
3
|
+
|
|
4
|
+
export const renderLlmsTxt = (
|
|
5
|
+
config: DocBridgeConfigV1,
|
|
6
|
+
knowledge: readonly KnowledgeEntry[],
|
|
7
|
+
projectName: string,
|
|
8
|
+
): string => {
|
|
9
|
+
const preamble =
|
|
10
|
+
config.index?.llmsTxt?.preamble ??
|
|
11
|
+
`# ${projectName}\n\n> Agent-readable documentation index generated by ak-docs (@agentskit/doc-bridge).\n`
|
|
12
|
+
|
|
13
|
+
const lines = knowledge.slice(0, 500).map((entry) => {
|
|
14
|
+
const desc = entry.description ? `: ${entry.description}` : ''
|
|
15
|
+
return `- [${entry.title}](${entry.path})${desc}`
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return `${preamble.trim()}\n\n## Knowledge\n\n${lines.join('\n')}\n`
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../../config/schema.js'
|
|
5
|
+
import { expandWorkspaceGlobs } from '../../lib/glob-expand.js'
|
|
6
|
+
import { toPosix } from '../../lib/paths.js'
|
|
7
|
+
|
|
8
|
+
export type DiscoveredPackage = {
|
|
9
|
+
readonly id: string
|
|
10
|
+
readonly path: string
|
|
11
|
+
readonly name?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parsePnpmWorkspace = (yaml: string): string[] => {
|
|
15
|
+
const lines = yaml.split('\n')
|
|
16
|
+
const patterns: string[] = []
|
|
17
|
+
let inPackages = false
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const trimmed = line.trim()
|
|
20
|
+
if (trimmed === 'packages:') {
|
|
21
|
+
inPackages = true
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
if (inPackages) {
|
|
25
|
+
if (trimmed.startsWith('- ')) {
|
|
26
|
+
patterns.push(trimmed.slice(2).trim().replace(/^['"]|['"]$/g, ''))
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (trimmed && !trimmed.startsWith('#')) inPackages = false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return patterns
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const readPackageJson = (dir: string): { name?: string } | null => {
|
|
36
|
+
const file = join(dir, 'package.json')
|
|
37
|
+
if (!existsSync(file)) return null
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(file, 'utf8')) as { name?: string }
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const discoverPnpmPackages = (
|
|
46
|
+
root: string,
|
|
47
|
+
config: DocBridgeConfigV1,
|
|
48
|
+
): DiscoveredPackage[] => {
|
|
49
|
+
const explicit = config.routing?.options?.packages
|
|
50
|
+
let patterns = explicit
|
|
51
|
+
if (!patterns?.length) {
|
|
52
|
+
const workspaceFile = join(root, 'pnpm-workspace.yaml')
|
|
53
|
+
if (existsSync(workspaceFile)) {
|
|
54
|
+
patterns = parsePnpmWorkspace(readFileSync(workspaceFile, 'utf8'))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!patterns?.length) return []
|
|
58
|
+
|
|
59
|
+
const dirs = expandWorkspaceGlobs(root, patterns)
|
|
60
|
+
const out: DiscoveredPackage[] = []
|
|
61
|
+
|
|
62
|
+
for (const abs of dirs) {
|
|
63
|
+
const pkg = readPackageJson(abs)
|
|
64
|
+
if (!pkg) continue
|
|
65
|
+
const rel = toPosix(abs.replace(`${toPosix(root)}/`, ''))
|
|
66
|
+
const folderId = basename(abs)
|
|
67
|
+
const id = pkg.name?.startsWith('@') ? pkg.name.split('/').pop() ?? folderId : (pkg.name ?? folderId)
|
|
68
|
+
out.push({ id, path: rel, ...(pkg.name ? { name: pkg.name } : {}) })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out.sort((a, b) => a.id.localeCompare(b.id))
|
|
72
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import {
|
|
6
|
+
firstHeading,
|
|
7
|
+
firstParagraph,
|
|
8
|
+
frontmatterString,
|
|
9
|
+
frontmatterStringList,
|
|
10
|
+
parseFrontmatter,
|
|
11
|
+
slugFromPath,
|
|
12
|
+
type FrontmatterData,
|
|
13
|
+
} from '../lib/markdown.js'
|
|
14
|
+
import { toPosix } from '../lib/paths.js'
|
|
15
|
+
import { walkFiles } from '../lib/walk.js'
|
|
16
|
+
import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
|
|
17
|
+
|
|
18
|
+
export type CorpusDoc = KnowledgeEntry & {
|
|
19
|
+
readonly absPath: string
|
|
20
|
+
readonly relPath: string
|
|
21
|
+
readonly frontmatter: FrontmatterData
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type OwnershipSeed = {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly path: string
|
|
27
|
+
readonly purpose?: string
|
|
28
|
+
readonly checks?: readonly string[]
|
|
29
|
+
readonly agentDoc: string
|
|
30
|
+
readonly humanDoc?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const relFromRoot = (root: string, abs: string): string => toPosix(abs.replace(`${toPosix(root)}/`, ''))
|
|
34
|
+
|
|
35
|
+
export const scanAgentCorpus = (root: string, config: DocBridgeConfigV1): CorpusDoc[] => {
|
|
36
|
+
const agentRoot = join(root, config.corpus.agent.root)
|
|
37
|
+
const files = walkFiles(agentRoot, { extensions: ['.md', '.mdx'] })
|
|
38
|
+
const corpusRelRoot = toPosix(config.corpus.agent.root)
|
|
39
|
+
|
|
40
|
+
return files.map((abs) => {
|
|
41
|
+
const relToCorpus = toPosix(abs.replace(`${toPosix(agentRoot)}/`, ''))
|
|
42
|
+
const relPath = `${corpusRelRoot}/${relToCorpus}`
|
|
43
|
+
const raw = readFileSync(abs, 'utf8')
|
|
44
|
+
const { data: frontmatter } = parseFrontmatter(raw)
|
|
45
|
+
const id =
|
|
46
|
+
frontmatterString(frontmatter, 'id') ??
|
|
47
|
+
frontmatterString(frontmatter, 'package') ??
|
|
48
|
+
slugFromPath(relToCorpus)
|
|
49
|
+
const title = firstHeading(raw) ?? id
|
|
50
|
+
const description = firstParagraph(raw)
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
type: 'agent-doc',
|
|
54
|
+
title,
|
|
55
|
+
path: relPath,
|
|
56
|
+
absPath: abs,
|
|
57
|
+
relPath,
|
|
58
|
+
frontmatter,
|
|
59
|
+
...(description ? { description } : {}),
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const guessAgentDocForPackage = (
|
|
65
|
+
corpus: readonly CorpusDoc[],
|
|
66
|
+
packageId: string,
|
|
67
|
+
): string | undefined => {
|
|
68
|
+
const candidates = [
|
|
69
|
+
(doc: CorpusDoc) => frontmatterString(doc.frontmatter, 'package') === packageId,
|
|
70
|
+
(doc: CorpusDoc) => doc.id === packageId,
|
|
71
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}.md`),
|
|
72
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}.mdx`),
|
|
73
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}/index.md`),
|
|
74
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}/index.mdx`),
|
|
75
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/${packageId}.md`),
|
|
76
|
+
(doc: CorpusDoc) => doc.relPath.endsWith(`/${packageId}.mdx`),
|
|
77
|
+
(doc: CorpusDoc) => doc.relPath.includes(`/packages/${packageId}/`),
|
|
78
|
+
]
|
|
79
|
+
for (const match of candidates) {
|
|
80
|
+
const hit = corpus.find(match)
|
|
81
|
+
if (hit) return hit.path
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Ownership seeds from agent-doc frontmatter (`package` + `editRoot`). */
|
|
87
|
+
export const ownershipFromFrontmatter = (corpus: readonly CorpusDoc[]): readonly OwnershipSeed[] => {
|
|
88
|
+
const out: OwnershipSeed[] = []
|
|
89
|
+
|
|
90
|
+
for (const doc of corpus) {
|
|
91
|
+
const type = frontmatterString(doc.frontmatter, 'type')
|
|
92
|
+
const id =
|
|
93
|
+
frontmatterString(doc.frontmatter, 'package') ??
|
|
94
|
+
(type === 'package' || type === 'module' || type === 'pattern'
|
|
95
|
+
? frontmatterString(doc.frontmatter, 'id') ?? doc.id
|
|
96
|
+
: undefined)
|
|
97
|
+
const path =
|
|
98
|
+
frontmatterString(doc.frontmatter, 'editRoot') ??
|
|
99
|
+
frontmatterString(doc.frontmatter, 'path') ??
|
|
100
|
+
(type === 'package' || type === 'module' ? `packages/${id}` : undefined)
|
|
101
|
+
if (!id || !path) continue
|
|
102
|
+
const purpose = frontmatterString(doc.frontmatter, 'purpose') ?? doc.description
|
|
103
|
+
const checks = frontmatterStringList(doc.frontmatter, 'checks')
|
|
104
|
+
const humanDoc = frontmatterString(doc.frontmatter, 'humanDoc')
|
|
105
|
+
out.push({
|
|
106
|
+
id,
|
|
107
|
+
path,
|
|
108
|
+
agentDoc: doc.path,
|
|
109
|
+
...(purpose ? { purpose } : {}),
|
|
110
|
+
...(checks ? { checks } : {}),
|
|
111
|
+
...(humanDoc ? { humanDoc } : {}),
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Path-based ownership when frontmatter is incomplete:
|
|
120
|
+
* - packages/id.md(x) → package path packages/id
|
|
121
|
+
* - pillars/.../name-pattern.md → pattern ownership (edit root = file)
|
|
122
|
+
* - registry/id/README.md → registry agent
|
|
123
|
+
*/
|
|
124
|
+
export const ownershipFromCorpus = (corpus: readonly CorpusDoc[]): readonly OwnershipSeed[] => {
|
|
125
|
+
const out: OwnershipSeed[] = []
|
|
126
|
+
const seen = new Set<string>()
|
|
127
|
+
|
|
128
|
+
for (const doc of corpus) {
|
|
129
|
+
// already handled if package+editRoot present
|
|
130
|
+
if (frontmatterString(doc.frontmatter, 'package') && frontmatterString(doc.frontmatter, 'editRoot')) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let id: string | undefined
|
|
135
|
+
let path: string | undefined
|
|
136
|
+
let purpose = frontmatterString(doc.frontmatter, 'purpose') ?? doc.description
|
|
137
|
+
|
|
138
|
+
const packagesFile = /(?:^|\/)packages\/([^/]+)\.(?:md|mdx)$/.exec(doc.relPath)
|
|
139
|
+
const packagesIndex = /(?:^|\/)packages\/([^/]+)\/index\.(?:md|mdx)$/.exec(doc.relPath)
|
|
140
|
+
const registryReadme = /(?:^|\/)registry\/([^/]+)\/README\.(?:md|mdx)$/i.exec(doc.relPath)
|
|
141
|
+
const patternFile = /(?:^|\/)pillars\/[^/]+\/([^/]+(?:-pattern)?)\.(?:md|mdx)$/.exec(doc.relPath)
|
|
142
|
+
const topLevelPkg = /(?:^|\/)for-agents\/([^/]+)\.(?:md|mdx)$/.exec(doc.relPath)
|
|
143
|
+
|
|
144
|
+
if (packagesFile?.[1] && packagesFile[1] !== 'index') {
|
|
145
|
+
id = packagesFile[1]
|
|
146
|
+
path = `packages/${id}`
|
|
147
|
+
} else if (packagesIndex?.[1]) {
|
|
148
|
+
id = packagesIndex[1]
|
|
149
|
+
path = `packages/${id}`
|
|
150
|
+
} else if (registryReadme?.[1]) {
|
|
151
|
+
id = registryReadme[1]
|
|
152
|
+
path = `registry/${id}`
|
|
153
|
+
} else if (patternFile?.[1] && patternFile[1] !== 'index' && patternFile[1] !== 'universal') {
|
|
154
|
+
id = patternFile[1]
|
|
155
|
+
path = doc.path
|
|
156
|
+
purpose = purpose ?? `Playbook pattern: ${id}`
|
|
157
|
+
} else if (topLevelPkg?.[1] && topLevelPkg[1] !== 'index' && topLevelPkg[1] !== 'INDEX') {
|
|
158
|
+
// agentskit for-agents/core.mdx style
|
|
159
|
+
id = topLevelPkg[1]
|
|
160
|
+
path = `packages/${id}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!id || !path || seen.has(id)) continue
|
|
164
|
+
seen.add(id)
|
|
165
|
+
|
|
166
|
+
const humanDoc = frontmatterString(doc.frontmatter, 'humanDoc')
|
|
167
|
+
const checks = frontmatterStringList(doc.frontmatter, 'checks')
|
|
168
|
+
out.push({
|
|
169
|
+
id,
|
|
170
|
+
path,
|
|
171
|
+
agentDoc: doc.path,
|
|
172
|
+
...(purpose ? { purpose } : {}),
|
|
173
|
+
...(checks ? { checks } : {}),
|
|
174
|
+
...(humanDoc ? { humanDoc } : {}),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { relFromRoot }
|