import { createHash } from "node:crypto"; import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path"; import { parse as parseYaml } from "yaml"; import { buildDollarRegistryForContext, buildRegistryFromSystemPrompt, createDollarSkillAutocompleteProvider, discoveryAutocompleteItems, dollarAutocompleteItems, extractDollarAutocompletePrefix, scoreDollarSuggestion, } from "./dollar"; import { buildRegistryFromRoots, normalizeKey, stringArray, uniqueStrings, } from "./skill-records"; import { buildDiscoveryRegistryFromParts, configuredDiscoveryCatalogs, readDiscoveryCatalog, validateDiscoveryRecords, recordAliases, recordDescription, recordKind, recordName, recordNormalizedAliases, recordNormalizedDescription, recordNormalizedName, recordPath, } from "./discovery-records"; import { collectRootDiagnostics, formatFindDiagnostics, formatRootDiagnostics, } from "./diagnostics"; import { installPiDollarAutocompleteEditor } from "./pi-dollar-editor"; import type { AgentMessage, BeforeAgentStartEvent, BeforeAgentStartResult, ConfigFile, ContextEvent, ContextInjection, ContextResult, DiscoveryRecord, ExtensionAPI, ExtensionContext, InvocationRule, MatchDecision, MatchPredicate, MessageContent, PathMode, RegexPattern, ScanConfig, SessionEntry, SkillRecord, SkillRequest, TextContent, } from "./types"; const CUSTOM_TYPE = "context-broker"; const BUILTIN_SKILL_PROMPT_TYPE = "skill-prompt"; const CONFIG_DIR_BASENAME = "context-broker"; const DEFAULT_CONFIG_FILE_BASENAME = "config"; const DEFAULT_CONFIG_EXTENSIONS = [".yml", ".yaml", ".json"]; const DEFAULT_RULE_ROOT_BASENAME = "rules"; const RULE_FILE_EXTENSIONS = new Set(DEFAULT_CONFIG_EXTENSIONS); const DEFAULT_PATH_MODE: PathMode = "home-relative"; const DOLLAR_SKILL_PATTERN = "(?:^|[\\s,,。.!!??;;、::([{(【])\\$(?[^\\s$]+?)(?=$|[\\s,,。.!!??;;、::))\\]】}])"; const DEFAULT_RULES: InvocationRule[] = [ { id: "dollar-skill", match: [{ regex: [{ pattern: DOLLAR_SKILL_PATTERN, flags: "g" }] }] }, { id: "skill-colon", match: [{ regex: [{ pattern: "^\\s*skill[::]\\s*(?.+?)\\s*$" }] }] }, { id: "use-skill-en", match: [{ regex: [{ pattern: "^\\s*use\\s+(?.+?)\\s+skill\\s*$", flags: "i" }] }] }, { id: "use-skill-en-reversed", match: [{ regex: [{ pattern: "^\\s*use\\s+skill\\s+(?.+?)\\s*$", flags: "i" }] }] }, { id: "use-skill-zh", match: [{ regex: [{ pattern: "^\\s*使用\\s*(?.+?)\\s*(?:skill|技能)\\s*$", flags: "i" }] }] }, ]; function splitEnvList(value: string | undefined): string[] { if (!value) return []; return value.split(delimiter).map((part) => part.trim()).filter(Boolean); } function homeDir(): string { return process.env.HOME || homedir(); } function expandPath(path: string, cwd = process.cwd()): string { const home = homeDir(); const expanded = path === "~" ? home : path.startsWith("~/") ? join(home, path.slice(2)) : path; return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded); } type HostName = "pi" | "omp"; function detectHost(): HostName | undefined { const override = process.env.CONTEXT_BROKER_HOST?.trim().toLowerCase(); if (override === "pi" || override === "omp") return override; const argv = [process.execPath, ...process.argv].map((part) => basename(part).toLowerCase()); if (argv.some((part) => part === "omp" || part.includes("oh-my-pi"))) return "omp"; if (argv.some((part) => part === "pi" || part.includes("earendil-works"))) return "pi"; if (process.env.PI_CONFIG_DIR) return "omp"; return undefined; } function defaultAgentDir(): string | undefined { return defaultAgentDirs()[0]; } function defaultAgentDirs(): string[] { const envAgentDir = process.env.PI_CODING_AGENT_DIR; if (envAgentDir) return [expandPath(envAgentDir)]; const host = detectHost(); const home = homeDir(); if (host === "omp") return [join(home, process.env.PI_CONFIG_DIR || ".omp", "agent")]; if (host === "pi") return [join(home, ".pi", "agent")]; return [ join(home, ".pi", "agent"), join(home, process.env.PI_CONFIG_DIR || ".omp", "agent"), ]; } function candidateConfigPaths(agentDir: string): string[] { return DEFAULT_CONFIG_EXTENSIONS.map((extension) => join(agentDir, CONFIG_DIR_BASENAME, `${DEFAULT_CONFIG_FILE_BASENAME}${extension}`)); } function firstExistingConfigPath(agentDir: string): string | undefined { return candidateConfigPaths(agentDir).find((path) => existsSync(path)); } function defaultConfigPath(): string | undefined { const agentDirs = defaultAgentDirs(); for (const agentDir of agentDirs) { const path = firstExistingConfigPath(agentDir); if (path) return path; } const agentDir = agentDirs[0]; return agentDir ? join(agentDir, CONFIG_DIR_BASENAME, `${DEFAULT_CONFIG_FILE_BASENAME}${DEFAULT_CONFIG_EXTENSIONS[0]}`) : undefined; } function resolveConfigPaths(): string[] { const explicit = process.env.CONTEXT_BROKER_CONFIG; if (explicit) return [expandPath(explicit)]; const paths = defaultAgentDirs() .map(firstExistingConfigPath) .filter((path): path is string => path !== undefined); return uniqueStrings(paths); } function resolveConfigPath(): string | undefined { const explicit = process.env.CONTEXT_BROKER_CONFIG; if (explicit) return expandPath(explicit); return resolveConfigPaths()[0]; } function parseConfigFile(configPath: string): unknown { const content = readFileSync(configPath, "utf8"); const extension = extname(configPath).toLowerCase(); if (extension === ".yml" || extension === ".yaml") return parseYaml(content); return JSON.parse(content); } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function mergeConfigs(configs: ConfigFile[]): ConfigFile { const requireAutoloadValues = configs .map((config) => config.requireAutoload) .filter((value): value is boolean => typeof value === "boolean"); const pathModes = configs.map((config) => config.pathMode).filter((value): value is PathMode => Boolean(value)); const logPathValues = configs.map((config) => config.logPaths).filter((value): value is boolean => typeof value === "boolean"); return { skillRoots: uniqueStrings(configs.flatMap((config) => config.skillRoots ?? [])), extraSkillRoots: uniqueStrings(configs.flatMap((config) => config.extraSkillRoots ?? [])), ruleRoots: uniqueStrings(configs.flatMap((config) => config.ruleRoots ?? [])), discoveryCatalogs: uniqueStrings(configs.flatMap((config) => config.discoveryCatalogs ?? [])), extraDiscoveryCatalogs: uniqueStrings(configs.flatMap((config) => config.extraDiscoveryCatalogs ?? [])), rules: uniqueRules(configs.flatMap((config) => config.rules ?? [])), requireAutoload: requireAutoloadValues.length > 0 ? requireAutoloadValues.at(-1) : undefined, pathMode: pathModes.at(-1), logPaths: logPathValues.length > 0 ? logPathValues.at(-1) : undefined, scan: mergeScanConfigs(configs.map((config) => config.scan)), debug: configs.some((config) => config.debug === true) || undefined, }; } function mergeScanConfigs(configs: Array): ScanConfig | undefined { const defined = configs.filter((config): config is ScanConfig => Boolean(config)); if (defined.length === 0) return undefined; const maxDepthValues = defined.map((config) => config.maxDepth).filter((value): value is number => typeof value === "number"); const maxSkillBytesValues = defined.map((config) => config.maxSkillBytes).filter((value): value is number => typeof value === "number"); return { maxDepth: maxDepthValues.at(-1), ignore: uniqueStrings(defined.flatMap((config) => config.ignore ?? [])), maxSkillBytes: maxSkillBytesValues.at(-1), }; } function uniqueRules(rules: InvocationRule[]): InvocationRule[] | undefined { const seen = new Set(); const result: InvocationRule[] = []; for (const rule of rules) { const key = JSON.stringify(rule); if (seen.has(key)) continue; seen.add(key); result.push(rule); } return result.length > 0 ? result : undefined; } function ruleRootsForConfig(configPath: string, config: ConfigFile): string[] { const baseDir = dirname(configPath); const defaultRuleRoot = join(baseDir, DEFAULT_RULE_ROOT_BASENAME); const roots = config.ruleRoots && config.ruleRoots.length > 0 ? config.ruleRoots : [defaultRuleRoot]; return uniqueStrings(roots.map((root) => expandPath(root, baseDir))); } function listRuleConfigFiles(ruleRoots: string[]): string[] { const files: string[] = []; for (const ruleRoot of ruleRoots) { let entries; try { entries = readdirSync(ruleRoot, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (!entry.isFile() || entry.name.startsWith(".")) continue; if (!RULE_FILE_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue; files.push(join(ruleRoot, entry.name)); } } return uniqueStrings(files).sort(); } function normalizeStringList(value: unknown): string[] { if (typeof value === "string") return value.trim() ? [value.trim()] : []; if (!Array.isArray(value)) return []; return uniqueStrings(value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)); } function normalizeInject(value: unknown): string[] { return normalizeStringList(value); } function normalizePathMode(value: unknown): PathMode | undefined { return value === "absolute" || value === "home-relative" || value === "basename" || value === "hash" ? value : undefined; } function normalizePositiveInteger(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return undefined; return value; } function normalizeScanConfig(value: unknown): ScanConfig | undefined { if (!isRecord(value)) return undefined; const ignore = normalizeStringList(value.ignore); const scan: ScanConfig = { maxDepth: normalizePositiveInteger(value.maxDepth), ignore, maxSkillBytes: normalizePositiveInteger(value.maxSkillBytes), }; return scan.maxDepth !== undefined || ignore.length > 0 || scan.maxSkillBytes !== undefined ? scan : undefined; } function normalizeRegexPattern(value: unknown): RegexPattern | undefined { if (typeof value === "string" && value.trim()) return { pattern: value }; if (!isRecord(value) || typeof value.pattern !== "string" || !value.pattern.trim()) return undefined; return { pattern: value.pattern, flags: typeof value.flags === "string" ? value.flags : undefined, }; } function normalizeRegexPatterns(value: unknown): RegexPattern[] { if (!Array.isArray(value)) { const pattern = normalizeRegexPattern(value); return pattern ? [pattern] : []; } return value .map(normalizeRegexPattern) .filter((pattern): pattern is RegexPattern => pattern !== undefined); } function normalizeNotList(value: unknown): MatchPredicate[] { if (!Array.isArray(value)) { const predicate = normalizeMatchPredicate(value); return predicate ? [predicate] : []; } return value .map(normalizeMatchPredicate) .filter((predicate): predicate is MatchPredicate => predicate !== undefined); } function normalizeMatchPredicate(value: unknown): MatchPredicate | undefined { if (!isRecord(value)) return undefined; const exact = normalizeStringList(value.exact); const contains = normalizeStringList(value.contains); const regex = normalizeRegexPatterns(value.regex); const not = normalizeNotList(value.not); if (exact.length === 0 && contains.length === 0 && regex.length === 0) return undefined; return { exact: exact.length > 0 ? exact : undefined, contains: contains.length > 0 ? contains : undefined, regex: regex.length > 0 ? regex : undefined, not: not.length > 0 ? not : undefined, }; } function normalizeMatchList(value: unknown): MatchPredicate[] { if (!Array.isArray(value)) { const predicate = normalizeMatchPredicate(value); return predicate ? [predicate] : []; } return value .map(normalizeMatchPredicate) .filter((predicate): predicate is MatchPredicate => predicate !== undefined); } function normalizeRuleConfigFile(parsed: unknown, filePath: string): ConfigFile { if (!isRecord(parsed) || parsed.enabled === false) return {}; const fileId = typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : basename(filePath, extname(filePath)); const inject = normalizeInject(parsed.inject); const match = normalizeMatchList(parsed.match); const rules: InvocationRule[] = inject.length > 0 && match.length > 0 ? [{ id: fileId, inject, match }] : []; return { rules: rules.length > 0 ? rules : undefined, debug: parsed.debug === true || undefined, }; } function readRuleConfigs(configPath: string, config: ConfigFile): ConfigFile[] { const configs: ConfigFile[] = []; for (const ruleFile of listRuleConfigFiles(ruleRootsForConfig(configPath, config))) { try { configs.push(normalizeRuleConfigFile(parseConfigFile(ruleFile), ruleFile)); } catch { continue; } } return configs; } function normalizeMainConfig(parsed: unknown): ConfigFile { if (!isRecord(parsed)) return {}; return { skillRoots: stringArray(parsed.skillRoots), extraSkillRoots: stringArray(parsed.extraSkillRoots), ruleRoots: stringArray(parsed.ruleRoots), discoveryCatalogs: stringArray(parsed.discoveryCatalogs), extraDiscoveryCatalogs: stringArray(parsed.extraDiscoveryCatalogs), requireAutoload: typeof parsed.requireAutoload === "boolean" ? parsed.requireAutoload : undefined, pathMode: normalizePathMode(parsed.pathMode), logPaths: typeof parsed.logPaths === "boolean" ? parsed.logPaths : undefined, scan: normalizeScanConfig(parsed.scan), debug: parsed.debug === true || undefined, }; } function readConfig(): ConfigFile { const configs: ConfigFile[] = []; for (const configPath of resolveConfigPaths()) { try { const parsed = parseConfigFile(configPath); const config = normalizeMainConfig(parsed); configs.push(config); configs.push(...readRuleConfigs(configPath, config)); } catch { continue; } } if (configs.length === 0) return {}; if (configs.length === 1) return configs[0]; return mergeConfigs(configs); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function collectConfigDiagnostics(): string[] { const diagnostics: string[] = []; for (const configPath of resolveConfigPaths()) { try { const parsed = parseConfigFile(configPath); const config = normalizeMainConfig(parsed); for (const ruleFile of listRuleConfigFiles(ruleRootsForConfig(configPath, config))) { try { normalizeRuleConfigFile(parseConfigFile(ruleFile), ruleFile); } catch (error) { diagnostics.push(`rule parse failed: ${ruleFile}: ${errorMessage(error)}`); } } } catch (error) { diagnostics.push(`config parse failed: ${configPath}: ${errorMessage(error)}`); } } return diagnostics; } function configuredRoots(config: ConfigFile): string[] { const envRoots = splitEnvList(process.env.CONTEXT_BROKER_ROOTS); const envExtraRoots = splitEnvList(process.env.CONTEXT_BROKER_EXTRA_ROOTS); const roots = [ ...(envRoots.length > 0 ? envRoots : config.skillRoots ?? []), ...(config.extraSkillRoots ?? []), ...envExtraRoots, ]; return uniqueStrings(roots.map((root) => expandPath(root))); } function configuredRules(config: ConfigFile): InvocationRule[] { const rules = config.rules ?? []; return uniqueRules([...DEFAULT_RULES, ...rules]) ?? DEFAULT_RULES; } function textFromContent(content: MessageContent | undefined): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter((part): part is TextContent => part.type === "text" && typeof part.text === "string") .map((part) => part.text) .join("\n"); } function latestUserText(messages: AgentMessage[]): string | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message.role !== "user") continue; const text = textFromContent(message.content).trim(); if (text) return text; } return undefined; } function shouldRequireAutoload(config: ConfigFile): boolean { const requireEnabled = process.env.CONTEXT_BROKER_REQUIRE_ENABLED; if (requireEnabled === "0") return false; if (requireEnabled === "1") return true; return config.requireAutoload !== false; } function scanOptions(config: ConfigFile): ScanConfig { return config.scan ?? {}; } async function buildRegistry(config = readConfig()): Promise { return buildRegistryFromRoots(configuredRoots(config), shouldRequireAutoload(config), scanOptions(config)); } function discoverUsableSkillRoots(config: ConfigFile = readConfig(), cwd = process.cwd()): string[] { return uniqueStrings(configuredRoots(config).map((root) => expandPath(root, cwd))); } async function buildDiscoveryRegistry(config: ConfigFile = readConfig(), cwd = process.cwd()): Promise { const skills = await buildRegistryFromRoots(discoverUsableSkillRoots(config, cwd), false, scanOptions(config)); return await buildDiscoveryRegistryFromParts(skills, configuredDiscoveryCatalogs(config, cwd), scanOptions(config)); } async function buildDollarRegistry(config: ConfigFile = readConfig(), cwd = process.cwd()): Promise { return await buildDiscoveryRegistry(config, cwd); } async function buildRawDollarRegistry(config: ConfigFile = readConfig(), cwd = process.cwd()): Promise { const skills = await buildRegistryFromRoots(discoverUsableSkillRoots(config, cwd), false, scanOptions(config)); return [ ...skills.map((skill) => ({ ...skill, kind: "skill" as const })), ...configuredDiscoveryCatalogs(config, cwd).flatMap((catalog) => { try { return readDiscoveryCatalog(catalog, skills, scanOptions(config)); } catch { return [] as DiscoveryRecord[]; } }), ]; } function compileRegex(pattern: RegexPattern): RegExp | undefined { try { return new RegExp(pattern.pattern, pattern.flags); } catch { return undefined; } } function predicateMatches(text: string, predicate: MatchPredicate): { matched: boolean; query?: string } { const [first] = predicateMatchResults(text, predicate); return first ?? { matched: false }; } function predicateMatchResults(text: string, predicate: MatchPredicate): Array<{ matched: true; query?: string }> { if (predicate.exact && !predicate.exact.some((value) => text.trim() === value.trim())) { return []; } if (predicate.contains && !predicate.contains.some((value) => text.includes(value))) { return []; } const results: Array<{ matched: true; query?: string }> = []; if (predicate.regex) { for (const pattern of predicate.regex) { const regex = compileRegex(pattern); if (!regex) continue; const global = regex.global; let match: RegExpExecArray | null; while ((match = regex.exec(text)) !== null) { const matchedQuery = match.groups?.query ?? match[1]; results.push({ matched: true, query: typeof matchedQuery === "string" && matchedQuery.trim() ? matchedQuery.trim() : undefined, }); if (!global) break; if (match[0] === "") regex.lastIndex += 1; } } if (results.length === 0) return []; } else { results.push({ matched: true }); } if (predicate.not?.some((negative) => predicateMatches(text, negative).matched)) { return []; } return results; } function parseInvocations(text: string, rules: InvocationRule[]): SkillRequest[] { const requests: SkillRequest[] = []; for (const rule of rules) { for (let index = 0; index < rule.match.length; index += 1) { const results = predicateMatchResults(text, rule.match[index]); if (results.length === 0) continue; for (const result of results) { const queries = rule.inject && rule.inject.length > 0 ? rule.inject : result.query ? [result.query] : []; for (const query of queries) { if (!query.trim()) continue; requests.push({ query: query.trim(), ruleId: rule.id, matchIndex: index, scope: rule.id === "dollar-skill" ? "global" : "configured", }); } } } } return requests; } function parseInvocation(text: string, rules: InvocationRule[]): { query: string; ruleId: string } | undefined { const [first] = parseInvocations(text, rules); return first ? { query: first.query, ruleId: first.ruleId } : undefined; } function matchDiscoveryRecord(query: string, registry: DiscoveryRecord[], allowFuzzy = false): MatchDecision { const normalized = normalizeKey(query); if (!normalized) return { decision: "skip", reason: "empty-query", query }; const candidates = registry.filter((record) => ( recordNormalizedName(record) === normalized || recordNormalizedDescription(record) === normalized || recordNormalizedAliases(record).includes(normalized) )); if (candidates.length === 1) { const record = candidates[0]; return { decision: "inject", reason: "unique-name-or-alias", query, record }; } if (candidates.length > 1) { return { decision: "skip", reason: "ambiguous", query, candidates }; } if (allowFuzzy) { const fuzzy = registry .map((record) => ({ record, score: scoreDollarSuggestion(record, query) })) .filter((item) => item.score >= 110) .sort((a, b) => b.score - a.score || recordName(a.record).localeCompare(recordName(b.record))); const [best, second] = fuzzy; if (best && (!second || best.score > second.score)) { return { decision: "inject", reason: "unique-fuzzy", query, record: best.record }; } if (best && second && best.score === second.score) { return { decision: "skip", reason: "ambiguous", query, candidates: fuzzy.filter((item) => item.score === best.score).map((item) => item.record) }; } } return { decision: "skip", reason: "not-found", query }; } function matchSkill(query: string, registry: SkillRecord[], allowFuzzy = false): MatchDecision { return matchDiscoveryRecord(query, registry, allowFuzzy); } function resolveSkillRequests( requests: SkillRequest[], registry: SkillRecord[], logExtra: Record = {}, globalRegistry: DiscoveryRecord[] = [], config: ConfigFile = {}, ): ContextInjection[] { const injections: ContextInjection[] = []; const byRecordKey = new Map(); for (const request of requests) { const decision = request.scope === "global" ? matchDiscoveryRecord(request.query, globalRegistry, true) : matchSkill(request.query, registry, false); if (decision.decision !== "inject") { logDecision(decision, { ...logExtra, ruleId: request.ruleId, matchIndex: request.matchIndex }, config); continue; } const recordKey = `${recordKind(decision.record)}:${resolve(recordPath(decision.record))}:${recordNormalizedName(decision.record)}`; const existing = byRecordKey.get(recordKey); if (existing) { existing.ruleIds = uniqueStrings([...existing.ruleIds, request.ruleId]); continue; } const injection = { record: decision.record, query: request.query, ruleIds: [request.ruleId] }; byRecordKey.set(recordKey, injection); injections.push(injection); } return injections; } function escapedPattern(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function messageTextIncludesRecordBlock(message: AgentMessage, record: DiscoveryRecord): boolean { const text = textFromContent(message.content); if (!text) return false; const namePatterns = [recordName(record), ...recordAliases(record)] .map(escapedPattern) .filter(Boolean); if (namePatterns.length === 0) return false; return namePatterns.some((name) => new RegExp(`]*name="${name}"`, "i").test(text)); } function messageTextIncludesSkillBlock(message: AgentMessage, skill: SkillRecord): boolean { return messageTextIncludesRecordBlock(message, skill); } function hasLoadedRecord(messages: AgentMessage[], record: DiscoveryRecord): boolean { const normalizedPath = resolve(recordPath(record)); const normalizedName = recordNormalizedName(record); const kind = recordKind(record); for (const message of messages) { const details = message.details && typeof message.details === "object" ? message.details as Record : {}; const detailName = typeof details.name === "string" ? normalizeKey(details.name) : ""; const detailKind = typeof details.kind === "string" ? details.kind : "skill"; const detailPath = typeof details.path === "string" ? resolve(details.path) : ""; const detailSourcePath = typeof details.sourcePath === "string" ? resolve(details.sourcePath) : ""; if (message.role === "custom" && message.customType === CUSTOM_TYPE) { const detailRecords = Array.isArray(details.records) ? details.records : []; if (detailRecords.some((item) => { if (!isRecord(item)) return false; const itemName = typeof item.name === "string" ? normalizeKey(item.name) : ""; const itemKind = typeof item.kind === "string" ? item.kind : "skill"; const itemPath = typeof item.path === "string" ? resolve(item.path) : ""; const itemSourcePath = typeof item.sourcePath === "string" ? resolve(item.sourcePath) : ""; return itemKind === kind && (itemName === normalizedName || itemPath === normalizedPath || itemSourcePath === normalizedPath); })) { return true; } if (detailKind === kind && (detailName === normalizedName || detailPath === normalizedPath || detailSourcePath === normalizedPath)) return true; } if (kind === "skill" && message.role === "custom" && message.customType === BUILTIN_SKILL_PROMPT_TYPE) { if (detailName === normalizedName || detailPath === normalizedPath) return true; } if (messageTextIncludesRecordBlock(message, record)) return true; } return false; } function hasLoadedSkill(messages: AgentMessage[], skill: SkillRecord): boolean { return hasLoadedRecord(messages, skill); } function messagesFromSessionEntries(entries: SessionEntry[] | undefined): AgentMessage[] { if (!entries) return []; return entries .map((entry): AgentMessage | undefined => { if (entry.type === "message" && entry.message) return entry.message; if (entry.type === "custom_message" || entry.type === "custom") { return { role: "custom", customType: entry.customType, content: entry.content, details: entry.details, }; } return undefined; }) .filter((message): message is AgentMessage => message !== undefined); } function messagesFromActiveSessionContext(ctx: ExtensionContext): AgentMessage[] { const activeMessages = ctx.sessionManager?.buildSessionContext?.()?.messages; if (Array.isArray(activeMessages)) return activeMessages; return messagesFromSessionEntries(ctx.sessionManager?.getEntries?.()); } function configuredPathMode(config: ConfigFile): PathMode { return config.pathMode ?? DEFAULT_PATH_MODE; } function hashPath(path: string): string { return `sha256:${createHash("sha256").update(resolve(path)).digest("hex").slice(0, 16)}`; } function formatPathForPayload(path: string, config: ConfigFile): string { const absolute = resolve(path); const mode = configuredPathMode(config); if (mode === "absolute") return absolute; if (mode === "basename") return basename(absolute); if (mode === "hash") return hashPath(absolute); const home = homeDir(); return absolute === home ? "~" : absolute.startsWith(`${home}/`) ? `~/${absolute.slice(home.length + 1)}` : absolute; } function recordDetail(injection: ContextInjection, config: ConfigFile): Record { return { kind: recordKind(injection.record), name: recordName(injection.record), path: formatPathForPayload(recordPath(injection.record), config), query: injection.query, ruleIds: injection.ruleIds, }; } function renderBundlePolicy(bundle: Extract): string[] { const policy = bundle.policy; if (!policy) return []; return [ ``, ...(Array.isArray(policy.prerequisites) && policy.prerequisites.length > 0 ? [ "", ...policy.prerequisites.map((member) => `${escapeText(String(member))}`), "", ] : []), ...(Array.isArray(policy.fallback) && policy.fallback.length > 0 ? [ "", ...policy.fallback.map((member) => `${escapeText(String(member))}`), "", ] : []), "", ]; } function renderBundleRecord(injection: ContextInjection, config: ConfigFile): string[] { const bundle = injection.record as Extract; const defaultRules = [ "This is a context bundle index, not a concrete skill.", "Select the needed member by name/description before acting.", "Read the selected member SKILL.md before using that member capability.", "Member descriptions are routing metadata, not full operating instructions.", ]; const rules = bundle.render?.rules && bundle.render.rules.length > 0 ? bundle.render.rules : defaultRules; return [ ``, ``, `${escapeText(bundle.description)}`, "", ...rules.map((rule) => `${escapeText(rule)}`), "", ...renderBundlePolicy(bundle), "", ...bundle.members.map((member) => `${escapeText(member.description ?? "")}`), "", "", ]; } function renderSkillRecord(injection: ContextInjection, config: ConfigFile): string[] { const skill = injection.record as SkillRecord; return [ ``, ``, "", escapeText(skill.body), "", "", ]; } function buildInjectedPayloadFromInjections(injections: ContextInjection[], config: ConfigFile = {}): Omit { const records = injections.map((injection) => recordDetail(injection, config)); return { customType: CUSTOM_TYPE, content: injections.flatMap((injection) => recordKind(injection.record) === "bundle" ? renderBundleRecord(injection, config) : renderSkillRecord(injection, config)).join("\n"), display: false, details: { ...(records.length === 1 ? records[0] : {}), records, injectedBy: CUSTOM_TYPE, version: 3, }, }; } function buildInjectedPayload(skill: SkillRecord, query: string, config: ConfigFile = {}): Omit { return buildInjectedPayloadFromInjections([{ record: skill, query, ruleIds: [] }], config); } function buildInjectedContextMessageFromInjections(injections: ContextInjection[], config: ConfigFile = {}): AgentMessage { return { role: "custom", ...buildInjectedPayloadFromInjections(injections, config), timestamp: Date.now(), }; } function buildInjectedContextMessage(skill: SkillRecord, query: string, config: ConfigFile = {}): AgentMessage { return buildInjectedContextMessageFromInjections([{ record: skill, query, ruleIds: [] }], config); } function escapeAttribute(value: string): string { return value.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } function logRecord(record: DiscoveryRecord, config: ConfigFile): Record { return { kind: recordKind(record), name: recordName(record), ...(config.logPaths === true ? { path: formatPathForPayload(recordPath(record), config) } : {}), }; } function logDecision(decision: MatchDecision | { decision: "skip"; reason: string }, extra: Record = {}, config: ConfigFile = {}): void { const logFile = process.env.CONTEXT_BROKER_LOG_FILE; if (!logFile) return; try { appendFileSync(logFile, `${JSON.stringify({ timestamp: new Date().toISOString(), decision: decision.decision, reason: decision.reason, query: "query" in decision ? decision.query : undefined, record: "record" in decision ? logRecord(decision.record, config) : undefined, candidates: "candidates" in decision ? decision.candidates?.map((record) => logRecord(record, config)) : undefined, ...extra, })}\n`); } catch { // Debug logging must never affect prompt execution. } } function logInjection(injections: ContextInjection[], extra: Record = {}, config: ConfigFile = {}): void { const logFile = process.env.CONTEXT_BROKER_LOG_FILE; if (!logFile || injections.length === 0) return; try { appendFileSync(logFile, `${JSON.stringify({ timestamp: new Date().toISOString(), decision: "inject", reason: "matched", query: injections.map((injection) => injection.query).join(", "), record: logRecord(injections[0].record, config), records: injections.map((injection) => ({ ...logRecord(injection.record, config), query: injection.query, ruleIds: injection.ruleIds, })), ...extra, })}\n`); } catch { // Debug logging must never affect prompt execution. } } function notifyInjectedSkill(ctx: ExtensionContext, skill: SkillRecord): void { notifyInjectedRecords(ctx, [skill]); } function notifyInjectedRecords(ctx: ExtensionContext, records: DiscoveryRecord[], alreadyLoaded: DiscoveryRecord[] = []): void { if (!ctx.hasUI) return; const parts = []; if (records.length > 0) parts.push(`injected ${records.map((record) => recordName(record)).join(", ")}`); if (alreadyLoaded.length > 0) parts.push(`already loaded ${alreadyLoaded.map((record) => recordName(record)).join(", ")}`); if (parts.length === 0) return; try { ctx.ui?.notify?.(`context-broker: ${parts.join("; ")}`, "info"); } catch { // UI notifications must never affect prompt execution. } } function notifyInjectedSkills(ctx: ExtensionContext, skills: SkillRecord[], alreadyLoaded: SkillRecord[] = []): void { notifyInjectedRecords(ctx, skills, alreadyLoaded); } async function invokeForContext( event: ContextEvent, registryPromise: Promise, config: ConfigFile, dollarRegistryPromise: Promise = Promise.resolve([]), ): Promise { const prompt = latestUserText(event.messages); if (!prompt) return undefined; const requests = parseInvocations(prompt, configuredRules(config)); if (requests.length === 0) return undefined; const registry = await registryPromise; const dollarRegistry = requests.some((request) => request.scope === "global") ? await dollarRegistryPromise : []; const injections = resolveSkillRequests(requests, registry, {}, dollarRegistry, config); const unloaded = injections.filter((injection) => { if (!hasLoadedRecord(event.messages, injection.record)) return true; logDecision( { decision: "skip", reason: "already-loaded", query: injection.query, candidates: [injection.record] }, { ruleIds: injection.ruleIds }, config, ); return false; }); if (unloaded.length === 0) { return undefined; } const message = buildInjectedContextMessageFromInjections(unloaded, config); logInjection(unloaded, { action: "inject" }, config); return { messages: [...event.messages, message] }; } async function invokeBeforeAgentStart( event: BeforeAgentStartEvent, ctx: ExtensionContext, registryPromise: Promise, config: ConfigFile, dollarRegistryPromise: Promise = Promise.resolve([]), ): Promise { const requests = parseInvocations(event.prompt, configuredRules(config)); if (requests.length === 0) return undefined; const registry = await registryPromise; const dollarRegistry = requests.some((request) => request.scope === "global") ? await buildDollarRegistryForContext(ctx, dollarRegistryPromise) : []; const injections = resolveSkillRequests(requests, registry, { phase: "before_agent_start" }, dollarRegistry, config); const sessionMessages = messagesFromActiveSessionContext(ctx); const alreadyLoaded: ContextInjection[] = []; const unloaded = injections.filter((injection) => { if (!hasLoadedRecord(sessionMessages, injection.record)) return true; alreadyLoaded.push(injection); logDecision( { decision: "skip", reason: "already-loaded", query: injection.query, candidates: [injection.record] }, { ruleIds: injection.ruleIds, phase: "before_agent_start" }, config, ); return false; }); if (unloaded.length === 0) { notifyInjectedRecords(ctx, [], alreadyLoaded.map((injection) => injection.record)); return undefined; } const message = buildInjectedPayloadFromInjections(unloaded, config); logInjection(unloaded, { phase: "before_agent_start", action: "inject" }, config); notifyInjectedRecords( ctx, unloaded.map((injection) => injection.record), alreadyLoaded.map((injection) => injection.record), ); return { message }; } function contextBrokerCommandCompletions(argumentPrefix: string) { const commands = [ { value: "status", label: "status", description: "Show loaded discovery record summary" }, { value: "doctor", label: "doctor", description: "Diagnose config, catalogs, names, aliases, and bundles" }, { value: "roots", label: "roots", description: "Show skill roots" }, { value: "catalogs", label: "catalogs", description: "Show discovery catalogs" }, { value: "find ", label: "find", description: "Explain lookup for a query" }, { value: "explain ", label: "explain", description: "Show resolved record and injection strategy" }, ]; const prefix = argumentPrefix.trimStart(); return commands.filter((command) => command.value.startsWith(prefix) || command.label.startsWith(prefix)); } function formatCatalogs(config: ConfigFile, cwd = process.cwd()): string { const catalogs = configuredDiscoveryCatalogs(config, cwd); if (catalogs.length === 0) return "context-broker catalogs: none"; return [ `context-broker catalogs: ${catalogs.length}`, ...catalogs.map((catalog) => `${existsSync(catalog) ? "ok" : "missing"} ${catalog}`), ].join("\n"); } async function formatContextBrokerDoctor(config: ConfigFile, cwd = process.cwd()): Promise { const roots = discoverUsableSkillRoots(config, cwd); const catalogs = configuredDiscoveryCatalogs(config, cwd); const rawRegistry = await buildRawDollarRegistry(config, cwd); const errors = validateDiscoveryRecords(rawRegistry); const diagnostics = collectConfigDiagnostics(); const bundles = rawRegistry.filter((record) => recordKind(record) === "bundle"); const skills = rawRegistry.filter((record) => recordKind(record) === "skill"); for (const catalog of catalogs) { try { readDiscoveryCatalog(catalog, skills as SkillRecord[], scanOptions(config)); } catch (error) { diagnostics.push(`catalog read failed: ${catalog}: ${errorMessage(error)}`); } } return [ "Context Broker Doctor", "", `Config: ${resolveConfigPath() ?? "none"}`, `Path mode: ${configuredPathMode(config)}`, `Log paths: ${config.logPaths === true ? "enabled" : "disabled"}`, `Scan: maxDepth=${config.scan?.maxDepth ?? 8}, maxSkillBytes=${config.scan?.maxSkillBytes ?? 65536}`, `Skill roots: ${roots.length}`, `Discovery catalogs: ${catalogs.length}`, ...catalogs.map((catalog) => ` ${existsSync(catalog) ? "ok" : "missing"} ${catalog}`), `Records: ${skills.length} skills, ${bundles.length} bundles`, diagnostics.length === 0 ? "Config diagnostics: ok" : "Config diagnostics: failed", ...diagnostics.map((diagnostic) => ` fail ${diagnostic}`), errors.length === 0 ? "Namespace: ok no discovery namespace collisions" : "Namespace: failed", ...errors.map((error) => ` fail ${error}`), "Bundles:", ...(bundles.length > 0 ? bundles.map((record) => ` ${recordName(record)}: ${(record as any).members?.length ?? 0} members`) : [" none"]), ].join("\n"); } function formatExplainRecord(record: DiscoveryRecord): string { if (recordKind(record) === "bundle") { const bundle = record as Extract; return [ `record: ${bundle.name}`, "kind: bundle", "inject: member-index", `members: ${bundle.members.length}`, `path: ${bundle.path}`, bundle.description ? `description: ${bundle.description}` : undefined, ].filter(Boolean).join("\n"); } return [ `record: ${recordName(record)}`, "kind: skill", "inject: full-file", `path: ${recordPath(record)}`, recordDescription(record) ? `description: ${recordDescription(record)}` : undefined, ].filter(Boolean).join("\n"); } async function dispatchContextBrokerCommand(args: string, ctx: ExtensionContext, config: ConfigFile, getDollarRegistry: (cwd?: string) => Promise, registryPromise: Promise): Promise { const [command = "status", ...rest] = args.trim().split(/\s+/).filter(Boolean); if (command === "roots") { const roots = discoverUsableSkillRoots(config, ctx.cwd); ctx.ui?.notify?.(formatRootDiagnostics(await collectRootDiagnostics(roots)), "info"); return; } if (command === "catalogs") { ctx.ui?.notify?.(formatCatalogs(config, ctx.cwd), "info"); return; } if (command === "doctor") { ctx.ui?.notify?.(await formatContextBrokerDoctor(config, ctx.cwd), "info"); return; } if (command === "find") { const query = rest.join(" "); const registry = await buildDollarRegistryForContext(ctx, getDollarRegistry(ctx.cwd)); const rawRegistry = await buildRawDollarRegistry(config, ctx.cwd); ctx.ui?.notify?.(formatFindDiagnostics(query, registry, rawRegistry), "info"); return; } if (command === "explain") { const query = rest.join(" "); const registry = await buildDollarRegistryForContext(ctx, getDollarRegistry(ctx.cwd)); const decision = matchDiscoveryRecord(query, registry, false); ctx.ui?.notify?.(decision.decision === "inject" ? formatExplainRecord(decision.record) : `context-broker explain: no unique record for ${query}`, decision.decision === "inject" ? "info" : "warning"); return; } const registry = await registryPromise; const dollarRegistry = await getDollarRegistry(ctx.cwd); const bundles = dollarRegistry.filter((record) => recordKind(record) === "bundle").length; ctx.ui?.notify?.( `context-broker status: ${registry.length} configured skills, ${dollarRegistry.length} $ records, ${bundles} bundles\ncommands: doctor, roots, catalogs, find , explain `, registry.length > 0 || dollarRegistry.length > 0 ? "info" : "warning", ); } export default function contextBroker(pi: ExtensionAPI): void { const config = readConfig(); const registryPromise = buildRegistry(config); const dollarRegistryByCwd = new Map>(); const getDollarRegistry = (cwd = process.cwd()) => { const key = resolve(cwd); let promise = dollarRegistryByCwd.get(key); if (!promise) { promise = buildDollarRegistry(config, key); dollarRegistryByCwd.set(key, promise); } return promise; }; pi.on("session_start", async (_event, ctx) => { await installPiDollarAutocompleteEditor(ctx); ctx.ui?.addAutocompleteProvider?.((current) => createDollarSkillAutocompleteProvider( current, buildDollarRegistryForContext(ctx, getDollarRegistry(ctx.cwd)), )); }); pi.on("before_agent_start", (event, ctx) => invokeBeforeAgentStart(event, ctx, registryPromise, config, getDollarRegistry(ctx.cwd))); const command = { description: "Inspect context-broker discovery records, bundles, catalogs, and config health", getArgumentCompletions: contextBrokerCommandCompletions, handler: (args: string, ctx: ExtensionContext) => dispatchContextBrokerCommand(args, ctx, config, getDollarRegistry, registryPromise), }; pi.registerCommand?.("context-broker", command); } export { CUSTOM_TYPE, BUILTIN_SKILL_PROMPT_TYPE, buildInjectedContextMessage, buildInjectedPayload, buildDiscoveryRegistry, buildDollarRegistry, buildRawDollarRegistry, buildRegistry, buildRegistryFromSystemPrompt, createDollarSkillAutocompleteProvider, defaultAgentDir, defaultAgentDirs, discoverUsableSkillRoots, defaultConfigPath, detectHost, discoveryAutocompleteItems, dollarAutocompleteItems, extractDollarAutocompletePrefix, hasLoadedSkill, homeDir, invokeBeforeAgentStart, invokeForContext, matchDiscoveryRecord, matchSkill, messagesFromSessionEntries, normalizeKey, normalizeRuleConfigFile, notifyInjectedSkill, parseConfigFile, readDiscoveryCatalog, parseInvocation, parseInvocations, readConfig, resolveConfigPath, resolveConfigPaths, configuredRules, ruleRootsForConfig, listRuleConfigFiles, };