diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1e7f4f70..e93ba8ef 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Breaking Changes - `before_agent_start` event now receives `systemPrompt` in the event object and returns `systemPrompt` (full replacement) instead of `systemPromptAppend`. Extensions that were appending must now use `event.systemPrompt + extra` pattern. ([#575](https://github.com/badlogic/pi-mono/issues/575)) +- `discoverSkills()` now returns `{ skills: Skill[], warnings: SkillWarning[] }` instead of `Skill[]`. This allows callers to handle skill loading warnings. ([#577](https://github.com/badlogic/pi-mono/pull/577) by [@cv](https://github.com/cv)) ### Added @@ -17,6 +18,7 @@ - `ssh.ts` example: remote tool execution via `--ssh user@host:/path` - Wayland clipboard support for `/copy` command using wl-copy with xclip/xsel fallback ([#570](https://github.com/badlogic/pi-mono/pull/570) by [@OgulcanCelik](https://github.com/OgulcanCelik)) - **Experimental:** `ctx.ui.custom()` now accepts `{ overlay: true }` option for floating modal components that composite over existing content without clearing the screen ([#558](https://github.com/badlogic/pi-mono/pull/558) by [@nicobailon](https://github.com/nicobailon)) +- `AgentSession.skills` and `AgentSession.skillWarnings` properties to access loaded skills without rediscovery ([#577](https://github.com/badlogic/pi-mono/pull/577) by [@cv](https://github.com/cv)) ### Fixed @@ -26,6 +28,7 @@ - Abort messages now show correct retry attempt count (e.g., "Aborted after 2 retry attempts") ([#568](https://github.com/badlogic/pi-mono/pull/568) by [@tmustier](https://github.com/tmustier)) - Fixed Antigravity provider returning 429 errors despite available quota ([#571](https://github.com/badlogic/pi-mono/pull/571) by [@ben-vargas](https://github.com/ben-vargas)) - Fixed malformed thinking text in Gemini/Antigravity responses where thinking content appeared as regular text or vice versa. Cross-model conversations now properly convert thinking blocks to plain text. ([#561](https://github.com/badlogic/pi-mono/issues/561)) +- `--no-skills` flag now correctly prevents skills from loading in interactive mode ([#577](https://github.com/badlogic/pi-mono/pull/577) by [@cv](https://github.com/cv)) ## [0.38.0] - 2026-01-08 diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 2a691b7f..1ee24d45 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -528,7 +528,7 @@ eventBus.on("my-extension:status", (data) => console.log(data)); import { createAgentSession, discoverSkills, type Skill } from "@mariozechner/pi-coding-agent"; // Discover and filter -const allSkills = discoverSkills(); +const { skills: allSkills, warnings } = discoverSkills(); const filtered = allSkills.filter(s => s.name.includes("search")); // Custom skill @@ -550,7 +550,7 @@ const { session } = await createAgentSession({ }); // Discovery with settings filter -const skills = discoverSkills(process.cwd(), undefined, { +const { skills } = discoverSkills(process.cwd(), undefined, { ignoredSkills: ["browser-*"], // glob patterns to exclude includeSkills: ["search-*"], // glob patterns to include (empty = all) }); @@ -747,7 +747,7 @@ const model = modelRegistry.find("provider", "id"); // Find specific model const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only // Skills -const skills = discoverSkills(cwd, agentDir, skillsSettings); +const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings); // Hooks (async - loads TypeScript) // Pass eventBus to share pi.events across hooks/tools diff --git a/packages/coding-agent/examples/sdk/04-skills.ts b/packages/coding-agent/examples/sdk/04-skills.ts index bf04633f..6fcb2a2a 100644 --- a/packages/coding-agent/examples/sdk/04-skills.ts +++ b/packages/coding-agent/examples/sdk/04-skills.ts @@ -8,11 +8,14 @@ import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@mariozechner/pi-coding-agent"; // Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc. -const allSkills = discoverSkills(); +const { skills: allSkills, warnings } = discoverSkills(); console.log( "Discovered skills:", allSkills.map((s) => s.name), ); +if (warnings.length > 0) { + console.log("Warnings:", warnings); +} // Filter to specific skills const filteredSkills = allSkills.filter((s) => s.name.includes("browser") || s.name.includes("search")); diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 5526bdaa..66db0f79 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -50,6 +50,7 @@ import type { ModelRegistry } from "./model-registry.js"; import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js"; import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager.js"; import type { SettingsManager, SkillsSettings } from "./settings-manager.js"; +import type { Skill, SkillWarning } from "./skills.js"; import type { BashOperations } from "./tools/bash.js"; /** Session-specific events that extend the core AgentEvent */ @@ -77,6 +78,10 @@ export interface AgentSessionConfig { promptTemplates?: PromptTemplate[]; /** Extension runner (created in sdk.ts with wrapped tools) */ extensionRunner?: ExtensionRunner; + /** Loaded skills (already discovered by SDK) */ + skills?: Skill[]; + /** Skill loading warnings (already captured by SDK) */ + skillWarnings?: SkillWarning[]; skillsSettings?: Required; /** Model registry for API key resolution and model discovery */ modelRegistry: ModelRegistry; @@ -177,6 +182,8 @@ export class AgentSession { private _extensionRunner: ExtensionRunner | undefined = undefined; private _turnIndex = 0; + private _skills: Skill[]; + private _skillWarnings: SkillWarning[]; private _skillsSettings: Required | undefined; // Model registry for API key resolution @@ -198,6 +205,8 @@ export class AgentSession { this._scopedModels = config.scopedModels ?? []; this._promptTemplates = config.promptTemplates ?? []; this._extensionRunner = config.extensionRunner; + this._skills = config.skills ?? []; + this._skillWarnings = config.skillWarnings ?? []; this._skillsSettings = config.skillsSettings; this._modelRegistry = config.modelRegistry; this._toolRegistry = config.toolRegistry ?? new Map(); @@ -870,6 +879,16 @@ export class AgentSession { return this._skillsSettings; } + /** Skills loaded by SDK (empty if --no-skills or skills: [] was passed) */ + get skills(): readonly Skill[] { + return this._skills; + } + + /** Skill loading warnings captured by SDK */ + get skillWarnings(): readonly SkillWarning[] { + return this._skillWarnings; + } + /** * Abort current operation and wait for agent to become idle. */ diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 985f7807..dd581368 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -43,7 +43,7 @@ import { ModelRegistry } from "./model-registry.js"; import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates.js"; import { SessionManager } from "./session-manager.js"; import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager.js"; -import { loadSkills as loadSkillsInternal, type Skill } from "./skills.js"; +import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./skills.js"; import { buildSystemPrompt as buildSystemPromptInternal, loadProjectContextFiles as loadContextFilesInternal, @@ -225,13 +225,16 @@ export async function discoverExtensions( /** * Discover skills from cwd and agentDir. */ -export function discoverSkills(cwd?: string, agentDir?: string, settings?: SkillsSettings): Skill[] { - const { skills } = loadSkillsInternal({ +export function discoverSkills( + cwd?: string, + agentDir?: string, + settings?: SkillsSettings, +): { skills: Skill[]; warnings: SkillWarning[] } { + return loadSkillsInternal({ ...settings, cwd: cwd ?? process.cwd(), agentDir: agentDir ?? getDefaultAgentDir(), }); - return skills; } /** @@ -419,7 +422,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} thinkingLevel = "off"; } - const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings()); + let skills: Skill[]; + let skillWarnings: SkillWarning[]; + if (options.skills !== undefined) { + skills = options.skills; + skillWarnings = []; + } else { + const discovered = discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings()); + skills = discovered.skills; + skillWarnings = discovered.warnings; + } time("discoverSkills"); const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir); @@ -641,13 +653,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} sessionManager.appendThinkingLevelChange(thinkingLevel); } - // Determine skillsSettings: if options.skills was explicitly provided (even []), - // mark skills as disabled so UI doesn't re-discover them - const skillsSettings = - options.skills !== undefined - ? { ...settingsManager.getSkillsSettings(), enabled: false } - : settingsManager.getSkillsSettings(); - const session = new AgentSession({ agent, sessionManager, @@ -655,7 +660,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} scopedModels: options.scopedModels, promptTemplates: promptTemplates, extensionRunner, - skillsSettings, + skills, + skillWarnings, + skillsSettings: settingsManager.getSkillsSettings(), modelRegistry, toolRegistry: wrappedToolRegistry ?? toolRegistry, rebuildSystemPrompt, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 32999bd2..e4dcffbd 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -43,7 +43,6 @@ import type { import { KeybindingsManager } from "../../core/keybindings.js"; import { createCompactionSummaryMessage } from "../../core/messages.js"; import { type SessionContext, SessionManager } from "../../core/session-manager.js"; -import { loadSkills } from "../../core/skills.js"; import { loadProjectContextFiles } from "../../core/system-prompt.js"; import { allTools } from "../../core/tools/index.js"; import type { TruncationResult } from "../../core/tools/truncate.js"; @@ -564,24 +563,22 @@ export class InteractiveMode { this.chatContainer.addChild(new Spacer(1)); } - // Show loaded skills - const skillsSettings = this.session.skillsSettings; - if (skillsSettings?.enabled !== false) { - const { skills, warnings: skillWarnings } = loadSkills(skillsSettings ?? {}); - if (skills.length > 0) { - const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n"); - this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); - } + // Show loaded skills (already discovered by SDK) + const skills = this.session.skills; + if (skills.length > 0) { + const skillList = skills.map((s) => theme.fg("dim", ` ${s.filePath}`)).join("\n"); + this.chatContainer.addChild(new Text(theme.fg("muted", "Loaded skills:\n") + skillList, 0, 0)); + this.chatContainer.addChild(new Spacer(1)); + } - // Show skill warnings if any - if (skillWarnings.length > 0) { - const warningList = skillWarnings - .map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)) - .join("\n"); - this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); - } + // Show skill warnings if any + const skillWarnings = this.session.skillWarnings; + if (skillWarnings.length > 0) { + const warningList = skillWarnings + .map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)) + .join("\n"); + this.chatContainer.addChild(new Text(theme.fg("warning", "Skill warnings:\n") + warningList, 0, 0)); + this.chatContainer.addChild(new Spacer(1)); } const extensionRunner = this.session.extensionRunner; diff --git a/packages/coding-agent/test/sdk-skills.test.ts b/packages/coding-agent/test/sdk-skills.test.ts index f8619dfa..dbf1e57e 100644 --- a/packages/coding-agent/test/sdk-skills.test.ts +++ b/packages/coding-agent/test/sdk-skills.test.ts @@ -14,7 +14,7 @@ describe("createAgentSession skills option", () => { skillsDir = join(tempDir, "skills", "test-skill"); mkdirSync(skillsDir, { recursive: true }); - // Create a test skill + // Create a test skill in the pi skills directory writeFileSync( join(skillsDir, "SKILL.md"), `--- @@ -35,18 +35,19 @@ This is a test skill. } }); - it("should discover skills by default", async () => { + it("should discover skills by default and expose them on session.skills", async () => { const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), }); - // skillsSettings.enabled should be true (from default settings) - expect(session.skillsSettings?.enabled).toBe(true); + // Skills should be discovered and exposed on the session + expect(session.skills.length).toBeGreaterThan(0); + expect(session.skills.some((s) => s.name === "test-skill")).toBe(true); }); - it("should disable skills in skillsSettings when options.skills is empty array", async () => { + it("should have empty skills when options.skills is empty array (--no-skills)", async () => { const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, @@ -54,27 +55,31 @@ This is a test skill. skills: [], // Explicitly empty - like --no-skills }); - // skillsSettings.enabled should be false so UI doesn't re-discover - expect(session.skillsSettings?.enabled).toBe(false); + // session.skills should be empty + expect(session.skills).toEqual([]); + // No warnings since we didn't discover + expect(session.skillWarnings).toEqual([]); }); - it("should disable skills in skillsSettings when options.skills is provided with skills", async () => { + it("should use provided skills when options.skills is explicitly set", async () => { + const customSkill = { + name: "custom-skill", + description: "A custom skill", + filePath: "/fake/path/SKILL.md", + baseDir: "/fake/path", + source: "custom" as const, + }; + const { session } = await createAgentSession({ cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), - skills: [ - { - name: "custom-skill", - description: "A custom skill", - filePath: "/fake/path/SKILL.md", - baseDir: "/fake/path", - source: "custom", - }, - ], + skills: [customSkill], }); - // skillsSettings.enabled should be false because skills were explicitly provided - expect(session.skillsSettings?.enabled).toBe(false); + // session.skills should contain only the provided skill + expect(session.skills).toEqual([customSkill]); + // No warnings since we didn't discover + expect(session.skillWarnings).toEqual([]); }); });