@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export { defineConfig } from './config/define-config.js'
|
|
2
|
+
export { applyConfigDefaults } from './config/defaults.js'
|
|
3
|
+
export {
|
|
4
|
+
loadConfig,
|
|
5
|
+
resolveProjectRoot,
|
|
6
|
+
ConfigNotFoundError,
|
|
7
|
+
type LoadConfigOptions,
|
|
8
|
+
type LoadConfigResult,
|
|
9
|
+
} from './config/load-config.js'
|
|
10
|
+
export {
|
|
11
|
+
DocBridgeConfigV1Schema,
|
|
12
|
+
type DocBridgeConfigV1,
|
|
13
|
+
type AgentCorpusConfig,
|
|
14
|
+
} from './config/schema.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
AgentHandoffV1Schema,
|
|
18
|
+
AgentHandoffLegacySchema,
|
|
19
|
+
AgentSearchV1Schema,
|
|
20
|
+
HandoffTargetTypeSchema,
|
|
21
|
+
HANDOFF_SCHEMA_VERSION,
|
|
22
|
+
normalizeAgentHandoff,
|
|
23
|
+
type AgentHandoffV1,
|
|
24
|
+
type AgentSearchV1,
|
|
25
|
+
type HandoffTarget,
|
|
26
|
+
type HandoffTargetType,
|
|
27
|
+
} from './schemas/agent-handoff.js'
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
DocBridgeIndexV1Schema,
|
|
31
|
+
KnowledgeEntrySchema,
|
|
32
|
+
INDEX_SCHEMA_VERSION,
|
|
33
|
+
type DocBridgeIndexV1,
|
|
34
|
+
type KnowledgeEntry,
|
|
35
|
+
} from './schemas/doc-bridge-index.js'
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
MemoryCandidateV1Schema,
|
|
39
|
+
MEMORY_CANDIDATE_SCHEMA_VERSION,
|
|
40
|
+
type MemoryCandidateV1,
|
|
41
|
+
} from './schemas/memory-candidate.js'
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
AgentHandoffV1JsonSchema,
|
|
45
|
+
DocBridgeIndexV1JsonSchema,
|
|
46
|
+
DocBridgeJsonSchemas,
|
|
47
|
+
MemoryCandidateV1JsonSchema,
|
|
48
|
+
} from './schemas/json-schemas.js'
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
parseAgentHandoff,
|
|
52
|
+
parseAgentSearch,
|
|
53
|
+
parseDocBridgeConfig,
|
|
54
|
+
parseDocBridgeIndex,
|
|
55
|
+
parseMemoryCandidate,
|
|
56
|
+
safeParseAgentHandoff,
|
|
57
|
+
type ParseIssue,
|
|
58
|
+
type ParseResult,
|
|
59
|
+
} from './validate.js'
|
|
60
|
+
|
|
61
|
+
export { buildDocBridgeIndex, type BuildIndexOptions, type BuildIndexResult } from './index-builder/build-index.js'
|
|
62
|
+
export {
|
|
63
|
+
scanHumanDocRecords,
|
|
64
|
+
scanHumanDocs,
|
|
65
|
+
type HumanDocMap,
|
|
66
|
+
type HumanDocRecord,
|
|
67
|
+
} from './index-builder/plugins/human-markdown.js'
|
|
68
|
+
export {
|
|
69
|
+
resolveGateIds,
|
|
70
|
+
runGate,
|
|
71
|
+
runGates,
|
|
72
|
+
type GateId,
|
|
73
|
+
type GateResult,
|
|
74
|
+
type GateRunResult,
|
|
75
|
+
} from './gates/run-gates.js'
|
|
76
|
+
export { MCP_TOOLS, handleMcpRequest, startMcpStdioServer } from './mcp/server.js'
|
|
77
|
+
export { sha256NormalizedV1 } from './index-builder/content-hash.js'
|
|
78
|
+
export { IndexNotFoundError, indexFilePath, loadDocBridgeIndex, resolveRoot } from './query/load-index.js'
|
|
79
|
+
export { runQuery, type QueryKind, type QueryRequest, type QueryResult } from './query/query.js'
|
|
80
|
+
export { searchIndex, type SearchMatch } from './query/search.js'
|
|
81
|
+
export {
|
|
82
|
+
ingestAgentMemory,
|
|
83
|
+
ingestCursorRules,
|
|
84
|
+
ingestMemoryCandidates,
|
|
85
|
+
} from './memory/ingest.js'
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
classifyMemoryCandidates,
|
|
89
|
+
draftMemoryPromotion,
|
|
90
|
+
scanMemorySafety,
|
|
91
|
+
type MemoryClassification,
|
|
92
|
+
type MemoryPromotionDraft,
|
|
93
|
+
type MemoryRoute,
|
|
94
|
+
type SafetyFinding,
|
|
95
|
+
} from './memory/pipeline.js'
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
chunksFromMarkdown,
|
|
99
|
+
loadFederatedChunks,
|
|
100
|
+
parseLlmsTxtLinks,
|
|
101
|
+
retrieveHybridChunks,
|
|
102
|
+
type FederatedRetrieverOptions,
|
|
103
|
+
type FetchText,
|
|
104
|
+
} from './federation/llms.js'
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
createDocBridgeRetriever,
|
|
108
|
+
retrieveDocBridgeChunks,
|
|
109
|
+
type DocBridgeRetrievedChunk,
|
|
110
|
+
type DocBridgeRetriever,
|
|
111
|
+
type DocBridgeRetrieverOptions,
|
|
112
|
+
} from './retriever/doc-bridge-retriever.js'
|
|
113
|
+
|
|
114
|
+
export { PACKAGE_VERSION } from './version.js'
|
|
115
|
+
|
|
116
|
+
export { collectPackages, buildLookup } from './index-builder/build-handoffs.js'
|
|
117
|
+
export { projectRootFromConfigPath } from './config/load-config.js'
|
|
118
|
+
export { createDocBridgeRag } from './intelligence/rag.js'
|
|
119
|
+
export { runChatOnce, startInkChat } from './intelligence/chat.js'
|
|
120
|
+
export { PeerMissingError, layer1InstallHint } from './intelligence/peers.js'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
4
|
+
import { importPeer } from './peers.js'
|
|
5
|
+
|
|
6
|
+
const readEnv = (name: string | undefined): string | undefined => {
|
|
7
|
+
if (!name) return undefined
|
|
8
|
+
const value = process.env[name]
|
|
9
|
+
return value && value.length > 0 ? value : undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ResolvedIntelligence = {
|
|
13
|
+
readonly adapter: unknown
|
|
14
|
+
readonly embed: unknown
|
|
15
|
+
readonly provider: string
|
|
16
|
+
readonly model?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const resolveIntelligenceRuntime = async (
|
|
20
|
+
config: DocBridgeConfigV1,
|
|
21
|
+
): Promise<ResolvedIntelligence> => {
|
|
22
|
+
if (!config.intelligence?.enabled) {
|
|
23
|
+
throw new Error('Intelligence is disabled. Set intelligence.enabled: true in doc-bridge config.')
|
|
24
|
+
}
|
|
25
|
+
const adapterCfg = config.intelligence.adapter
|
|
26
|
+
if (!adapterCfg) {
|
|
27
|
+
throw new Error('intelligence.adapter is required for chat/RAG (provider + optional model).')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const adapters = await importPeer<typeof import('@agentskit/adapters')>('@agentskit/adapters')
|
|
31
|
+
const provider = adapterCfg.provider
|
|
32
|
+
const model = adapterCfg.model
|
|
33
|
+
const apiKey = readEnv(adapterCfg.apiKeyEnv) ?? readEnv('OPENAI_API_KEY') ?? readEnv('ANTHROPIC_API_KEY')
|
|
34
|
+
const baseUrl = adapterCfg.baseUrl
|
|
35
|
+
|
|
36
|
+
let adapter: unknown
|
|
37
|
+
let embed: unknown
|
|
38
|
+
|
|
39
|
+
switch (provider) {
|
|
40
|
+
case 'ollama': {
|
|
41
|
+
adapter = adapters.ollama({ model: model ?? 'llama3.2', ...(baseUrl ? { baseUrl } : {}) })
|
|
42
|
+
embed = adapters.ollamaEmbedder({
|
|
43
|
+
model: typeof adapterCfg.options?.embedModel === 'string'
|
|
44
|
+
? adapterCfg.options.embedModel
|
|
45
|
+
: 'nomic-embed-text',
|
|
46
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
47
|
+
})
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
case 'openai': {
|
|
51
|
+
if (!apiKey) throw new Error(`Missing API key env ${adapterCfg.apiKeyEnv ?? 'OPENAI_API_KEY'}`)
|
|
52
|
+
adapter = adapters.openai({ apiKey, ...(model ? { model } : {}), ...(baseUrl ? { baseUrl } : {}) })
|
|
53
|
+
embed = adapters.openaiEmbedder({ apiKey, model: 'text-embedding-3-small' })
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
case 'anthropic': {
|
|
57
|
+
if (!apiKey) throw new Error(`Missing API key env ${adapterCfg.apiKeyEnv ?? 'ANTHROPIC_API_KEY'}`)
|
|
58
|
+
adapter = adapters.anthropic({ apiKey, ...(model ? { model } : {}) })
|
|
59
|
+
// Anthropic has no default embedder in adapters — fall back to openai-compatible if key present
|
|
60
|
+
const openaiKey = readEnv('OPENAI_API_KEY')
|
|
61
|
+
if (openaiKey) embed = adapters.openaiEmbedder({ apiKey: openaiKey })
|
|
62
|
+
else {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'Anthropic chat requires an embedder for RAG. Set OPENAI_API_KEY for embeddings or use provider: ollama.',
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
case 'openrouter': {
|
|
70
|
+
if (!apiKey) throw new Error(`Missing API key env ${adapterCfg.apiKeyEnv ?? 'OPENROUTER_API_KEY'}`)
|
|
71
|
+
adapter = adapters.openrouter({ apiKey, ...(model ? { model } : {}) })
|
|
72
|
+
embed = adapters.openaiEmbedder({
|
|
73
|
+
apiKey,
|
|
74
|
+
model: 'text-embedding-3-small',
|
|
75
|
+
// openrouter embeddings path may vary; adapters handle openai-compatible
|
|
76
|
+
})
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
case 'custom':
|
|
80
|
+
throw new Error(
|
|
81
|
+
'provider: custom requires a programmatic integration. Use ollama, openai, anthropic, or openrouter in v0.1.',
|
|
82
|
+
)
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unsupported intelligence.adapter.provider: ${String(provider)}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { adapter, embed, provider, ...(model ? { model } : {}) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const defaultVectorStorePath = (root: string): string => join(root, '.doc-bridge', 'vectors')
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
3
|
+
import { runQuery } from '../query/query.js'
|
|
4
|
+
import { resolveIntelligenceRuntime } from './adapter.js'
|
|
5
|
+
import {
|
|
6
|
+
PeerMissingError,
|
|
7
|
+
importPeer,
|
|
8
|
+
isPeerResolutionFailure,
|
|
9
|
+
layer1InstallHint,
|
|
10
|
+
} from './peers.js'
|
|
11
|
+
import { createDocBridgeRag } from './rag.js'
|
|
12
|
+
|
|
13
|
+
const wrapIntelligenceError = (error: unknown): Error => {
|
|
14
|
+
if (error instanceof PeerMissingError) return error
|
|
15
|
+
if (isPeerResolutionFailure(error)) {
|
|
16
|
+
return new PeerMissingError('@agentskit/*', layer1InstallHint())
|
|
17
|
+
}
|
|
18
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
19
|
+
if (/fetch failed|econnrefused|enotfound|network|socket/i.test(message)) {
|
|
20
|
+
return new Error(
|
|
21
|
+
`Intelligence provider request failed (${message}).\n` +
|
|
22
|
+
`Check the model server (e.g. \`ollama serve\`) and Layer 1 peers:\n ${layer1InstallHint()}`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
return error instanceof Error ? error : new Error(message)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const handoffFirstHint = (
|
|
29
|
+
index: DocBridgeIndexV1,
|
|
30
|
+
config: DocBridgeConfigV1,
|
|
31
|
+
question: string,
|
|
32
|
+
): string | undefined => {
|
|
33
|
+
if (config.intelligence?.chat?.handoffFirst === false) return undefined
|
|
34
|
+
const tokens = question
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.split(/[^a-z0-9@/_-]+/)
|
|
37
|
+
.filter((t) => t.length >= 2)
|
|
38
|
+
const packages = index.lookup?.packages ?? []
|
|
39
|
+
const hit = packages.find((id) => tokens.includes(id.toLowerCase()))
|
|
40
|
+
if (!hit) return undefined
|
|
41
|
+
try {
|
|
42
|
+
const handoff = runQuery(index, config, { kind: 'ownership', id: hit, agent: true })
|
|
43
|
+
return [
|
|
44
|
+
'## Deterministic AgentHandoff (prefer for edits)',
|
|
45
|
+
'```json',
|
|
46
|
+
JSON.stringify(handoff, null, 2),
|
|
47
|
+
'```',
|
|
48
|
+
'Use startHere / editRoots / checks before broad exploration.',
|
|
49
|
+
].join('\n')
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const systemPrompt = (
|
|
56
|
+
index: DocBridgeIndexV1,
|
|
57
|
+
config: DocBridgeConfigV1,
|
|
58
|
+
provider: string,
|
|
59
|
+
model: string | undefined,
|
|
60
|
+
question?: string,
|
|
61
|
+
): string => {
|
|
62
|
+
const prefix = question ? handoffFirstHint(index, config, question) : undefined
|
|
63
|
+
return [
|
|
64
|
+
'You are a documentation assistant for this repository (doc-bridge).',
|
|
65
|
+
'Prefer deterministic AgentHandoff fields for code edits.',
|
|
66
|
+
'Cite file paths from retrieved context.',
|
|
67
|
+
`Provider: ${provider}${model ? ` / ${model}` : ''}.`,
|
|
68
|
+
'If unsure, suggest: ak-docs query ownership <id> --agent or MCP handoff.resolve.',
|
|
69
|
+
prefix ?? '',
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join('\n\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const runChatOnce = async (
|
|
76
|
+
root: string,
|
|
77
|
+
config: DocBridgeConfigV1,
|
|
78
|
+
index: DocBridgeIndexV1,
|
|
79
|
+
question: string,
|
|
80
|
+
): Promise<{ content: string; handoffPrefixed: boolean }> => {
|
|
81
|
+
try {
|
|
82
|
+
const { adapter, provider, model } = await resolveIntelligenceRuntime(config)
|
|
83
|
+
const core = await importPeer<typeof import('@agentskit/core')>('@agentskit/core')
|
|
84
|
+
const rag = await createDocBridgeRag(root, config, index)
|
|
85
|
+
await rag.ingest()
|
|
86
|
+
|
|
87
|
+
const controller = core.createChatController({
|
|
88
|
+
adapter,
|
|
89
|
+
retriever: rag.retriever,
|
|
90
|
+
system: systemPrompt(index, config, provider, model, question),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const result = await controller.send(question)
|
|
94
|
+
const content =
|
|
95
|
+
typeof result?.content === 'string' ? result.content : JSON.stringify(result, null, 2)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content,
|
|
99
|
+
handoffPrefixed: Boolean(handoffFirstHint(index, config, question)),
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw wrapIntelligenceError(error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const startInkChat = async (
|
|
107
|
+
root: string,
|
|
108
|
+
config: DocBridgeConfigV1,
|
|
109
|
+
index: DocBridgeIndexV1,
|
|
110
|
+
): Promise<void> => {
|
|
111
|
+
try {
|
|
112
|
+
const { adapter, provider, model } = await resolveIntelligenceRuntime(config)
|
|
113
|
+
const rag = await createDocBridgeRag(root, config, index)
|
|
114
|
+
await rag.ingest()
|
|
115
|
+
|
|
116
|
+
const React = await importPeer<typeof import('react')>('react')
|
|
117
|
+
const ink = await importPeer<typeof import('ink')>('ink')
|
|
118
|
+
const agentskitInk = await importPeer<typeof import('@agentskit/ink')>('@agentskit/ink')
|
|
119
|
+
|
|
120
|
+
const App = () => {
|
|
121
|
+
const chat = agentskitInk.useChat({
|
|
122
|
+
adapter,
|
|
123
|
+
retriever: rag.retriever,
|
|
124
|
+
system: systemPrompt(index, config, provider, model),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return React.createElement(
|
|
128
|
+
agentskitInk.ChatContainer as never,
|
|
129
|
+
null,
|
|
130
|
+
...chat.messages.map((msg) =>
|
|
131
|
+
React.createElement(agentskitInk.Message as never, {
|
|
132
|
+
key: msg.id,
|
|
133
|
+
message: msg,
|
|
134
|
+
} as never),
|
|
135
|
+
),
|
|
136
|
+
React.createElement(agentskitInk.InputBar as never, {
|
|
137
|
+
chat,
|
|
138
|
+
placeholder: 'Ask about project docs (handoff-first RAG)…',
|
|
139
|
+
} as never),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const instance = ink.render(React.createElement(App))
|
|
144
|
+
await instance.waitUntilExit()
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw wrapIntelligenceError(error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class PeerMissingError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
readonly peer: string,
|
|
4
|
+
readonly installHint: string,
|
|
5
|
+
) {
|
|
6
|
+
super(
|
|
7
|
+
`Optional peer "${peer}" is not installed.\nInstall Layer 1 intelligence peers:\n ${installHint}`,
|
|
8
|
+
)
|
|
9
|
+
this.name = 'PeerMissingError'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LAYER1_INSTALL =
|
|
14
|
+
'npm i -D @agentskit/rag @agentskit/ink @agentskit/adapters @agentskit/memory react'
|
|
15
|
+
|
|
16
|
+
/** True when a dynamic import failed because the package could not be resolved/loaded. */
|
|
17
|
+
export const isPeerResolutionFailure = (error: unknown): boolean => {
|
|
18
|
+
const parts: string[] = []
|
|
19
|
+
let cur: unknown = error
|
|
20
|
+
for (let i = 0; i < 6 && cur; i += 1) {
|
|
21
|
+
if (cur instanceof Error) {
|
|
22
|
+
parts.push(cur.message, cur.name)
|
|
23
|
+
const code = (cur as NodeJS.ErrnoException).code
|
|
24
|
+
if (code) parts.push(String(code))
|
|
25
|
+
cur = cur.cause
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
parts.push(String(cur))
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
const text = parts.join(' ').toLowerCase()
|
|
32
|
+
return (
|
|
33
|
+
text.includes('cannot find package') ||
|
|
34
|
+
text.includes('cannot find module') ||
|
|
35
|
+
text.includes('module not found') ||
|
|
36
|
+
text.includes('err_module_not_found') ||
|
|
37
|
+
text.includes('err_package_not_found') ||
|
|
38
|
+
text.includes('err_package_path_not_exported') ||
|
|
39
|
+
text.includes('failed to resolve module') ||
|
|
40
|
+
text.includes('failed to resolve') ||
|
|
41
|
+
// Node / network resolution edge cases for bare package imports
|
|
42
|
+
text.includes('fetch failed') ||
|
|
43
|
+
text.includes('enotfound') ||
|
|
44
|
+
text.includes('getaddrinfo')
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const importPeer = async <T>(name: string): Promise<T> => {
|
|
49
|
+
try {
|
|
50
|
+
return (await import(name)) as T
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isPeerResolutionFailure(error)) {
|
|
53
|
+
throw new PeerMissingError(name, LAYER1_INSTALL)
|
|
54
|
+
}
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const layer1InstallHint = (): string => LAYER1_INSTALL
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
6
|
+
import { defaultVectorStorePath, resolveIntelligenceRuntime } from './adapter.js'
|
|
7
|
+
import { importPeer } from './peers.js'
|
|
8
|
+
|
|
9
|
+
export type RagSearchHit = {
|
|
10
|
+
readonly id?: string
|
|
11
|
+
readonly content: string
|
|
12
|
+
readonly score?: number
|
|
13
|
+
readonly source?: string
|
|
14
|
+
readonly metadata?: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DocBridgeRag = {
|
|
18
|
+
readonly ingest: () => Promise<{ documentCount: number; storePath: string }>
|
|
19
|
+
readonly search: (query: string, topK?: number) => Promise<RagSearchHit[]>
|
|
20
|
+
readonly retriever: unknown
|
|
21
|
+
readonly storePath: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const loadDocuments = (
|
|
25
|
+
root: string,
|
|
26
|
+
index: DocBridgeIndexV1,
|
|
27
|
+
sources: readonly string[],
|
|
28
|
+
): Array<{ id: string; content: string; source: string; metadata: Record<string, unknown> }> => {
|
|
29
|
+
const includeAgent = sources.includes('agent') || sources.length === 0
|
|
30
|
+
const docs: Array<{ id: string; content: string; source: string; metadata: Record<string, unknown> }> = []
|
|
31
|
+
|
|
32
|
+
if (includeAgent) {
|
|
33
|
+
for (const entry of index.knowledge) {
|
|
34
|
+
const abs = join(root, entry.path)
|
|
35
|
+
let content = ''
|
|
36
|
+
try {
|
|
37
|
+
content = readFileSync(abs, 'utf8')
|
|
38
|
+
} catch {
|
|
39
|
+
content = [entry.title, entry.description].filter(Boolean).join('\n\n')
|
|
40
|
+
}
|
|
41
|
+
docs.push({
|
|
42
|
+
id: entry.id,
|
|
43
|
+
content,
|
|
44
|
+
source: entry.path,
|
|
45
|
+
metadata: { type: entry.type, title: entry.title, path: entry.path },
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Human corpus is linked via humanDoc URLs; optional future: ingest human paths from adapters
|
|
51
|
+
return docs
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const createDocBridgeRag = async (
|
|
55
|
+
root: string,
|
|
56
|
+
config: DocBridgeConfigV1,
|
|
57
|
+
index: DocBridgeIndexV1,
|
|
58
|
+
): Promise<DocBridgeRag> => {
|
|
59
|
+
const { embed } = await resolveIntelligenceRuntime(config)
|
|
60
|
+
const ragMod = await importPeer<typeof import('@agentskit/rag')>('@agentskit/rag')
|
|
61
|
+
const memoryMod = await importPeer<typeof import('@agentskit/memory')>('@agentskit/memory')
|
|
62
|
+
|
|
63
|
+
const storePath =
|
|
64
|
+
typeof config.intelligence?.retriever?.options?.storePath === 'string'
|
|
65
|
+
? join(root, config.intelligence.retriever.options.storePath)
|
|
66
|
+
: defaultVectorStorePath(root)
|
|
67
|
+
|
|
68
|
+
const store = memoryMod.fileVectorMemory({ path: storePath })
|
|
69
|
+
const rag = ragMod.createRAG({
|
|
70
|
+
embed,
|
|
71
|
+
store,
|
|
72
|
+
topK: 6,
|
|
73
|
+
chunkSize: 900,
|
|
74
|
+
chunkOverlap: 120,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const sources = config.intelligence?.chat?.sources ?? ['agent', 'human']
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
storePath,
|
|
81
|
+
retriever: rag,
|
|
82
|
+
ingest: async () => {
|
|
83
|
+
const documents = loadDocuments(root, index, sources)
|
|
84
|
+
await rag.ingest(documents)
|
|
85
|
+
return { documentCount: documents.length, storePath }
|
|
86
|
+
},
|
|
87
|
+
search: async (query, topK = 6) => {
|
|
88
|
+
const hits = await rag.search(query, { topK })
|
|
89
|
+
return hits.map((hit) => ({
|
|
90
|
+
...(hit.id ? { id: hit.id } : {}),
|
|
91
|
+
content: hit.content,
|
|
92
|
+
...(hit.score !== undefined ? { score: hit.score } : {}),
|
|
93
|
+
...(hit.source ? { source: hit.source } : {}),
|
|
94
|
+
...(hit.metadata ? { metadata: hit.metadata } : {}),
|
|
95
|
+
}))
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { toPosix } from './paths.js'
|
|
5
|
+
|
|
6
|
+
/** Expand simple workspace globs: `packages/*`, `apps/*`, literal paths. */
|
|
7
|
+
export const expandWorkspaceGlobs = (root: string, patterns: readonly string[]): string[] => {
|
|
8
|
+
const dirs = new Set<string>()
|
|
9
|
+
|
|
10
|
+
for (const pattern of patterns) {
|
|
11
|
+
const normalized = toPosix(pattern).replace(/\/$/, '')
|
|
12
|
+
if (!normalized.includes('*')) {
|
|
13
|
+
const abs = join(root, normalized)
|
|
14
|
+
if (existsSync(abs)) dirs.add(toPosix(abs))
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const star = normalized.indexOf('*')
|
|
19
|
+
const base = join(root, normalized.slice(0, star).replace(/\/$/, ''))
|
|
20
|
+
if (!existsSync(base)) continue
|
|
21
|
+
|
|
22
|
+
for (const name of readdirSync(base)) {
|
|
23
|
+
const abs = join(base, name)
|
|
24
|
+
try {
|
|
25
|
+
if (statSync(abs).isDirectory()) dirs.add(toPosix(abs))
|
|
26
|
+
} catch {
|
|
27
|
+
// skip
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [...dirs].sort()
|
|
33
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type FrontmatterValue = string | boolean | readonly string[]
|
|
2
|
+
export type FrontmatterData = Record<string, FrontmatterValue>
|
|
3
|
+
|
|
4
|
+
export const parseFrontmatter = (
|
|
5
|
+
markdown: string,
|
|
6
|
+
): { readonly data: FrontmatterData; readonly body: string } => {
|
|
7
|
+
const normalized = markdown.replace(/^\uFEFF/, '')
|
|
8
|
+
if (!normalized.startsWith('---\n') && !normalized.startsWith('---\r\n')) {
|
|
9
|
+
return { data: {}, body: markdown }
|
|
10
|
+
}
|
|
11
|
+
const endMatch = /\r?\n---\r?\n/.exec(normalized.slice(3))
|
|
12
|
+
if (!endMatch || endMatch.index === undefined) {
|
|
13
|
+
return { data: {}, body: markdown }
|
|
14
|
+
}
|
|
15
|
+
const block = normalized.slice(4, 3 + endMatch.index)
|
|
16
|
+
const body = normalized.slice(3 + endMatch.index + endMatch[0].length)
|
|
17
|
+
const data: Record<string, FrontmatterValue> = {}
|
|
18
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
19
|
+
const line = rawLine.trim()
|
|
20
|
+
if (!line || line.startsWith('#')) continue
|
|
21
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
|
|
22
|
+
if (!m?.[1]) continue
|
|
23
|
+
const key = m[1]
|
|
24
|
+
const raw = (m[2] ?? '').trim()
|
|
25
|
+
if (raw === 'true') data[key] = true
|
|
26
|
+
else if (raw === 'false') data[key] = false
|
|
27
|
+
else if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
28
|
+
data[key] = raw
|
|
29
|
+
.slice(1, -1)
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((part) => part.trim().replace(/^['"]|['"]$/g, ''))
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
} else {
|
|
34
|
+
data[key] = raw.replace(/^['"]|['"]$/g, '')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { data, body }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const frontmatterString = (
|
|
41
|
+
data: FrontmatterData,
|
|
42
|
+
key: string,
|
|
43
|
+
): string | undefined => {
|
|
44
|
+
const value = data[key]
|
|
45
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const frontmatterStringList = (
|
|
49
|
+
data: FrontmatterData,
|
|
50
|
+
key: string,
|
|
51
|
+
): string[] | undefined => {
|
|
52
|
+
const value = data[key]
|
|
53
|
+
if (Array.isArray(value)) return value.filter((item): item is string => typeof item === 'string')
|
|
54
|
+
if (typeof value === 'string' && value.length > 0) return [value]
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const firstHeading = (markdown: string): string | undefined => {
|
|
59
|
+
const { body } = parseFrontmatter(markdown)
|
|
60
|
+
for (const line of body.split('\n')) {
|
|
61
|
+
const m = /^#\s+(.+)$/.exec(line.trim())
|
|
62
|
+
if (m?.[1]) return m[1].trim()
|
|
63
|
+
}
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Complete first prose block — prefer full sentences, cap length without mid-word cuts. */
|
|
68
|
+
export const firstParagraph = (markdown: string, maxLen = 400): string | undefined => {
|
|
69
|
+
const { body } = parseFrontmatter(markdown)
|
|
70
|
+
const lines = body.split('\n')
|
|
71
|
+
const buf: string[] = []
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const t = line.trim()
|
|
74
|
+
if (!t) {
|
|
75
|
+
if (buf.length) break
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (t.startsWith('#')) continue
|
|
79
|
+
if (t.startsWith('---')) continue
|
|
80
|
+
if (t.startsWith('```')) break
|
|
81
|
+
if (t.startsWith('|') || t.startsWith('- [') || t.startsWith('* [')) {
|
|
82
|
+
if (buf.length) break
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
buf.push(t)
|
|
86
|
+
if (buf.join(' ').length >= maxLen) break
|
|
87
|
+
}
|
|
88
|
+
let text = buf.join(' ').replace(/\s+/g, ' ').trim()
|
|
89
|
+
if (!text) return undefined
|
|
90
|
+
if (text.length <= maxLen) return text
|
|
91
|
+
// Prefer ending on sentence boundary
|
|
92
|
+
const sliced = text.slice(0, maxLen)
|
|
93
|
+
const sentenceEnd = Math.max(sliced.lastIndexOf('. '), sliced.lastIndexOf('! '), sliced.lastIndexOf('? '))
|
|
94
|
+
if (sentenceEnd > maxLen * 0.4) return sliced.slice(0, sentenceEnd + 1).trim()
|
|
95
|
+
const wordEnd = sliced.lastIndexOf(' ')
|
|
96
|
+
return (wordEnd > 0 ? sliced.slice(0, wordEnd) : sliced).trim()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Flatten markdown body for search (strip fences/links noise lightly). */
|
|
100
|
+
export const extractSearchBody = (markdown: string, maxLen = 6_000): string => {
|
|
101
|
+
const { body } = parseFrontmatter(markdown)
|
|
102
|
+
const text = body
|
|
103
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
104
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
|
|
105
|
+
.replace(/\[[^\]]*\]\([^)]+\)/g, ' ')
|
|
106
|
+
.replace(/^#+\s+/gm, '')
|
|
107
|
+
.replace(/[|>*_`#]/g, ' ')
|
|
108
|
+
.replace(/\s+/g, ' ')
|
|
109
|
+
.trim()
|
|
110
|
+
return text.length > maxLen ? text.slice(0, maxLen) : text
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const slugFromPath = (relPath: string): string => {
|
|
114
|
+
const base = relPath.replace(/\.mdx?$/, '')
|
|
115
|
+
const parts = base.split('/')
|
|
116
|
+
return parts[parts.length - 1] ?? base
|
|
117
|
+
}
|