feat(coding-agent): add resources_discover hook

This commit is contained in:
Mario Zechner 2026-02-01 02:20:23 +01:00
parent 6b6030d549
commit 3b8d0a8921
18 changed files with 489 additions and 53 deletions

View file

@ -21,6 +21,7 @@ read README.md, then ask which module(s) to work on. Based on the answer, read t
## Commands
- After code changes (not documentation changes): `npm run check` (get full output, no tail). Fix all errors, warnings, and infos before committing.
- Note: `npm run check` does not run tests.
- NEVER run: `npm run dev`, `npm run build`, `npm test`
- Only run specific tests if user instructs: `npm test -- test/specific.test.ts`
- When writing tests, run them, identify issues in either the test or implementation, and iterate until fixed.

View file

@ -11,6 +11,7 @@
- Added OAuth `modifyModels` hook support for extension-registered providers at registration time. ([#940](https://github.com/badlogic/pi-mono/pull/940) by [@4h9fbZ](https://github.com/4h9fbZ))
- Added Qwen thinking format support for OpenAI-compatible completions via `enable_thinking`. ([#940](https://github.com/badlogic/pi-mono/pull/940) by [@4h9fbZ](https://github.com/4h9fbZ))
- Added sticky column tracking for vertical cursor navigation so the editor restores the preferred column when moving across short lines. ([#1120](https://github.com/badlogic/pi-mono/pull/1120) by [@Perlence](https://github.com/Perlence))
- Added `resources_discover` extension hook to supply additional skills, prompts, and themes on startup and reload.
### Fixed

View file

@ -88,6 +88,12 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|-----------|-------------|
| `mac-system-theme.ts` | Syncs pi theme with macOS dark/light mode |
### Resources
| Extension | Description |
|-----------|-------------|
| `dynamic-resources/` | Loads skills, prompts, and themes using `resources_discover` |
### Messages & Communication
| Extension | Description |

View file

@ -0,0 +1,8 @@
---
name: dynamic-resources
description: Example skill loaded from resources_discover
---
# Dynamic Resources Skill
This skill is provided by the dynamic-resources extension.

View file

@ -0,0 +1,79 @@
{
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
"name": "dynamic-resources",
"vars": {
"cyan": "#00d7ff",
"blue": "#5f87ff",
"green": "#b5bd68",
"red": "#cc6666",
"yellow": "#ffff00",
"gray": "#808080",
"dimGray": "#666666",
"darkGray": "#505050",
"accent": "#8abeb7",
"selectedBg": "#3a3a4a",
"userMsgBg": "#343541",
"toolPendingBg": "#282832",
"toolSuccessBg": "#283228",
"toolErrorBg": "#3c2828",
"customMsgBg": "#2d2838"
},
"colors": {
"accent": "accent",
"border": "blue",
"borderAccent": "cyan",
"borderMuted": "darkGray",
"success": "green",
"error": "red",
"warning": "yellow",
"muted": "gray",
"dim": "dimGray",
"text": "",
"thinkingText": "gray",
"selectedBg": "selectedBg",
"userMessageBg": "userMsgBg",
"userMessageText": "",
"customMessageBg": "customMsgBg",
"customMessageText": "",
"customMessageLabel": "#9575cd",
"toolPendingBg": "toolPendingBg",
"toolSuccessBg": "toolSuccessBg",
"toolErrorBg": "toolErrorBg",
"toolTitle": "",
"toolOutput": "gray",
"mdHeading": "#f0c674",
"mdLink": "#81a2be",
"mdLinkUrl": "dimGray",
"mdCode": "accent",
"mdCodeBlock": "green",
"mdCodeBlockBorder": "gray",
"mdQuote": "gray",
"mdQuoteBorder": "gray",
"mdHr": "gray",
"mdListBullet": "accent",
"toolDiffAdded": "green",
"toolDiffRemoved": "red",
"toolDiffContext": "gray",
"syntaxComment": "#6A9955",
"syntaxKeyword": "#569CD6",
"syntaxFunction": "#DCDCAA",
"syntaxVariable": "#9CDCFE",
"syntaxString": "#CE9178",
"syntaxNumber": "#B5CEA8",
"syntaxType": "#4EC9B0",
"syntaxOperator": "#D4D4D4",
"syntaxPunctuation": "#D4D4D4",
"thinkingOff": "darkGray",
"thinkingMinimal": "#6e6e6e",
"thinkingLow": "#5f87af",
"thinkingMedium": "#81a2be",
"thinkingHigh": "#b294bb",
"thinkingXhigh": "#d183e8",
"bashMode": "green"
},
"export": {
"pageBg": "#18181e",
"cardBg": "#1e1e24",
"infoBg": "#3c3728"
}
}

View file

@ -0,0 +1,5 @@
---
description: Example prompt template loaded from resources_discover
---
Summarize the current repository structure and mention any build or test commands.

View file

@ -0,0 +1,15 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
const baseDir = dirname(fileURLToPath(import.meta.url));
export default function (pi: ExtensionAPI) {
pi.on("resources_discover", () => {
return {
skillPaths: [join(baseDir, "SKILL.md")],
promptPaths: [join(baseDir, "dynamic.md")],
themePaths: [join(baseDir, "dynamic.json")],
};
});
}

View file

@ -54,6 +54,7 @@ const resourceLoader: ResourceLoader = {
Available: read, bash. Be concise.`,
getAppendSystemPrompt: () => [],
getPathMetadata: () => new Map(),
extendResources: () => {},
reload: async () => {},
};

View file

@ -14,7 +14,7 @@
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { basename, dirname, join } from "node:path";
import type {
Agent,
AgentEvent,
@ -64,7 +64,7 @@ import {
import type { BashExecutionMessage, CustomMessage } from "./messages.js";
import type { ModelRegistry } from "./model-registry.js";
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
import type { ResourceLoader } from "./resource-loader.js";
import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js";
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
import type { SettingsManager } from "./settings-manager.js";
import { buildSystemPrompt } from "./system-prompt.js";
@ -1690,9 +1690,63 @@ export class AgentSession {
if (this._extensionRunner) {
this._applyExtensionBindings(this._extensionRunner);
await this._extensionRunner.emit({ type: "session_start" });
await this.extendResourcesFromExtensions("startup");
}
}
private async extendResourcesFromExtensions(reason: "startup" | "reload"): Promise<void> {
if (!this._extensionRunner?.hasHandlers("resources_discover")) {
return;
}
const { skillPaths, promptPaths, themePaths } = await this._extensionRunner.emitResourcesDiscover(
this._cwd,
reason,
);
if (skillPaths.length === 0 && promptPaths.length === 0 && themePaths.length === 0) {
return;
}
const extensionPaths: ResourceExtensionPaths = {
skillPaths: this.buildExtensionResourcePaths(skillPaths),
promptPaths: this.buildExtensionResourcePaths(promptPaths),
themePaths: this.buildExtensionResourcePaths(themePaths),
};
this._resourceLoader.extendResources(extensionPaths);
this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
this.agent.setSystemPrompt(this._baseSystemPrompt);
}
private buildExtensionResourcePaths(entries: Array<{ path: string; extensionPath: string }>): Array<{
path: string;
metadata: { source: string; scope: "temporary"; origin: "top-level"; baseDir?: string };
}> {
return entries.map((entry) => {
const source = this.getExtensionSourceLabel(entry.extensionPath);
const baseDir = entry.extensionPath.startsWith("<") ? undefined : dirname(entry.extensionPath);
return {
path: entry.path,
metadata: {
source,
scope: "temporary",
origin: "top-level",
baseDir,
},
};
});
}
private getExtensionSourceLabel(extensionPath: string): string {
if (extensionPath.startsWith("<")) {
return `extension:${extensionPath.replace(/[<>]/g, "")}`;
}
const base = basename(extensionPath);
const name = base.replace(/\.(ts|js)$/, "");
return `extension:${name}`;
}
private _applyExtensionBindings(runner: ExtensionRunner): void {
runner.setUIContext(this._extensionUIContext);
runner.bindCommandContext(this._extensionCommandContextActions);
@ -1882,6 +1936,7 @@ export class AgentSession {
this._extensionErrorListener;
if (this._extensionRunner && hasBindings) {
await this._extensionRunner.emit({ type: "session_start" });
await this.extendResourcesFromExtensions("reload");
}
}

View file

@ -83,6 +83,9 @@ export type {
// Commands
RegisteredCommand,
RegisteredTool,
// Events - Resources
ResourcesDiscoverEvent,
ResourcesDiscoverResult,
SendMessageHandler,
SendUserMessageHandler,
SessionBeforeCompactEvent,

View file

@ -35,6 +35,8 @@ import type {
MessageRenderer,
RegisteredCommand,
RegisteredTool,
ResourcesDiscoverEvent,
ResourcesDiscoverResult,
SessionBeforeCompactResult,
SessionBeforeTreeResult,
ToolCallEvent,
@ -629,6 +631,54 @@ export class ExtensionRunner {
return undefined;
}
async emitResourcesDiscover(
cwd: string,
reason: ResourcesDiscoverEvent["reason"],
): Promise<{
skillPaths: Array<{ path: string; extensionPath: string }>;
promptPaths: Array<{ path: string; extensionPath: string }>;
themePaths: Array<{ path: string; extensionPath: string }>;
}> {
const ctx = this.createContext();
const skillPaths: Array<{ path: string; extensionPath: string }> = [];
const promptPaths: Array<{ path: string; extensionPath: string }> = [];
const themePaths: Array<{ path: string; extensionPath: string }> = [];
for (const ext of this.extensions) {
const handlers = ext.handlers.get("resources_discover");
if (!handlers || handlers.length === 0) continue;
for (const handler of handlers) {
try {
const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
const handlerResult = await handler(event, ctx);
const result = handlerResult as ResourcesDiscoverResult | undefined;
if (result?.skillPaths?.length) {
skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
}
if (result?.promptPaths?.length) {
promptPaths.push(...result.promptPaths.map((path) => ({ path, extensionPath: ext.path })));
}
if (result?.themePaths?.length) {
themePaths.push(...result.themePaths.map((path) => ({ path, extensionPath: ext.path })));
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
extensionPath: ext.path,
event: "resources_discover",
error: message,
stack,
});
}
}
}
return { skillPaths, promptPaths, themePaths };
}
/** Emit input event. Transforms chain, "handled" short-circuits. */
async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {
const ctx = this.createContext();

View file

@ -329,6 +329,24 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
renderResult?: (result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme) => Component;
}
// ============================================================================
// Resource Events
// ============================================================================
/** Fired after session_start to allow extensions to provide additional resource paths. */
export interface ResourcesDiscoverEvent {
type: "resources_discover";
cwd: string;
reason: "startup" | "reload";
}
/** Result from resources_discover event handler */
export interface ResourcesDiscoverResult {
skillPaths?: string[];
promptPaths?: string[];
themePaths?: string[];
}
// ============================================================================
// Session Events
// ============================================================================
@ -621,6 +639,7 @@ export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
/** Union of all event types */
export type ExtensionEvent =
| ResourcesDiscoverEvent
| SessionEvent
| ContextEvent
| BeforeAgentStartEvent
@ -736,6 +755,7 @@ export interface ExtensionAPI {
// Event Subscription
// =========================================================================
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
on(
event: "session_before_switch",

View file

@ -18,6 +18,12 @@ import { SettingsManager } from "./settings-manager.js";
import type { Skill } from "./skills.js";
import { loadSkills } from "./skills.js";
export interface ResourceExtensionPaths {
skillPaths?: Array<{ path: string; metadata: PathMetadata }>;
promptPaths?: Array<{ path: string; metadata: PathMetadata }>;
themePaths?: Array<{ path: string; metadata: PathMetadata }>;
}
export interface ResourceLoader {
getExtensions(): LoadExtensionsResult;
getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
@ -27,6 +33,7 @@ export interface ResourceLoader {
getSystemPrompt(): string | undefined;
getAppendSystemPrompt(): string[];
getPathMetadata(): Map<string, PathMetadata>;
extendResources(paths: ResourceExtensionPaths): void;
reload(): Promise<void>;
}
@ -187,6 +194,9 @@ export class DefaultResourceLoader implements ResourceLoader {
private systemPrompt?: string;
private appendSystemPrompt: string[];
private pathMetadata: Map<string, PathMetadata>;
private lastSkillPaths: string[];
private lastPromptPaths: string[];
private lastThemePaths: string[];
constructor(options: DefaultResourceLoaderOptions) {
this.cwd = options.cwd ?? process.cwd();
@ -227,6 +237,9 @@ export class DefaultResourceLoader implements ResourceLoader {
this.agentsFiles = [];
this.appendSystemPrompt = [];
this.pathMetadata = new Map();
this.lastSkillPaths = [];
this.lastPromptPaths = [];
this.lastThemePaths = [];
}
getExtensions(): LoadExtensionsResult {
@ -261,6 +274,36 @@ export class DefaultResourceLoader implements ResourceLoader {
return this.pathMetadata;
}
extendResources(paths: ResourceExtensionPaths): void {
const skillPaths = this.normalizeExtensionPaths(paths.skillPaths ?? []);
const promptPaths = this.normalizeExtensionPaths(paths.promptPaths ?? []);
const themePaths = this.normalizeExtensionPaths(paths.themePaths ?? []);
if (skillPaths.length > 0) {
this.lastSkillPaths = this.mergePaths(
this.lastSkillPaths,
skillPaths.map((entry) => entry.path),
);
this.updateSkillsFromPaths(this.lastSkillPaths, skillPaths);
}
if (promptPaths.length > 0) {
this.lastPromptPaths = this.mergePaths(
this.lastPromptPaths,
promptPaths.map((entry) => entry.path),
);
this.updatePromptsFromPaths(this.lastPromptPaths, promptPaths);
}
if (themePaths.length > 0) {
this.lastThemePaths = this.mergePaths(
this.lastThemePaths,
themePaths.map((entry) => entry.path),
);
this.updateThemesFromPaths(this.lastThemePaths, themePaths);
}
}
async reload(): Promise<void> {
const resolvedPaths = await this.packageManager.resolve();
const cliExtensionPaths = await this.packageManager.resolveExtensionSources(this.additionalExtensionPaths, {
@ -356,67 +399,22 @@ export class DefaultResourceLoader implements ResourceLoader {
? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths)
: this.mergePaths([...enabledSkills, ...cliEnabledSkills], this.additionalSkillPaths);
let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
if (this.noSkills && skillPaths.length === 0) {
skillsResult = { skills: [], diagnostics: [] };
} else {
skillsResult = loadSkills({
cwd: this.cwd,
agentDir: this.agentDir,
skillPaths,
includeDefaults: false,
});
}
const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult;
this.skills = resolvedSkills.skills;
this.skillDiagnostics = resolvedSkills.diagnostics;
for (const skill of this.skills) {
this.addDefaultMetadataForPath(skill.filePath);
}
this.lastSkillPaths = skillPaths;
this.updateSkillsFromPaths(skillPaths);
const promptPaths = this.noPromptTemplates
? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths)
: this.mergePaths([...enabledPrompts, ...cliEnabledPrompts], this.additionalPromptTemplatePaths);
let promptsResult: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };
if (this.noPromptTemplates && promptPaths.length === 0) {
promptsResult = { prompts: [], diagnostics: [] };
} else {
const allPrompts = loadPromptTemplates({
cwd: this.cwd,
agentDir: this.agentDir,
promptPaths,
includeDefaults: false,
});
promptsResult = this.dedupePrompts(allPrompts);
}
const resolvedPrompts = this.promptsOverride ? this.promptsOverride(promptsResult) : promptsResult;
this.prompts = resolvedPrompts.prompts;
this.promptDiagnostics = resolvedPrompts.diagnostics;
for (const prompt of this.prompts) {
this.addDefaultMetadataForPath(prompt.filePath);
}
this.lastPromptPaths = promptPaths;
this.updatePromptsFromPaths(promptPaths);
const themePaths = this.noThemes
? this.mergePaths(cliEnabledThemes, this.additionalThemePaths)
: this.mergePaths([...enabledThemes, ...cliEnabledThemes], this.additionalThemePaths);
let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] };
if (this.noThemes && themePaths.length === 0) {
themesResult = { themes: [], diagnostics: [] };
} else {
const loaded = this.loadThemes(themePaths, false);
const deduped = this.dedupeThemes(loaded.themes);
themesResult = { themes: deduped.themes, diagnostics: [...loaded.diagnostics, ...deduped.diagnostics] };
}
const resolvedThemes = this.themesOverride ? this.themesOverride(themesResult) : themesResult;
this.themes = resolvedThemes.themes;
this.themeDiagnostics = resolvedThemes.diagnostics;
for (const theme of this.themes) {
if (theme.sourcePath) {
this.addDefaultMetadataForPath(theme.sourcePath);
}
}
this.lastThemePaths = themePaths;
this.updateThemesFromPaths(themePaths);
for (const extension of this.extensionsResult.extensions) {
this.addDefaultMetadataForPath(extension.path);
@ -440,6 +438,128 @@ export class DefaultResourceLoader implements ResourceLoader {
: baseAppend;
}
private normalizeExtensionPaths(
entries: Array<{ path: string; metadata: PathMetadata }>,
): Array<{ path: string; metadata: PathMetadata }> {
return entries.map((entry) => ({
path: this.resolveResourcePath(entry.path),
metadata: entry.metadata,
}));
}
private updateSkillsFromPaths(
skillPaths: string[],
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
): void {
let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
if (this.noSkills && skillPaths.length === 0) {
skillsResult = { skills: [], diagnostics: [] };
} else {
skillsResult = loadSkills({
cwd: this.cwd,
agentDir: this.agentDir,
skillPaths,
includeDefaults: false,
});
}
const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult;
this.skills = resolvedSkills.skills;
this.skillDiagnostics = resolvedSkills.diagnostics;
this.applyExtensionMetadata(
extensionPaths,
this.skills.map((skill) => skill.filePath),
);
for (const skill of this.skills) {
this.addDefaultMetadataForPath(skill.filePath);
}
}
private updatePromptsFromPaths(
promptPaths: string[],
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
): void {
let promptsResult: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };
if (this.noPromptTemplates && promptPaths.length === 0) {
promptsResult = { prompts: [], diagnostics: [] };
} else {
const allPrompts = loadPromptTemplates({
cwd: this.cwd,
agentDir: this.agentDir,
promptPaths,
includeDefaults: false,
});
promptsResult = this.dedupePrompts(allPrompts);
}
const resolvedPrompts = this.promptsOverride ? this.promptsOverride(promptsResult) : promptsResult;
this.prompts = resolvedPrompts.prompts;
this.promptDiagnostics = resolvedPrompts.diagnostics;
this.applyExtensionMetadata(
extensionPaths,
this.prompts.map((prompt) => prompt.filePath),
);
for (const prompt of this.prompts) {
this.addDefaultMetadataForPath(prompt.filePath);
}
}
private updateThemesFromPaths(
themePaths: string[],
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
): void {
let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] };
if (this.noThemes && themePaths.length === 0) {
themesResult = { themes: [], diagnostics: [] };
} else {
const loaded = this.loadThemes(themePaths, false);
const deduped = this.dedupeThemes(loaded.themes);
themesResult = { themes: deduped.themes, diagnostics: [...loaded.diagnostics, ...deduped.diagnostics] };
}
const resolvedThemes = this.themesOverride ? this.themesOverride(themesResult) : themesResult;
this.themes = resolvedThemes.themes;
this.themeDiagnostics = resolvedThemes.diagnostics;
const themePathsWithSource = this.themes.flatMap((theme) => (theme.sourcePath ? [theme.sourcePath] : []));
this.applyExtensionMetadata(extensionPaths, themePathsWithSource);
for (const theme of this.themes) {
if (theme.sourcePath) {
this.addDefaultMetadataForPath(theme.sourcePath);
}
}
}
private applyExtensionMetadata(
extensionPaths: Array<{ path: string; metadata: PathMetadata }>,
resourcePaths: string[],
): void {
if (extensionPaths.length === 0) {
return;
}
const normalized = extensionPaths.map((entry) => ({
path: resolve(entry.path),
metadata: entry.metadata,
}));
for (const entry of normalized) {
if (!this.pathMetadata.has(entry.path)) {
this.pathMetadata.set(entry.path, entry.metadata);
}
}
for (const resourcePath of resourcePaths) {
const normalizedResourcePath = resolve(resourcePath);
if (this.pathMetadata.has(normalizedResourcePath) || this.pathMetadata.has(resourcePath)) {
continue;
}
const match = normalized.find(
(entry) =>
normalizedResourcePath === entry.path || normalizedResourcePath.startsWith(`${entry.path}${sep}`),
);
if (match) {
this.pathMetadata.set(normalizedResourcePath, match.metadata);
}
}
}
private mergePaths(primary: string[], additional: string[]): string[] {
const merged: string[] = [];
const seen = new Set<string>();

View file

@ -1066,6 +1066,9 @@ export class InteractiveMode {
},
});
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
this.rebuildAutocomplete();
const extensionRunner = this.session.extensionRunner;
if (!extensionRunner) {
this.showLoadedResources({ extensionPaths: [], force: false });

View file

@ -168,6 +168,71 @@ Content`,
});
});
describe("extendResources", () => {
it("should load skills and prompts with extension metadata", async () => {
const extraSkillDir = join(tempDir, "extra-skills", "extra-skill");
mkdirSync(extraSkillDir, { recursive: true });
const skillPath = join(extraSkillDir, "SKILL.md");
writeFileSync(
skillPath,
`---
name: extra-skill
description: Extra skill
---
Extra content`,
);
const extraPromptDir = join(tempDir, "extra-prompts");
mkdirSync(extraPromptDir, { recursive: true });
const promptPath = join(extraPromptDir, "extra.md");
writeFileSync(
promptPath,
`---
description: Extra prompt
---
Extra prompt content`,
);
const loader = new DefaultResourceLoader({ cwd, agentDir });
await loader.reload();
loader.extendResources({
skillPaths: [
{
path: extraSkillDir,
metadata: {
source: "extension:extra",
scope: "temporary",
origin: "top-level",
baseDir: extraSkillDir,
},
},
],
promptPaths: [
{
path: promptPath,
metadata: {
source: "extension:extra",
scope: "temporary",
origin: "top-level",
baseDir: extraPromptDir,
},
},
],
});
const { skills } = loader.getSkills();
expect(skills.some((skill) => skill.name === "extra-skill")).toBe(true);
const { prompts } = loader.getPrompts();
expect(prompts.some((prompt) => prompt.name === "extra")).toBe(true);
const metadata = loader.getPathMetadata();
expect(metadata.get(skillPath)?.source).toBe("extension:extra");
expect(metadata.get(promptPath)?.source).toBe("extension:extra");
});
});
describe("noSkills option", () => {
it("should skip skill discovery when noSkills is true", async () => {
const skillsDir = join(agentDir, "skills");

View file

@ -59,6 +59,7 @@ This is a test skill.
getSystemPrompt: () => undefined,
getAppendSystemPrompt: () => [],
getPathMetadata: () => new Map(),
extendResources: () => {},
reload: async () => {},
};
@ -92,6 +93,7 @@ This is a test skill.
getSystemPrompt: () => undefined,
getAppendSystemPrompt: () => [],
getPathMetadata: () => new Map(),
extendResources: () => {},
reload: async () => {},
};

View file

@ -184,6 +184,7 @@ export function createTestResourceLoader(): ResourceLoader {
getSystemPrompt: () => undefined,
getAppendSystemPrompt: () => [],
getPathMetadata: () => new Map(),
extendResources: () => {},
reload: async () => {},
};
}

View file

@ -459,6 +459,7 @@ function createRunner(sandboxConfig: SandboxConfig, channelId: string, channelDi
getSystemPrompt: () => systemPrompt,
getAppendSystemPrompt: () => [],
getPathMetadata: () => new Map(),
extendResources: () => {},
reload: async () => {},
};