From 4e5af01d73a09e78244029d766c352971b515398 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 23 Mar 2026 02:02:42 +0100 Subject: [PATCH] fix(coding-agent): unify source provenance, closes #1734 --- packages/coding-agent/CHANGELOG.md | 23 ++++++ packages/coding-agent/docs/extensions.md | 33 +++++++-- .../examples/extensions/commands.ts | 6 +- .../coding-agent/examples/sdk/04-skills.ts | 10 ++- .../examples/sdk/08-prompt-templates.ts | 3 +- .../coding-agent/src/core/agent-session.ts | 61 +++++++++------- .../src/core/export-html/index.ts | 4 +- .../coding-agent/src/core/extensions/index.ts | 2 +- .../src/core/extensions/loader.ts | 13 +++- .../coding-agent/src/core/extensions/types.ts | 17 ++--- packages/coding-agent/src/core/index.ts | 2 +- .../coding-agent/src/core/prompt-templates.ts | 65 ++++++++--------- .../coding-agent/src/core/resource-loader.ts | 26 ++++--- packages/coding-agent/src/core/sdk.ts | 1 - packages/coding-agent/src/core/skills.ts | 31 ++++++-- .../coding-agent/src/core/slash-commands.ts | 6 +- packages/coding-agent/src/core/source-info.ts | 29 ++++++-- packages/coding-agent/src/index.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 2 +- .../coding-agent/src/modes/rpc/rpc-mode.ts | 11 +-- .../coding-agent/src/modes/rpc/rpc-types.ts | 7 +- .../test/agent-session-dynamic-tools.test.ts | 61 +++++++++++++++- .../test/compaction-extensions.test.ts | 5 ++ .../coding-agent/test/resource-loader.test.ts | 3 +- packages/coding-agent/test/sdk-skills.test.ts | 3 +- packages/coding-agent/test/skills.test.ts | 71 ++++++++++--------- 26 files changed, 340 insertions(+), 157 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9f34ac76..80cd0b89 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,14 +5,37 @@ ### Breaking Changes - Changed `ToolDefinition.renderCall` and `renderResult` semantics. Fallback rendering now happens only when a renderer is not defined for that slot. If `renderCall` or `renderResult` is defined, it must return a `Component`. +- Changed slash command provenance to use `sourceInfo` consistently. RPC `get_commands`, `RpcSlashCommand`, and SDK `SlashCommandInfo` no longer expose `location` or `path`. Use `sourceInfo` instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) +- Removed legacy `source` fields from `Skill` and `PromptTemplate`. Use `sourceInfo.source` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) +- Removed `ResourceLoader.getPathMetadata()`. Resource provenance is now attached directly to loaded resources via `sourceInfo` ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) +- Removed `extensionPath` from `RegisteredCommand` and `RegisteredTool`. Use `sourceInfo.path` for provenance instead ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) + +#### Migration Notes + +Resource, command, and tool provenance now use `sourceInfo` consistently. + +Common updates: +- RPC `get_commands`: replace `path` and `location` with `sourceInfo.path`, `sourceInfo.scope`, and `sourceInfo.source` +- `SlashCommandInfo`: replace `command.path` and `command.location` with `command.sourceInfo` +- `Skill` and `PromptTemplate`: replace `.source` with `.sourceInfo.source` +- `RegisteredCommand` and `RegisteredTool`: replace `.extensionPath` with `.sourceInfo.path` +- Custom `ResourceLoader` implementations: remove `getPathMetadata()` and read provenance from loaded resources directly + +Examples: +- `command.path` -> `command.sourceInfo.path` +- `command.location === "user"` -> `command.sourceInfo.scope === "user"` +- `skill.source` -> `skill.sourceInfo.source` +- `tool.extensionPath` -> `tool.sourceInfo.path` ### Changed - Built-in tools now work like custom tools in extensions. To get built-in tool definitions, import `readToolDefinition` / `createReadToolDefinition()` and the equivalent `bash`, `edit`, `write`, `grep`, `find`, and `ls` exports from `@mariozechner/pi-coding-agent`. - Cleaned up `buildSystemPrompt()` so built-in tool snippets and tool-local guidelines come from built-in `ToolDefinition` metadata, while cross-tool and global prompt rules stay in system prompt construction. +- Added structured `sourceInfo` to `pi.getAllTools()` results for built-in, SDK, and extension tools ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) ### Fixed +- Fixed slash command source attribution for extension commands, prompt templates, and skills in autocomplete and command discovery ([#1734](https://github.com/badlogic/pi-mono/issues/1734)) - Fixed auto-resized image handling to enforce the inline image size limit on the final base64 payload, return text-only fallbacks when resizing cannot produce a safe image, and avoid falling back to the original image in `read` and `@file` auto-resize paths ([#2055](https://github.com/badlogic/pi-mono/issues/2055)) - Fixed `pi update` for git packages to skip destructive reset, clean, and reinstall steps when the fetched target already matches the local checkout ([#2503](https://github.com/badlogic/pi-mono/issues/2503)) - Fixed print and JSON mode to take over stdout during non-interactive startup, keeping package-manager and other incidental chatter off protocol/output stdout ([#2482](https://github.com/badlogic/pi-mono/issues/2482)) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 8ca32c08..a693d763 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1126,6 +1126,7 @@ The list matches the RPC `get_commands` ordering: extensions first, then templat ```typescript const commands = pi.getCommands(); const bySource = commands.filter((command) => command.source === "extension"); +const userScoped = commands.filter((command) => command.sourceInfo.scope === "user"); ``` Each entry has this shape: @@ -1135,11 +1136,18 @@ Each entry has this shape: name: string; // Command name without the leading slash description?: string; source: "extension" | "prompt" | "skill"; - location?: "user" | "project" | "path"; // For templates and skills - path?: string; // Files backing templates, skills, and extensions + sourceInfo: { + path: string; + source: string; + scope: "user" | "project" | "temporary"; + origin: "package" | "top-level"; + baseDir?: string; + }; } ``` +Use `sourceInfo` as the canonical provenance field. Do not infer ownership from command names or from ad hoc path parsing. + Built-in interactive commands (like `/model` and `/settings`) are not included here. They are handled only in interactive mode and would not execute if sent via `prompt`. @@ -1191,12 +1199,27 @@ const result = await pi.exec("git", ["status"], { signal, timeout: 5000 }); Manage active tools. This works for both built-in tools and dynamically registered tools. ```typescript -const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"] -const all = pi.getAllTools(); // [{ name: "read", description: "Read file contents..." }, ...] -const names = all.map(t => t.name); // Just names if needed +const active = pi.getActiveTools(); +const all = pi.getAllTools(); +// [{ +// name: "read", +// description: "Read file contents...", +// parameters: ..., +// sourceInfo: { path: "", source: "builtin", scope: "temporary", origin: "top-level" } +// }, ...] +const names = all.map(t => t.name); +const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin"); +const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk"); pi.setActiveTools(["read", "bash"]); // Switch to read-only ``` +`pi.getAllTools()` returns `name`, `description`, `parameters`, and `sourceInfo`. + +Typical `sourceInfo.source` values: +- `builtin` for built-in tools +- `sdk` for tools passed via `createAgentSession({ customTools })` +- extension source metadata for tools registered by extensions + ### pi.setModel(model) Set the current model. Returns `false` if no API key is available for the model. See [models.md](models.md) for configuring custom models. diff --git a/packages/coding-agent/examples/extensions/commands.ts b/packages/coding-agent/examples/extensions/commands.ts index 8f25b65c..0784c7a1 100644 --- a/packages/coding-agent/examples/extensions/commands.ts +++ b/packages/coding-agent/examples/extensions/commands.ts @@ -60,10 +60,10 @@ export default function commandsExtension(pi: ExtensionAPI) { if (selected && !selected.startsWith("---")) { const cmdName = selected.split(" - ")[0].slice(1); // Remove leading / const cmd = commands.find((c) => c.name === cmdName); - if (cmd?.path) { - const showPath = await ctx.ui.confirm(cmd.name, `View source path?\n${cmd.path}`); + if (cmd?.sourceInfo.path) { + const showPath = await ctx.ui.confirm(cmd.name, `View source path?\n${cmd.sourceInfo.path}`); if (showPath) { - ctx.ui.notify(cmd.path, "info"); + ctx.ui.notify(cmd.sourceInfo.path, "info"); } } } diff --git a/packages/coding-agent/examples/sdk/04-skills.ts b/packages/coding-agent/examples/sdk/04-skills.ts index 0e7aa7d0..9ed9696b 100644 --- a/packages/coding-agent/examples/sdk/04-skills.ts +++ b/packages/coding-agent/examples/sdk/04-skills.ts @@ -5,7 +5,13 @@ * Discover, filter, merge, or replace them. */ -import { createAgentSession, DefaultResourceLoader, SessionManager, type Skill } from "@mariozechner/pi-coding-agent"; +import { + createAgentSession, + createSyntheticSourceInfo, + DefaultResourceLoader, + SessionManager, + type Skill, +} from "@mariozechner/pi-coding-agent"; // Or define custom skills inline const customSkill: Skill = { @@ -13,7 +19,7 @@ const customSkill: Skill = { description: "Custom project instructions", filePath: "/virtual/SKILL.md", baseDir: "/virtual", - source: "path", + sourceInfo: createSyntheticSourceInfo("/virtual/SKILL.md", { source: "sdk" }), disableModelInvocation: false, }; diff --git a/packages/coding-agent/examples/sdk/08-prompt-templates.ts b/packages/coding-agent/examples/sdk/08-prompt-templates.ts index 1926846b..b47aebb7 100644 --- a/packages/coding-agent/examples/sdk/08-prompt-templates.ts +++ b/packages/coding-agent/examples/sdk/08-prompt-templates.ts @@ -6,6 +6,7 @@ import { createAgentSession, + createSyntheticSourceInfo, DefaultResourceLoader, type PromptTemplate, SessionManager, @@ -15,8 +16,8 @@ import { const deployTemplate: PromptTemplate = { name: "deploy", description: "Deploy the application", - source: "path", filePath: "/virtual/prompts/deploy.md", + sourceInfo: createSyntheticSourceInfo("/virtual/prompts/deploy.md", { source: "sdk" }), content: `# Deploy Instructions 1. Build: npm run build diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index b124b815..c1c2d09b 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -75,7 +75,8 @@ import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.j import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js"; import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js"; import type { SettingsManager } from "./settings-manager.js"; -import type { SlashCommandInfo, SlashCommandLocation } from "./slash-commands.js"; +import type { SlashCommandInfo } from "./slash-commands.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; import { buildSystemPrompt } from "./system-prompt.js"; import type { BashOperations } from "./tools/bash.js"; import { createAllToolDefinitions } from "./tools/index.js"; @@ -201,6 +202,11 @@ export interface SessionStats { cost: number; } +interface ToolDefinitionEntry { + definition: ToolDefinition; + sourceInfo: SourceInfo; +} + // ============================================================================ // Constants // ============================================================================ @@ -274,7 +280,7 @@ export class AgentSession { // Tool registry for extension getTools/setTools private _toolRegistry: Map = new Map(); - private _toolDefinitions: Map = new Map(); + private _toolDefinitions: Map = new Map(); private _toolPromptSnippets: Map = new Map(); private _toolPromptGuidelines: Map = new Map(); @@ -710,18 +716,19 @@ export class AgentSession { } /** - * Get all configured tools with name, description, and parameter schema. + * Get all configured tools with name, description, parameter schema, and source metadata. */ getAllTools(): ToolInfo[] { - return Array.from(this._toolDefinitions.values()).map((t) => ({ - name: t.name, - description: t.description, - parameters: t.parameters, + return Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({ + name: definition.name, + description: definition.description, + parameters: definition.parameters, + sourceInfo, })); } getToolDefinition(name: string): ToolDefinition | undefined { - return this._toolDefinitions.get(name); + return this._toolDefinitions.get(name)?.definition; } /** @@ -2084,20 +2091,12 @@ export class AgentSession { } private _bindExtensionCore(runner: ExtensionRunner): void { - const normalizeLocation = (source: string): SlashCommandLocation | undefined => { - if (source === "user" || source === "project" || source === "path") { - return source; - } - return undefined; - }; - const getCommands = (): SlashCommandInfo[] => { const extensionCommands: SlashCommandInfo[] = runner.getRegisteredCommands().map((command) => ({ name: command.name, description: command.description, source: "extension", sourceInfo: command.sourceInfo, - path: command.extensionPath, })); const templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({ @@ -2105,8 +2104,6 @@ export class AgentSession { description: template.description, source: "prompt", sourceInfo: template.sourceInfo, - location: normalizeLocation(template.source), - path: template.filePath, })); const skills: SlashCommandInfo[] = this._resourceLoader.getSkills().skills.map((skill) => ({ @@ -2114,8 +2111,6 @@ export class AgentSession { description: skill.description, source: "skill", sourceInfo: skill.sourceInfo, - location: normalizeLocation(skill.source), - path: skill.filePath, })); return [...extensionCommands, ...templates, ...skills]; @@ -2209,16 +2204,30 @@ export class AgentSession { const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? []; const allCustomTools = [ ...registeredTools, - ...this._customTools.map((def) => ({ definition: def, extensionPath: "" })), + ...this._customTools.map((definition) => ({ + definition, + sourceInfo: createSyntheticSourceInfo(``, { source: "sdk" }), + })), ]; - const definitionRegistry = new Map(this._baseToolDefinitions); - for (const { definition } of allCustomTools) { - definitionRegistry.set(definition.name, definition); + const definitionRegistry = new Map( + Array.from(this._baseToolDefinitions.entries()).map(([name, definition]) => [ + name, + { + definition, + sourceInfo: createSyntheticSourceInfo(``, { source: "builtin" }), + }, + ]), + ); + for (const tool of allCustomTools) { + definitionRegistry.set(tool.definition.name, { + definition: tool.definition, + sourceInfo: tool.sourceInfo, + }); } this._toolDefinitions = definitionRegistry; this._toolPromptSnippets = new Map( Array.from(definitionRegistry.values()) - .map((definition) => { + .map(({ definition }) => { const snippet = this._normalizePromptSnippet(definition.promptSnippet); return snippet ? ([definition.name, snippet] as const) : undefined; }) @@ -2226,7 +2235,7 @@ export class AgentSession { ); this._toolPromptGuidelines = new Map( Array.from(definitionRegistry.values()) - .map((definition) => { + .map(({ definition }) => { const guidelines = this._normalizePromptGuidelines(definition.promptGuidelines); return guidelines.length > 0 ? ([definition.name, guidelines] as const) : undefined; }) diff --git a/packages/coding-agent/src/core/export-html/index.ts b/packages/coding-agent/src/core/export-html/index.ts index 7ead340d..94af061b 100644 --- a/packages/coding-agent/src/core/export-html/index.ts +++ b/packages/coding-agent/src/core/export-html/index.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.js"; import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js"; -import type { ToolInfo } from "../extensions/types.js"; +import type { ToolDefinition } from "../extensions/types.js"; import type { SessionEntry } from "../session-manager.js"; import { SessionManager } from "../session-manager.js"; @@ -131,7 +131,7 @@ interface SessionData { entries: ReturnType; leafId: string | null; systemPrompt?: string; - tools?: ToolInfo[]; + tools?: Array>; /** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */ renderedTools?: Record; } diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 4535dac4..e0826562 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -2,7 +2,7 @@ * Extension system for lifecycle events and custom tools. */ -export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from "../slash-commands.js"; +export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js"; export type { SourceInfo } from "../source-info.js"; export { createExtensionRuntime, diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 1afff56d..667a1d7d 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -26,6 +26,7 @@ import * as _bundledPiCodingAgent from "../../index.js"; import { createEventBus, type EventBus } from "../event-bus.js"; import type { ExecOptions } from "../exec.js"; import { execCommand } from "../exec.js"; +import { createSyntheticSourceInfo } from "../source-info.js"; import type { Extension, ExtensionAPI, @@ -174,15 +175,14 @@ function createExtensionAPI( registerTool(tool: ToolDefinition): void { extension.tools.set(tool.name, { definition: tool, - extensionPath: extension.path, + sourceInfo: extension.sourceInfo, }); runtime.refreshTools(); }, - registerCommand(name: string, options: Omit): void { + registerCommand(name: string, options: Omit): void { extension.commands.set(name, { name, - extensionPath: extension.path, sourceInfo: extension.sourceInfo, ...options, }); @@ -307,9 +307,16 @@ async function loadExtensionModule(extensionPath: string) { * Create an Extension object with empty collections. */ function createExtension(extensionPath: string, resolvedPath: string): Extension { + const source = + extensionPath.startsWith("<") && extensionPath.endsWith(">") + ? extensionPath.slice(1, -1).split(":")[0] || "temporary" + : "local"; + const baseDir = extensionPath.startsWith("<") ? undefined : path.dirname(resolvedPath); + return { path: extensionPath, resolvedPath, + sourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }), handlers: new Map(), tools: new Map(), messageRenderers: new Map(), diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 9102f965..6ac089f5 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -961,8 +961,7 @@ export type MessageRenderer = ( export interface RegisteredCommand { name: string; - extensionPath: string; - sourceInfo?: SourceInfo; + sourceInfo: SourceInfo; description?: string; getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null; handler: (args: string, ctx: ExtensionCommandContext) => Promise; @@ -1038,7 +1037,7 @@ export interface ExtensionAPI { // ========================================================================= /** Register a custom command. */ - registerCommand(name: string, options: Omit): void; + registerCommand(name: string, options: Omit): void; /** Register a keyboard shortcut. */ registerShortcut( @@ -1110,7 +1109,7 @@ export interface ExtensionAPI { /** Get the list of currently active tool names. */ getActiveTools(): string[]; - /** Get all configured tools with name and description. */ + /** Get all configured tools with parameter schema and source metadata. */ getAllTools(): ToolInfo[]; /** Set the active tools by name. */ @@ -1277,7 +1276,7 @@ export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise; export interface RegisteredTool { definition: ToolDefinition; - extensionPath: string; + sourceInfo: SourceInfo; } export interface ExtensionFlag { @@ -1315,8 +1314,10 @@ export type GetSessionNameHandler = () => string | undefined; export type GetActiveToolsHandler = () => string[]; -/** Tool info with name, description, and parameter schema */ -export type ToolInfo = Pick; +/** Tool info with name, description, parameter schema, and source metadata */ +export type ToolInfo = Pick & { + sourceInfo: SourceInfo; +}; export type GetAllToolsHandler = () => ToolInfo[]; @@ -1417,7 +1418,7 @@ export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionAction export interface Extension { path: string; resolvedPath: string; - sourceInfo?: SourceInfo; + sourceInfo: SourceInfo; handlers: Map; tools: Map; messageRenderers: Map; diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index dd47b8d1..74b56744 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -14,7 +14,6 @@ export { export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations } from "./bash-executor.js"; export type { CompactionResult } from "./compaction/index.js"; export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js"; - // Extensions system export { type AgentEndEvent, @@ -59,3 +58,4 @@ export { type TurnEndEvent, type TurnStartEvent, } from "./extensions/index.js"; +export { createSyntheticSourceInfo } from "./source-info.js"; diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index fa0a8774..7c5dbc3f 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -1,9 +1,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "fs"; import { homedir } from "os"; -import { basename, isAbsolute, join, resolve, sep } from "path"; +import { basename, dirname, isAbsolute, join, resolve, sep } from "path"; import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js"; import { parseFrontmatter } from "../utils/frontmatter.js"; -import type { SourceInfo } from "./source-info.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; /** * Represents a prompt template loaded from a markdown file @@ -12,8 +12,7 @@ export interface PromptTemplate { name: string; description: string; content: string; - source: string; // "user", "project", or "path" - sourceInfo?: SourceInfo; + sourceInfo: SourceInfo; filePath: string; // Absolute path to the template file } @@ -101,7 +100,7 @@ export function substituteArgs(content: string, args: string[]): string { return result; } -function loadTemplateFromFile(filePath: string, source: string): PromptTemplate | null { +function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptTemplate | null { try { const rawContent = readFileSync(filePath, "utf-8"); const { frontmatter, body } = parseFrontmatter>(rawContent); @@ -123,7 +122,7 @@ function loadTemplateFromFile(filePath: string, source: string): PromptTemplate name, description, content: body, - source, + sourceInfo, filePath, }; } catch { @@ -134,7 +133,7 @@ function loadTemplateFromFile(filePath: string, source: string): PromptTemplate /** * Scan a directory for .md files (non-recursive) and load them as prompt templates. */ -function loadTemplatesFromDir(dir: string, source: string): PromptTemplate[] { +function loadTemplatesFromDir(dir: string, getSourceInfo: (filePath: string) => SourceInfo): PromptTemplate[] { const templates: PromptTemplate[] = []; if (!existsSync(dir)) { @@ -160,7 +159,7 @@ function loadTemplatesFromDir(dir: string, source: string): PromptTemplate[] { } if (isFile && entry.name.endsWith(".md")) { - const template = loadTemplateFromFile(fullPath, source); + const template = loadTemplateFromFile(fullPath, getSourceInfo(fullPath)); if (template) { templates.push(template); } @@ -211,18 +210,7 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P const templates: PromptTemplate[] = []; - if (includeDefaults) { - // 1. Load global templates from agentDir/prompts/ - // Note: if agentDir is provided, it should be the agent dir, not the prompts dir - const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; - templates.push(...loadTemplatesFromDir(globalPromptsDir, "user")); - - // 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/ - const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); - templates.push(...loadTemplatesFromDir(projectPromptsDir, "project")); - } - - const userPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; + const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); const isUnderPath = (target: string, root: string): boolean => { @@ -234,18 +222,32 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P return target.startsWith(prefix); }; - const getSource = (resolvedPath: string): string => { - if (!includeDefaults) { - if (isUnderPath(resolvedPath, userPromptsDir)) { - return "user"; - } - if (isUnderPath(resolvedPath, projectPromptsDir)) { - return "project"; - } + const getSourceInfo = (resolvedPath: string): SourceInfo => { + if (isUnderPath(resolvedPath, globalPromptsDir)) { + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + scope: "user", + baseDir: globalPromptsDir, + }); } - return "path"; + if (isUnderPath(resolvedPath, projectPromptsDir)) { + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + scope: "project", + baseDir: projectPromptsDir, + }); + } + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + baseDir: statSync(resolvedPath).isDirectory() ? resolvedPath : dirname(resolvedPath), + }); }; + if (includeDefaults) { + templates.push(...loadTemplatesFromDir(globalPromptsDir, getSourceInfo)); + templates.push(...loadTemplatesFromDir(projectPromptsDir, getSourceInfo)); + } + // 3. Load explicit prompt paths for (const rawPath of promptPaths) { const resolvedPath = resolvePromptPath(rawPath, resolvedCwd); @@ -255,11 +257,10 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P try { const stats = statSync(resolvedPath); - const source = getSource(resolvedPath); if (stats.isDirectory()) { - templates.push(...loadTemplatesFromDir(resolvedPath, source)); + templates.push(...loadTemplatesFromDir(resolvedPath, getSourceInfo)); } else if (stats.isFile() && resolvedPath.endsWith(".md")) { - const template = loadTemplateFromFile(resolvedPath, source); + const template = loadTemplateFromFile(resolvedPath, getSourceInfo(resolvedPath)); if (template) { templates.push(template); } diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index b77c34cd..d781afd3 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -470,6 +470,7 @@ export class DefaultResourceLoader implements ResourceLoader { ...skill, sourceInfo: this.findSourceInfoForPath(skill.filePath, this.extensionSkillSourceInfos, metadataByPath) ?? + skill.sourceInfo ?? this.getDefaultSourceInfoForPath(skill.filePath), })); this.skillDiagnostics = resolvedSkills.diagnostics; @@ -493,6 +494,7 @@ export class DefaultResourceLoader implements ResourceLoader { ...prompt, sourceInfo: this.findSourceInfoForPath(prompt.filePath, this.extensionPromptSourceInfos, metadataByPath) ?? + prompt.sourceInfo ?? this.getDefaultSourceInfoForPath(prompt.filePath), })); this.promptDiagnostics = resolvedPrompts.diagnostics; @@ -512,8 +514,9 @@ export class DefaultResourceLoader implements ResourceLoader { const sourcePath = theme.sourcePath; theme.sourceInfo = sourcePath ? (this.findSourceInfoForPath(sourcePath, this.extensionThemeSourceInfos, metadataByPath) ?? + theme.sourceInfo ?? this.getDefaultSourceInfoForPath(sourcePath)) - : undefined; + : theme.sourceInfo; return theme; }); this.themeDiagnostics = resolvedThemes.diagnostics; @@ -527,6 +530,9 @@ export class DefaultResourceLoader implements ResourceLoader { for (const command of extension.commands.values()) { command.sourceInfo = extension.sourceInfo; } + for (const tool of extension.tools.values()) { + tool.sourceInfo = extension.sourceInfo; + } } } @@ -576,11 +582,7 @@ export class DefaultResourceLoader implements ResourceLoader { return undefined; } - private getDefaultSourceInfoForPath(filePath: string): SourceInfo | undefined { - if (!filePath) { - return undefined; - } - + private getDefaultSourceInfoForPath(filePath: string): SourceInfo { if (filePath.startsWith("<") && filePath.endsWith(">")) { return { path: filePath, @@ -606,17 +608,23 @@ export class DefaultResourceLoader implements ResourceLoader { for (const root of agentRoots) { if (this.isUnderPath(normalizedPath, root)) { - return { path: filePath, source: "local", scope: "user", origin: "top-level" }; + return { path: filePath, source: "local", scope: "user", origin: "top-level", baseDir: root }; } } for (const root of projectRoots) { if (this.isUnderPath(normalizedPath, root)) { - return { path: filePath, source: "local", scope: "project", origin: "top-level" }; + return { path: filePath, source: "local", scope: "project", origin: "top-level", baseDir: root }; } } - return undefined; + return { + path: filePath, + source: "local", + scope: "temporary", + origin: "top-level", + baseDir: statSync(normalizedPath).isDirectory() ? normalizedPath : resolve(normalizedPath, ".."), + }; } private mergePaths(primary: string[], additional: string[]): string[] { diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index a67ab3f3..0d83c15e 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -90,7 +90,6 @@ export type { ExtensionContext, ExtensionFactory, SlashCommandInfo, - SlashCommandLocation, SlashCommandSource, ToolDefinition, } from "./extensions/index.js"; diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 149c2135..fc6077f9 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -5,7 +5,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "pat import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; import { parseFrontmatter } from "../utils/frontmatter.js"; import type { ResourceDiagnostic } from "./diagnostics.js"; -import type { SourceInfo } from "./source-info.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; /** Max name length per spec */ const MAX_NAME_LENGTH = 64; @@ -76,8 +76,7 @@ export interface Skill { description: string; filePath: string; baseDir: string; - source: string; - sourceInfo?: SourceInfo; + sourceInfo: SourceInfo; disableModelInvocation: boolean; } @@ -138,6 +137,30 @@ export interface LoadSkillsFromDirOptions { source: string; } +function createSkillSourceInfo(filePath: string, baseDir: string, source: string): SourceInfo { + switch (source) { + case "user": + return createSyntheticSourceInfo(filePath, { + source: "local", + scope: "user", + baseDir, + }); + case "project": + return createSyntheticSourceInfo(filePath, { + source: "local", + scope: "project", + baseDir, + }); + case "path": + return createSyntheticSourceInfo(filePath, { + source: "local", + baseDir, + }); + default: + return createSyntheticSourceInfo(filePath, { source, baseDir }); + } +} + /** * Load skills from a directory. * @@ -293,7 +316,7 @@ function loadSkillFromFile( description: frontmatter.description, filePath, baseDir: skillDir, - source, + sourceInfo: createSkillSourceInfo(filePath, skillDir, source), disableModelInvocation: frontmatter["disable-model-invocation"] === true, }, diagnostics, diff --git a/packages/coding-agent/src/core/slash-commands.ts b/packages/coding-agent/src/core/slash-commands.ts index 2cc3f74a..803f20a8 100644 --- a/packages/coding-agent/src/core/slash-commands.ts +++ b/packages/coding-agent/src/core/slash-commands.ts @@ -2,15 +2,11 @@ import type { SourceInfo } from "./source-info.js"; export type SlashCommandSource = "extension" | "prompt" | "skill"; -export type SlashCommandLocation = "user" | "project" | "path"; - export interface SlashCommandInfo { name: string; description?: string; source: SlashCommandSource; - sourceInfo?: SourceInfo; - location?: SlashCommandLocation; - path?: string; + sourceInfo: SourceInfo; } export interface BuiltinSlashCommand { diff --git a/packages/coding-agent/src/core/source-info.ts b/packages/coding-agent/src/core/source-info.ts index 425eaf2f..c75d3f94 100644 --- a/packages/coding-agent/src/core/source-info.ts +++ b/packages/coding-agent/src/core/source-info.ts @@ -1,14 +1,17 @@ import type { PathMetadata } from "./package-manager.js"; +export type SourceScope = "user" | "project" | "temporary"; +export type SourceOrigin = "package" | "top-level"; + export interface SourceInfo { - path?: string; + path: string; source: string; - scope: "user" | "project" | "temporary"; - origin: "package" | "top-level"; + scope: SourceScope; + origin: SourceOrigin; baseDir?: string; } -export function createSourceInfo(path: string | undefined, metadata: PathMetadata): SourceInfo { +export function createSourceInfo(path: string, metadata: PathMetadata): SourceInfo { return { path, source: metadata.source, @@ -17,3 +20,21 @@ export function createSourceInfo(path: string | undefined, metadata: PathMetadat baseDir: metadata.baseDir, }; } + +export function createSyntheticSourceInfo( + path: string, + options: { + source: string; + scope?: SourceScope; + origin?: SourceOrigin; + baseDir?: string; + }, +): SourceInfo { + return { + path, + source: options.source, + scope: options.scope ?? "temporary", + origin: options.origin ?? "top-level", + baseDir: options.baseDir, + }; +} diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 030c9d51..8f64f0e9 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -108,7 +108,6 @@ export type { SessionSwitchEvent, SessionTreeEvent, SlashCommandInfo, - SlashCommandLocation, SlashCommandSource, SourceInfo, TerminalInputHandler, @@ -215,6 +214,7 @@ export { type Skill, type SkillFrontmatter, } from "./core/skills.js"; +export { createSyntheticSourceInfo } from "./core/source-info.js"; // Tools export { type BashOperations, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 135dbf82..8071f733 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -333,7 +333,7 @@ export class InteractiveMode { .map((command) => ({ type: "warning" as const, message: `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`, - path: command.extensionPath, + path: command.sourceInfo.path, })); } diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 4e9c127e..deadea8d 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -544,35 +544,30 @@ export async function runRpcMode(session: AgentSession): Promise { case "get_commands": { const commands: RpcSlashCommand[] = []; - // Extension commands for (const command of session.extensionRunner?.getRegisteredCommands() ?? []) { commands.push({ name: command.name, description: command.description, source: "extension", - path: command.extensionPath, + sourceInfo: command.sourceInfo, }); } - // Prompt templates (source is always "user" | "project" | "path" in coding-agent) for (const template of session.promptTemplates) { commands.push({ name: template.name, description: template.description, source: "prompt", - location: template.source as RpcSlashCommand["location"], - path: template.filePath, + sourceInfo: template.sourceInfo, }); } - // Skills (source is always "user" | "project" | "path" in coding-agent) for (const skill of session.resourceLoader.getSkills().skills) { commands.push({ name: `skill:${skill.name}`, description: skill.description, source: "skill", - location: skill.source as RpcSlashCommand["location"], - path: skill.filePath, + sourceInfo: skill.sourceInfo, }); } diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index d21bf14f..5612f370 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -10,6 +10,7 @@ import type { ImageContent, Model } from "@mariozechner/pi-ai"; import type { SessionStats } from "../../core/agent-session.js"; import type { BashResult } from "../../core/bash-executor.js"; import type { CompactionResult } from "../../core/compaction/index.js"; +import type { SourceInfo } from "../../core/source-info.js"; // ============================================================================ // RPC Commands (stdin) @@ -78,10 +79,8 @@ export interface RpcSlashCommand { description?: string; /** What kind of command this is */ source: "extension" | "prompt" | "skill"; - /** Where the command was loaded from (undefined for extensions) */ - location?: "user" | "project" | "path"; - /** File path to the command source */ - path?: string; + /** Source metadata for the owning resource */ + sourceInfo: SourceInfo; } // ============================================================================ diff --git a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts index 766dcd6d..496b50ca 100644 --- a/packages/coding-agent/test/agent-session-dynamic-tools.test.ts +++ b/packages/coding-agent/test/agent-session-dynamic-tools.test.ts @@ -67,7 +67,23 @@ describe("AgentSession dynamic tool registration", () => { await session.bindExtensions({}); - expect(session.getAllTools().map((tool) => tool.name)).toContain("dynamic_tool"); + const allTools = session.getAllTools(); + const dynamicTool = allTools.find((tool) => tool.name === "dynamic_tool"); + const readTool = allTools.find((tool) => tool.name === "read"); + + expect(allTools.map((tool) => tool.name)).toContain("dynamic_tool"); + expect(dynamicTool?.sourceInfo).toMatchObject({ + path: "", + source: "inline", + scope: "temporary", + origin: "top-level", + }); + expect(readTool?.sourceInfo).toMatchObject({ + path: "", + source: "builtin", + scope: "temporary", + origin: "top-level", + }); expect(session.getActiveToolNames()).toContain("dynamic_tool"); expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior"); expect(session.systemPrompt).toContain("- Use dynamic_tool when the user asks for dynamic behavior tests."); @@ -75,6 +91,49 @@ describe("AgentSession dynamic tool registration", () => { session.dispose(); }); + it("returns source metadata for SDK custom tools", async () => { + const settingsManager = SettingsManager.create(tempDir, agentDir); + const sessionManager = SessionManager.inMemory(); + const resourceLoader = new DefaultResourceLoader({ + cwd: tempDir, + agentDir, + settingsManager, + }); + await resourceLoader.reload(); + + const { session } = await createAgentSession({ + cwd: tempDir, + agentDir, + model: getModel("anthropic", "claude-sonnet-4-5")!, + settingsManager, + sessionManager, + resourceLoader, + customTools: [ + { + name: "sdk_tool", + label: "SDK Tool", + description: "Tool registered through createAgentSession", + parameters: Type.Object({}), + execute: async () => ({ + content: [{ type: "text", text: "ok" }], + details: {}, + }), + }, + ], + }); + + const sdkTool = session.getAllTools().find((tool) => tool.name === "sdk_tool"); + expect(sdkTool?.sourceInfo).toMatchObject({ + path: "", + source: "sdk", + scope: "temporary", + origin: "top-level", + }); + expect(session.getActiveToolNames()).toContain("sdk_tool"); + + session.dispose(); + }); + it("keeps custom tools active but omits them from available tools when promptSnippet is not provided", async () => { const settingsManager = SettingsManager.create(tempDir, agentDir); const sessionManager = SessionManager.inMemory(); diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 15d075a8..b110150e 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -20,6 +20,7 @@ import { import { ModelRegistry } from "../src/core/model-registry.js"; import { SessionManager } from "../src/core/session-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; +import { createSyntheticSourceInfo } from "../src/core/source-info.js"; import { codingTools } from "../src/core/tools/index.js"; import { createTestResourceLoader } from "./utilities.js"; @@ -74,6 +75,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { return { path: "test-extension", resolvedPath: "/test/test-extension.ts", + sourceInfo: createSyntheticSourceInfo("", { source: "test" }), handlers, tools: new Map(), messageRenderers: new Map(), @@ -228,6 +230,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const throwingExtension: Extension = { path: "throwing-extension", resolvedPath: "/test/throwing-extension.ts", + sourceInfo: createSyntheticSourceInfo("", { source: "test" }), handlers: new Map Promise)[]>([ [ "session_before_compact", @@ -276,6 +279,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const extension1: Extension = { path: "extension1", resolvedPath: "/test/extension1.ts", + sourceInfo: createSyntheticSourceInfo("", { source: "test" }), handlers: new Map Promise)[]>([ [ "session_before_compact", @@ -306,6 +310,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const extension2: Extension = { path: "extension2", resolvedPath: "/test/extension2.ts", + sourceInfo: createSyntheticSourceInfo("", { source: "test" }), handlers: new Map Promise)[]>([ [ "session_before_compact", diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index f36abe69..2e44f538 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -9,6 +9,7 @@ import { DefaultResourceLoader } from "../src/core/resource-loader.js"; import { SessionManager } from "../src/core/session-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; import type { Skill } from "../src/core/skills.js"; +import { createSyntheticSourceInfo } from "../src/core/source-info.js"; describe("DefaultResourceLoader", () => { let tempDir: string; @@ -411,7 +412,7 @@ Content`, description: "Injected skill", filePath: "/fake/path", baseDir: "/fake", - source: "custom", + sourceInfo: createSyntheticSourceInfo("/fake/path", { source: "custom" }), disableModelInvocation: false, }; const loader = new DefaultResourceLoader({ diff --git a/packages/coding-agent/test/sdk-skills.test.ts b/packages/coding-agent/test/sdk-skills.test.ts index 1ceec315..5f95e67b 100644 --- a/packages/coding-agent/test/sdk-skills.test.ts +++ b/packages/coding-agent/test/sdk-skills.test.ts @@ -6,6 +6,7 @@ import { createExtensionRuntime } from "../src/core/extensions/loader.js"; import type { ResourceLoader } from "../src/core/resource-loader.js"; import { createAgentSession } from "../src/core/sdk.js"; import { SessionManager } from "../src/core/session-manager.js"; +import { createSyntheticSourceInfo } from "../src/core/source-info.js"; describe("createAgentSession skills option", () => { let tempDir: string; @@ -79,7 +80,7 @@ This is a test skill. description: "A custom skill", filePath: "/fake/path/SKILL.md", baseDir: "/fake/path", - source: "custom" as const, + sourceInfo: createSyntheticSourceInfo("/fake/path/SKILL.md", { source: "sdk" }), disableModelInvocation: false, }; diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index 5c6d19e1..f7426b30 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -3,10 +3,29 @@ import { join, resolve } from "path"; import { describe, expect, it } from "vitest"; import type { ResourceDiagnostic } from "../src/core/diagnostics.js"; import { formatSkillsForPrompt, loadSkills, loadSkillsFromDir, type Skill } from "../src/core/skills.js"; +import { createSyntheticSourceInfo } from "../src/core/source-info.js"; const fixturesDir = resolve(__dirname, "fixtures/skills"); const collisionFixturesDir = resolve(__dirname, "fixtures/skills-collision"); +function createTestSkill(options: { + name: string; + description: string; + filePath: string; + baseDir: string; + disableModelInvocation?: boolean; + source?: string; +}): Skill { + return { + name: options.name, + description: options.description, + filePath: options.filePath, + baseDir: options.baseDir, + sourceInfo: createSyntheticSourceInfo(options.filePath, { source: options.source ?? "test" }), + disableModelInvocation: options.disableModelInvocation ?? false, + }; +} + describe("skills", () => { describe("loadSkillsFromDir", () => { it("should load a valid skill", () => { @@ -18,7 +37,7 @@ describe("skills", () => { expect(skills).toHaveLength(1); expect(skills[0].name).toBe("valid-skill"); expect(skills[0].description).toBe("A valid skill for testing purposes."); - expect(skills[0].source).toBe("test"); + expect(skills[0].sourceInfo.source).toBe("test"); expect(diagnostics).toHaveLength(0); }); @@ -210,14 +229,12 @@ describe("skills", () => { it("should format skills as XML", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "test-skill", description: "A test skill.", filePath: "/path/to/skill/SKILL.md", baseDir: "/path/to/skill", - source: "test", - disableModelInvocation: false, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -232,14 +249,12 @@ describe("skills", () => { it("should include intro text before XML", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "test-skill", description: "A test skill.", filePath: "/path/to/skill/SKILL.md", baseDir: "/path/to/skill", - source: "test", - disableModelInvocation: false, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -252,14 +267,12 @@ describe("skills", () => { it("should escape XML special characters", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "test-skill", description: 'A skill with & "characters".', filePath: "/path/to/skill/SKILL.md", baseDir: "/path/to/skill", - source: "test", - disableModelInvocation: false, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -271,22 +284,18 @@ describe("skills", () => { it("should format multiple skills", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "skill-one", description: "First skill.", filePath: "/path/one/SKILL.md", baseDir: "/path/one", - source: "test", - disableModelInvocation: false, - }, - { + }), + createTestSkill({ name: "skill-two", description: "Second skill.", filePath: "/path/two/SKILL.md", baseDir: "/path/two", - source: "test", - disableModelInvocation: false, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -298,22 +307,19 @@ describe("skills", () => { it("should exclude skills with disableModelInvocation from prompt", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "visible-skill", description: "A visible skill.", filePath: "/path/visible/SKILL.md", baseDir: "/path/visible", - source: "test", - disableModelInvocation: false, - }, - { + }), + createTestSkill({ name: "hidden-skill", description: "A hidden skill.", filePath: "/path/hidden/SKILL.md", baseDir: "/path/hidden", - source: "test", disableModelInvocation: true, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -325,14 +331,13 @@ describe("skills", () => { it("should return empty string when all skills have disableModelInvocation", () => { const skills: Skill[] = [ - { + createTestSkill({ name: "hidden-skill", description: "A hidden skill.", filePath: "/path/hidden/SKILL.md", baseDir: "/path/hidden", - source: "test", disableModelInvocation: true, - }, + }), ]; const result = formatSkillsForPrompt(skills); @@ -351,7 +356,7 @@ describe("skills", () => { skillPaths: [join(fixturesDir, "valid-skill")], }); expect(skills).toHaveLength(1); - expect(skills[0].source).toBe("path"); + expect(skills[0].sourceInfo.scope).toBe("temporary"); expect(diagnostics).toHaveLength(0); }); @@ -415,7 +420,7 @@ describe("skills", () => { } expect(skillMap.size).toBe(1); - expect(skillMap.get("calendar")?.source).toBe("first"); + expect(skillMap.get("calendar")?.sourceInfo.source).toBe("first"); expect(collisionWarnings).toHaveLength(1); expect(collisionWarnings[0].message).toContain("name collision"); });