feat(coding-agent): ResourceLoader, package management, and /reload command (#645)

- Add ResourceLoader interface and DefaultResourceLoader implementation
- Add PackageManager for npm/git extension sources with install/remove/update
- Add session.reload() and session.bindExtensions() APIs
- Add /reload command in interactive mode
- Add CLI flags: --skill, --theme, --prompt-template, --no-themes, --no-prompt-templates
- Add pi install/remove/update commands for extension management
- Refactor settings.json to use arrays for skills, prompts, themes
- Remove legacy SkillsSettings source flags and filters
- Update SDK examples and documentation for ResourceLoader pattern
- Add theme registration and loadThemeFromPath for dynamic themes
- Add getShellEnv to include bin dir in PATH for bash commands
This commit is contained in:
Mario Zechner 2026-01-20 23:34:53 +01:00
parent 866d21c252
commit b846a4bfcf
51 changed files with 2724 additions and 1852 deletions

View file

@ -18,19 +18,6 @@ export interface RetrySettings {
baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)
}
export interface SkillsSettings {
enabled?: boolean; // default: true
enableCodexUser?: boolean; // default: true
enableClaudeUser?: boolean; // default: true
enableClaudeProject?: boolean; // default: true
enablePiUser?: boolean; // default: true
enablePiProject?: boolean; // default: true
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
customDirectories?: string[]; // default: []
ignoredSkills?: string[]; // default: [] (glob patterns to exclude; takes precedence over includeSkills)
includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
}
export interface TerminalSettings {
showImages?: boolean; // default: true (only relevant if terminal supports images)
}
@ -67,8 +54,11 @@ export interface Settings {
quietStartup?: boolean;
shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support)
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
extensions?: string[]; // Array of extension file paths
skills?: SkillsSettings;
extensions?: string[]; // Array of extension file paths or directories
skills?: string[]; // Array of skill file paths or directories
prompts?: string[]; // Array of prompt template paths or directories
themes?: string[]; // Array of theme file paths or directories
enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands
terminal?: TerminalSettings;
images?: ImageSettings;
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
@ -165,6 +155,27 @@ export class SettingsManager {
settings.steeringMode = settings.queueMode;
delete settings.queueMode;
}
if (
"skills" in settings &&
typeof settings.skills === "object" &&
settings.skills !== null &&
!Array.isArray(settings.skills)
) {
const skillsSettings = settings.skills as {
enableSkillCommands?: boolean;
customDirectories?: unknown;
};
if (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {
settings.enableSkillCommands = skillsSettings.enableSkillCommands;
}
if (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {
settings.skills = skillsSettings.customDirectories;
} else {
delete settings.skills;
}
}
return settings as Settings;
}
@ -183,6 +194,14 @@ export class SettingsManager {
}
}
getGlobalSettings(): Settings {
return structuredClone(this.globalSettings);
}
getProjectSettings(): Settings {
return this.loadProjectSettings();
}
/** Apply additional overrides on top of current settings */
applyOverrides(overrides: Partial<Settings>): void {
this.settings = deepMergeSettings(this.settings, overrides);
@ -214,6 +233,21 @@ export class SettingsManager {
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
}
private saveProjectSettings(settings: Settings): void {
if (!this.persist || !this.projectSettingsPath) {
return;
}
try {
const dir = dirname(this.projectSettingsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(this.projectSettingsPath, JSON.stringify(settings, null, 2), "utf-8");
} catch (error) {
console.error(`Warning: Could not save project settings file: ${error}`);
}
}
getLastChangelogVersion(): string | undefined {
return this.settings.lastChangelogVersion;
}
@ -391,42 +425,46 @@ export class SettingsManager {
this.save();
}
getSkillsEnabled(): boolean {
return this.settings.skills?.enabled ?? true;
setProjectExtensionPaths(paths: string[]): void {
const projectSettings = this.loadProjectSettings();
projectSettings.extensions = paths;
this.saveProjectSettings(projectSettings);
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
}
setSkillsEnabled(enabled: boolean): void {
if (!this.globalSettings.skills) {
this.globalSettings.skills = {};
}
this.globalSettings.skills.enabled = enabled;
getSkillPaths(): string[] {
return [...(this.settings.skills ?? [])];
}
setSkillPaths(paths: string[]): void {
this.globalSettings.skills = paths;
this.save();
}
getSkillsSettings(): Required<SkillsSettings> {
return {
enabled: this.settings.skills?.enabled ?? true,
enableCodexUser: this.settings.skills?.enableCodexUser ?? true,
enableClaudeUser: this.settings.skills?.enableClaudeUser ?? true,
enableClaudeProject: this.settings.skills?.enableClaudeProject ?? true,
enablePiUser: this.settings.skills?.enablePiUser ?? true,
enablePiProject: this.settings.skills?.enablePiProject ?? true,
enableSkillCommands: this.settings.skills?.enableSkillCommands ?? true,
customDirectories: [...(this.settings.skills?.customDirectories ?? [])],
ignoredSkills: [...(this.settings.skills?.ignoredSkills ?? [])],
includeSkills: [...(this.settings.skills?.includeSkills ?? [])],
};
getPromptTemplatePaths(): string[] {
return [...(this.settings.prompts ?? [])];
}
setPromptTemplatePaths(paths: string[]): void {
this.globalSettings.prompts = paths;
this.save();
}
getThemePaths(): string[] {
return [...(this.settings.themes ?? [])];
}
setThemePaths(paths: string[]): void {
this.globalSettings.themes = paths;
this.save();
}
getEnableSkillCommands(): boolean {
return this.settings.skills?.enableSkillCommands ?? true;
return this.settings.enableSkillCommands ?? true;
}
setEnableSkillCommands(enabled: boolean): void {
if (!this.globalSettings.skills) {
this.globalSettings.skills = {};
}
this.globalSettings.skills.enableSkillCommands = enabled;
this.globalSettings.enableSkillCommands = enabled;
this.save();
}