@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/cli/program.js +283 -105
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +6 -3
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-CD_zmKXf.d.ts → index-CPUJbTbg.d.ts} +19 -10
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +278 -100
  11. package/dist/index.js.map +1 -1
  12. package/docs/DOGFOOD.md +92 -0
  13. package/package.json +27 -9
  14. package/scripts/prepare.mjs +44 -0
  15. package/src/cli/program.ts +933 -0
  16. package/src/config/defaults.ts +63 -0
  17. package/src/config/define-config.ts +6 -0
  18. package/src/config/index.ts +15 -0
  19. package/src/config/load-config.ts +138 -0
  20. package/src/config/schema.ts +294 -0
  21. package/src/federation/llms.ts +145 -0
  22. package/src/gates/run-gates.ts +274 -0
  23. package/src/index-builder/build-handoffs.ts +217 -0
  24. package/src/index-builder/build-index.ts +137 -0
  25. package/src/index-builder/capabilities.ts +37 -0
  26. package/src/index-builder/content-hash.ts +19 -0
  27. package/src/index-builder/human-adapters/core.ts +100 -0
  28. package/src/index-builder/human-adapters/docusaurus.ts +113 -0
  29. package/src/index-builder/human-adapters/fumadocs.ts +70 -0
  30. package/src/index-builder/human-adapters/index.ts +52 -0
  31. package/src/index-builder/human-adapters/plain-markdown.ts +10 -0
  32. package/src/index-builder/llms-txt.ts +19 -0
  33. package/src/index-builder/plugins/human-markdown.ts +7 -0
  34. package/src/index-builder/plugins/pnpm-monorepo.ts +72 -0
  35. package/src/index-builder/scan-corpus.ts +181 -0
  36. package/src/index.ts +120 -0
  37. package/src/intelligence/adapter.ts +90 -0
  38. package/src/intelligence/chat.ts +148 -0
  39. package/src/intelligence/peers.ts +59 -0
  40. package/src/intelligence/rag.ts +98 -0
  41. package/src/lib/glob-expand.ts +33 -0
  42. package/src/lib/markdown.ts +90 -0
  43. package/src/lib/package-manager.ts +104 -0
  44. package/src/lib/paths.ts +6 -0
  45. package/src/lib/walk.ts +45 -0
  46. package/src/mcp/server.ts +276 -0
  47. package/src/memory/ingest.ts +51 -0
  48. package/src/memory/pipeline.ts +119 -0
  49. package/src/query/load-index.ts +25 -0
  50. package/src/query/query.ts +142 -0
  51. package/src/query/search.ts +58 -0
  52. package/src/retriever/doc-bridge-retriever.ts +62 -0
  53. package/src/schemas/agent-handoff.ts +82 -0
  54. package/src/schemas/doc-bridge-index.ts +106 -0
  55. package/src/schemas/json-schemas.ts +156 -0
  56. package/src/schemas/memory-candidate.ts +37 -0
  57. package/src/shims/agentskit-peers.d.ts +80 -0
  58. package/src/validate.ts +67 -0
  59. package/src/version.ts +1 -0
  60. package/tsconfig.json +21 -0
  61. 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,90 @@
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
+ export const firstParagraph = (markdown: string): string | undefined => {
68
+ const { body } = parseFrontmatter(markdown)
69
+ const lines = body.split('\n')
70
+ const buf: string[] = []
71
+ for (const line of lines) {
72
+ const t = line.trim()
73
+ if (!t) {
74
+ if (buf.length) break
75
+ continue
76
+ }
77
+ if (t.startsWith('#')) continue
78
+ if (t.startsWith('---')) continue
79
+ buf.push(t)
80
+ if (buf.join(' ').length > 40) break
81
+ }
82
+ const text = buf.join(' ').trim()
83
+ return text || undefined
84
+ }
85
+
86
+ export const slugFromPath = (relPath: string): string => {
87
+ const base = relPath.replace(/\.mdx?$/, '')
88
+ const parts = base.split('/')
89
+ return parts[parts.length - 1] ?? base
90
+ }