ikie-cli 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +22 -2
- package/dist/index.js +68 -1
- package/dist/repl.js +95 -0
- package/dist/skills.d.ts +67 -0
- package/dist/skills.js +345 -0
- package/dist/theme.js +1 -0
- package/dist/tools.js +104 -2
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -76,5 +76,5 @@ export declare class Agent {
|
|
|
76
76
|
private checkPermission;
|
|
77
77
|
}
|
|
78
78
|
export declare const SUBAGENT_FRAMING = "You are a focused sub-agent spawned by Ikie to autonomously complete ONE specific task.\nWork independently \u2014 do not ask the user questions. Use your tools to gather what you\nneed, do the work, and verify it. When finished, your FINAL message must be a concise\nsummary of what you did and any key results (paths changed, findings, answers). That\nsummary is the only thing returned to the main agent, so make it self-contained.";
|
|
79
|
-
export declare const PLAN_MODE_ADDENDUM = "\n\n## PLAN MODE (read-only)\nYou are currently in **plan mode**. You have ONLY read-only tools (read_file,\nlist_dir, search_files, grep, fetch_url, web_search, spawn_agent, ask_user). You CANNOT write files, edit\nfiles, run shell commands, or change anything \u2014 those tools are unavailable and any\nattempt will be blocked.\n\nYour job is to investigate and produce a clear, actionable plan:\n- Explore the relevant files and understand the current state before proposing anything.\n- Do NOT describe changes as if you already made them. You haven't.\n- End your response with a concise plan: a short **## Plan** heading followed by\n numbered steps. Name the specific files to change and what to change in each. Call\n out risks, assumptions, or open questions.\n- Keep it tight \u2014 enough to execute from, not an essay.\n\nAfter you present the plan, the user will be asked whether to execute it. On approval,\nyou'll be switched to agent mode and asked to carry out exactly this plan.";
|
|
80
|
-
export declare function buildSystemPrompt(projectContext: string, memoryContext: string): string;
|
|
79
|
+
export declare const PLAN_MODE_ADDENDUM = "\n\n## PLAN MODE (read-only)\nYou are currently in **plan mode**. You have ONLY read-only tools (read_file,\nlist_dir, search_files, grep, fetch_url, web_search, use_skill, spawn_agent, ask_user). You CANNOT write files, edit\nfiles, run shell commands, or change anything \u2014 those tools are unavailable and any\nattempt will be blocked.\n\nYour job is to investigate and produce a clear, actionable plan:\n- Explore the relevant files and understand the current state before proposing anything.\n- Do NOT describe changes as if you already made them. You haven't.\n- End your response with a concise plan: a short **## Plan** heading followed by\n numbered steps. Name the specific files to change and what to change in each. Call\n out risks, assumptions, or open questions.\n- Keep it tight \u2014 enough to execute from, not an essay.\n\nAfter you present the plan, the user will be asked whether to execute it. On approval,\nyou'll be switched to agent mode and asked to carry out exactly this plan.";
|
|
80
|
+
export declare function buildSystemPrompt(projectContext: string, memoryContext: string, skillsCatalog?: string): string;
|
package/dist/agent.js
CHANGED
|
@@ -68,6 +68,9 @@ function toolPhaseLabel(name) {
|
|
|
68
68
|
case 'git_branch': return 'Git branch';
|
|
69
69
|
case 'fetch_url': return 'Fetching';
|
|
70
70
|
case 'web_search': return 'Searching web';
|
|
71
|
+
case 'use_skill': return 'Loading skill';
|
|
72
|
+
case 'install_skill': return 'Installing skill';
|
|
73
|
+
case 'remove_skill': return 'Removing skill';
|
|
71
74
|
default: return `Preparing ${name}`;
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -807,7 +810,7 @@ export const PLAN_MODE_ADDENDUM = `
|
|
|
807
810
|
|
|
808
811
|
## PLAN MODE (read-only)
|
|
809
812
|
You are currently in **plan mode**. You have ONLY read-only tools (read_file,
|
|
810
|
-
list_dir, search_files, grep, fetch_url, web_search, spawn_agent, ask_user). You CANNOT write files, edit
|
|
813
|
+
list_dir, search_files, grep, fetch_url, web_search, use_skill, spawn_agent, ask_user). You CANNOT write files, edit
|
|
811
814
|
files, run shell commands, or change anything — those tools are unavailable and any
|
|
812
815
|
attempt will be blocked.
|
|
813
816
|
|
|
@@ -821,7 +824,7 @@ Your job is to investigate and produce a clear, actionable plan:
|
|
|
821
824
|
|
|
822
825
|
After you present the plan, the user will be asked whether to execute it. On approval,
|
|
823
826
|
you'll be switched to agent mode and asked to carry out exactly this plan.`;
|
|
824
|
-
export function buildSystemPrompt(projectContext, memoryContext) {
|
|
827
|
+
export function buildSystemPrompt(projectContext, memoryContext, skillsCatalog = '') {
|
|
825
828
|
const parts = [
|
|
826
829
|
`You are Ikie, an elite agentic software engineer running in the terminal.
|
|
827
830
|
|
|
@@ -930,11 +933,28 @@ changes they didn't ask for.
|
|
|
930
933
|
\`task\` and any needed \`context\`, since it does not see this conversation.
|
|
931
934
|
- \`switch_mode\`: Request permission to switch between plan and agent mode. Use when the current
|
|
932
935
|
mode is not sufficient for what you need to do next. The user must approve the switch.
|
|
936
|
+
- \`use_skill\`: Load a **skill** — a curated pack of expert instructions (plus optional bundled
|
|
937
|
+
files/scripts) for a specific kind of task. The skills installed on this machine are listed
|
|
938
|
+
under "Available Skills" below by name + description. When a task matches one, call
|
|
939
|
+
\`use_skill\` with its exact name FIRST to pull in the full instructions, then follow them.
|
|
940
|
+
- \`install_skill\`: Install a skill from a git URL (GitHub, GitLab, Bitbucket) or local path.
|
|
941
|
+
Skills are instruction packs that give the agent specialized expertise. Example:
|
|
942
|
+
\`install_skill(source: "https://github.com/user/repo.git")\`.
|
|
943
|
+
- \`remove_skill\`: Remove a previously installed skill by name. Call without a name to list
|
|
944
|
+
all installed skills.
|
|
933
945
|
- \`ask_user\`: Ask the user a clarifying question when you need more info to proceed.
|
|
934
946
|
The user's answer is returned as the tool result. Use sparingly — only when genuinely
|
|
935
947
|
unsure. Don't ask for confirmation on safe operations.
|
|
936
948
|
`,
|
|
937
949
|
];
|
|
950
|
+
if (skillsCatalog) {
|
|
951
|
+
parts.push(`## Available Skills\n` +
|
|
952
|
+
`These skills are installed and ready. Each is a specialized instruction pack. ` +
|
|
953
|
+
`When the user's task matches a skill's description, call \`use_skill\` with its exact ` +
|
|
954
|
+
`name BEFORE doing the work, then follow the loaded instructions. Only the name and ` +
|
|
955
|
+
`description are shown here — the full instructions load on demand.\n\n` +
|
|
956
|
+
skillsCatalog);
|
|
957
|
+
}
|
|
938
958
|
if (projectContext)
|
|
939
959
|
parts.push(`## Project Context\n${projectContext}`);
|
|
940
960
|
if (memoryContext)
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import minimist from 'minimist';
|
|
|
5
5
|
import { loadConfig, getApiKey, IKIE_API_BASE, IKIE_HOST, DEFAULT_MODEL, hasExplicitModel } from './config.js';
|
|
6
6
|
import { detectProjectContext, formatContextForPrompt } from './context.js';
|
|
7
7
|
import { loadAllMemory, formatMemoryForPrompt } from './memory.js';
|
|
8
|
+
import { discoverSkills, formatSkillsForPrompt } from './skills.js';
|
|
8
9
|
import { buildSystemPrompt, Agent } from './agent.js';
|
|
9
10
|
import { startREPL } from './repl.js';
|
|
10
11
|
import { login, logout } from './auth.js';
|
|
@@ -23,6 +24,9 @@ ${c.primary('Usage:')}
|
|
|
23
24
|
${c.accent('ikie')} ${c.muted('"<message>"')} One-shot command
|
|
24
25
|
${c.accent('ikie login')} Sign in to your ikie account
|
|
25
26
|
${c.accent('ikie logout')} Sign out of your ikie account
|
|
27
|
+
${c.accent('ikie skills')} List installed skills
|
|
28
|
+
${c.accent('ikie skills install')} ${c.muted('<src>')} Install a skill (git URL or path)
|
|
29
|
+
${c.accent('ikie skills remove')} ${c.muted('<name>')} Remove an installed skill
|
|
26
30
|
${c.accent('ikie')} ${c.muted('--model <id>')} Use specific model
|
|
27
31
|
|
|
28
32
|
${c.primary('Options:')}
|
|
@@ -40,6 +44,63 @@ ${c.primary('In-session commands:')}
|
|
|
40
44
|
${c.muted('Ctrl+V Paste image from clipboard')}
|
|
41
45
|
`);
|
|
42
46
|
}
|
|
47
|
+
async function runSkillsCli(args, force) {
|
|
48
|
+
const { discoverSkills, installSkill, removeSkill, listSkillFiles } = await import('./skills.js');
|
|
49
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
50
|
+
if (sub === 'install' || sub === 'add') {
|
|
51
|
+
const src = args.slice(1).filter(a => a !== '--force').join(' ').trim();
|
|
52
|
+
if (!src) {
|
|
53
|
+
console.error(errorLine('Usage: ikie skills install <local-path|git-url> [--force]'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(c.muted(`Installing skill from ${src}…`));
|
|
57
|
+
try {
|
|
58
|
+
const r = await installSkill(src, { force });
|
|
59
|
+
if (r.installed.length) {
|
|
60
|
+
console.log(`${c.success('✓')} Installed: ${c.white(r.installed.join(', '))}`);
|
|
61
|
+
}
|
|
62
|
+
for (const s of r.skipped)
|
|
63
|
+
console.log(`${c.muted('•')} Skipped ${s.name} — ${s.reason}`);
|
|
64
|
+
if (!r.installed.length && !r.skipped.length)
|
|
65
|
+
console.log(c.muted('Nothing installed.'));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(errorLine(err instanceof Error ? err.message : String(err)));
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (sub === 'remove' || sub === 'rm' || sub === 'uninstall') {
|
|
74
|
+
const name = args.slice(1).join(' ').trim();
|
|
75
|
+
if (!name) {
|
|
76
|
+
console.error(errorLine('Usage: ikie skills remove <name>'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (removeSkill(name))
|
|
80
|
+
console.log(`${c.success('✓')} Removed skill "${name}".`);
|
|
81
|
+
else {
|
|
82
|
+
console.error(errorLine(`No user-installed skill named "${name}".`));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// list
|
|
88
|
+
const skills = discoverSkills();
|
|
89
|
+
if (!skills.length) {
|
|
90
|
+
console.log('No skills installed.');
|
|
91
|
+
console.log(`\nInstall one with:\n ${c.accent('ikie skills install <git-url|path>')}`);
|
|
92
|
+
console.log(`\nClaude Code skills (.claude/skills) are also picked up automatically.`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`\n${c.primary.bold('Installed Skills')} ${c.muted(`(${skills.length})`)}\n`);
|
|
96
|
+
for (const s of skills) {
|
|
97
|
+
const nFiles = listSkillFiles(s).length;
|
|
98
|
+
console.log(` ${c.secondary('●')} ${c.white.bold(s.name)} ${c.muted(`[${s.source}·${s.origin}]`)}${nFiles ? c.muted(` · ${nFiles} files`) : ''}`);
|
|
99
|
+
if (s.description)
|
|
100
|
+
console.log(` ${c.dim(s.description)}`);
|
|
101
|
+
}
|
|
102
|
+
console.log();
|
|
103
|
+
}
|
|
43
104
|
async function main() {
|
|
44
105
|
if (argv.version) {
|
|
45
106
|
try {
|
|
@@ -68,6 +129,10 @@ async function main() {
|
|
|
68
129
|
logout();
|
|
69
130
|
process.exit(0);
|
|
70
131
|
}
|
|
132
|
+
if (cmd === 'skills') {
|
|
133
|
+
await runSkillsCli(argv._.slice(1).map(String), Boolean(argv.force));
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
71
136
|
const config = loadConfig();
|
|
72
137
|
if (argv.model)
|
|
73
138
|
config.model = argv.model;
|
|
@@ -115,7 +180,9 @@ ${errorLine('Not signed in.')}
|
|
|
115
180
|
const projectContextStr = formatContextForPrompt(projectCtx);
|
|
116
181
|
const memory = loadAllMemory();
|
|
117
182
|
const memoryStr = formatMemoryForPrompt(memory);
|
|
118
|
-
const
|
|
183
|
+
const skills = discoverSkills();
|
|
184
|
+
const skillsStr = formatSkillsForPrompt(skills);
|
|
185
|
+
const systemPrompt = buildSystemPrompt(projectContextStr, memoryStr, skillsStr);
|
|
119
186
|
if (argv.verbose) {
|
|
120
187
|
console.log(c.muted('─'.repeat(60)));
|
|
121
188
|
console.log(c.muted('System prompt:'));
|
package/dist/repl.js
CHANGED
|
@@ -93,6 +93,16 @@ const SLASH_CMDS = [
|
|
|
93
93
|
{ name: 'rpm', desc: 'Set request limit', args: '[number]' },
|
|
94
94
|
{ name: 'tokens', desc: 'Token estimate' },
|
|
95
95
|
{ name: 'usage', desc: 'Show account usage & credit' },
|
|
96
|
+
{
|
|
97
|
+
name: 'skills',
|
|
98
|
+
desc: 'List/install skills',
|
|
99
|
+
subs: [
|
|
100
|
+
{ name: 'list', desc: 'List installed skills' },
|
|
101
|
+
{ name: 'show', desc: 'Show a skill\'s instructions' },
|
|
102
|
+
{ name: 'install', desc: 'Install a skill from a path or git URL' },
|
|
103
|
+
{ name: 'remove', desc: 'Remove an installed skill' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
96
106
|
{ name: 'mode', desc: 'Show/set mode (plan|agent)', args: '[plan|agent]' },
|
|
97
107
|
{ name: 'plan', desc: 'Switch to plan mode (read-only)' },
|
|
98
108
|
{ name: 'agent', desc: 'Switch to agent mode (full)' },
|
|
@@ -165,6 +175,10 @@ ${c.primary.bold('Ikie Commands')}
|
|
|
165
175
|
${c.warning('/rpm [number]')} Show or set request limit
|
|
166
176
|
${c.warning('/tokens')} Show conversation token estimate
|
|
167
177
|
${c.warning('/usage')} Show account usage & credit balance
|
|
178
|
+
${c.warning('/skills')} List installed skills
|
|
179
|
+
${c.warning('/skills show <name>')} Show a skill's instructions
|
|
180
|
+
${c.warning('/skills install <src>')} Install a skill (git URL or path)
|
|
181
|
+
${c.warning('/skills remove <name>')} Remove an installed skill
|
|
168
182
|
${c.warning('/plan')} Plan mode — research & propose, no changes
|
|
169
183
|
${c.warning('/agent')} Agent mode — full execution (default)
|
|
170
184
|
${c.warning('/mode')} Show or set mode ${c.muted('(Shift+Tab toggles)')}
|
|
@@ -374,6 +388,87 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
374
388
|
await login();
|
|
375
389
|
return true;
|
|
376
390
|
}
|
|
391
|
+
case 'skills': {
|
|
392
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
393
|
+
const { discoverSkills, getSkill, listSkillFiles, installSkill, removeSkill } = await import('./skills.js');
|
|
394
|
+
if (sub === 'install' || sub === 'add') {
|
|
395
|
+
const rest = args.slice(1).filter(a => a !== '--force');
|
|
396
|
+
const force = args.includes('--force');
|
|
397
|
+
const src = rest.join(' ').trim();
|
|
398
|
+
if (!src) {
|
|
399
|
+
console.log(errorLine('Usage: /skills install <local-path|git-url> [--force]'));
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
console.log(c.muted(` Installing skill from ${src}…`));
|
|
403
|
+
try {
|
|
404
|
+
const r = await installSkill(src, { force });
|
|
405
|
+
if (r.installed.length) {
|
|
406
|
+
console.log(successLine(`Installed ${r.installed.length} skill${r.installed.length === 1 ? '' : 's'}: ${r.installed.join(', ')}`));
|
|
407
|
+
console.log(infoLine('Available now — the agent will use them automatically when relevant.'));
|
|
408
|
+
}
|
|
409
|
+
for (const s of r.skipped) {
|
|
410
|
+
console.log(infoLine(`Skipped "${s.name}" — ${s.reason}`));
|
|
411
|
+
}
|
|
412
|
+
if (!r.installed.length && !r.skipped.length) {
|
|
413
|
+
console.log(infoLine('Nothing installed.'));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
if (sub === 'remove' || sub === 'rm' || sub === 'uninstall') {
|
|
422
|
+
const nameArg = args.slice(1).join(' ').trim();
|
|
423
|
+
if (!nameArg) {
|
|
424
|
+
console.log(errorLine('Usage: /skills remove <name>'));
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (removeSkill(nameArg)) {
|
|
428
|
+
console.log(successLine(`Removed skill "${nameArg}".`));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log(errorLine(`No user-installed skill named "${nameArg}".`));
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if (sub === 'show' || sub === 'view') {
|
|
436
|
+
const nameArg = args.slice(1).join(' ').trim();
|
|
437
|
+
if (!nameArg) {
|
|
438
|
+
console.log(errorLine('Usage: /skills show <name>'));
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
const skill = getSkill(nameArg);
|
|
442
|
+
if (!skill) {
|
|
443
|
+
console.log(errorLine(`No skill named "${nameArg}".`));
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
console.log(`\n${c.primary.bold(skill.name)} ${c.muted(`(${skill.source} · ${skill.origin})`)}`);
|
|
447
|
+
if (skill.description)
|
|
448
|
+
console.log(c.white(skill.description));
|
|
449
|
+
console.log(c.dim(skill.dir));
|
|
450
|
+
console.log(renderMarkdown(skill.body || '(no instructions)'));
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
const skills = discoverSkills();
|
|
454
|
+
if (!skills.length) {
|
|
455
|
+
console.log(infoLine('No skills installed.'));
|
|
456
|
+
console.log(`\n ${c.muted('Install one with')} ${c.warning('/skills install <git-url|path>')}`);
|
|
457
|
+
console.log(` ${c.muted('or drop a folder with a')} ${c.warning('SKILL.md')} ${c.muted('into')} ${c.dim('~/.ikie/skills/')}`);
|
|
458
|
+
console.log(`\n ${c.muted('Claude Code skills (.claude/skills) work as-is.')}\n`);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
console.log(`\n${c.primary.bold('Installed Skills')} ${c.muted(`(${skills.length})`)}\n`);
|
|
462
|
+
for (const s of skills) {
|
|
463
|
+
const nFiles = listSkillFiles(s).length;
|
|
464
|
+
const extras = nFiles ? c.muted(` · ${nFiles} file${nFiles === 1 ? '' : 's'}`) : '';
|
|
465
|
+
console.log(` ${c.secondary('●')} ${c.white.bold(s.name)} ${c.muted(`[${s.source}·${s.origin}]`)}${extras}`);
|
|
466
|
+
if (s.description)
|
|
467
|
+
console.log(` ${c.dim(s.description)}`);
|
|
468
|
+
}
|
|
469
|
+
console.log(`\n ${c.muted('Used automatically when relevant ·')} ${c.warning('/skills show <name>')} ${c.muted('·')} ${c.warning('/skills install <src>')}\n`);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
377
472
|
case 'logout': {
|
|
378
473
|
if (!isLoggedIn(config)) {
|
|
379
474
|
console.log(infoLine('Not signed in.'));
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills — progressively-disclosed instruction packs, compatible with the
|
|
3
|
+
* Claude Code "Agent Skills" format (a folder containing a `SKILL.md` with
|
|
4
|
+
* YAML frontmatter `name` + `description`, and an optional body of
|
|
5
|
+
* instructions plus bundled resource files/scripts).
|
|
6
|
+
*
|
|
7
|
+
* The agent only ever sees each skill's name + description up front (cheap).
|
|
8
|
+
* When a skill looks relevant it calls the `use_skill` tool to load the full
|
|
9
|
+
* SKILL.md body on demand, along with the skill's directory so it can read any
|
|
10
|
+
* bundled files or run bundled scripts with its normal tools.
|
|
11
|
+
*/
|
|
12
|
+
export interface Skill {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
/** Full markdown body of SKILL.md (everything after the frontmatter). */
|
|
16
|
+
body: string;
|
|
17
|
+
/** Absolute path to the skill's directory. */
|
|
18
|
+
dir: string;
|
|
19
|
+
/** Absolute path to the SKILL.md file. */
|
|
20
|
+
file: string;
|
|
21
|
+
/** Where it came from, for display: "project" or "user". */
|
|
22
|
+
source: 'project' | 'user';
|
|
23
|
+
/** The roots this skill was discovered under, e.g. ".ikie" or ".claude". */
|
|
24
|
+
origin: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Discover all available skills across project and user roots.
|
|
28
|
+
* The first occurrence of a given (lowercased) name wins, so a project skill
|
|
29
|
+
* shadows a user skill with the same name.
|
|
30
|
+
*/
|
|
31
|
+
export declare function discoverSkills(cwd?: string): Skill[];
|
|
32
|
+
/** Look up a single skill by name (case-insensitive). */
|
|
33
|
+
export declare function getSkill(name: string, cwd?: string): Skill | undefined;
|
|
34
|
+
/** List the bundled files in a skill directory (excluding the SKILL.md itself). */
|
|
35
|
+
export declare function listSkillFiles(skill: Skill): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Render the skill catalog for the system prompt: each skill's name +
|
|
38
|
+
* description, so the model knows what exists and when to reach for one.
|
|
39
|
+
* Returns '' when no skills are installed (so the section is omitted).
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatSkillsForPrompt(skills: Skill[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* Build the full text returned to the model when it loads a skill via
|
|
44
|
+
* `use_skill`: the instructions plus a manifest of bundled files and the
|
|
45
|
+
* skill's directory, so the agent can read resources / run scripts itself.
|
|
46
|
+
*/
|
|
47
|
+
export declare function renderSkill(skill: Skill): string;
|
|
48
|
+
/** Where user-installed skills live. */
|
|
49
|
+
export declare function userSkillsDir(): string;
|
|
50
|
+
export interface InstallResult {
|
|
51
|
+
installed: string[];
|
|
52
|
+
skipped: {
|
|
53
|
+
name: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
}[];
|
|
56
|
+
from: 'local' | 'git';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Install one or more skills from a local path or a git repository URL into the
|
|
60
|
+
* user skills directory (~/.ikie/skills). A source may be a single skill folder
|
|
61
|
+
* (contains SKILL.md) or a collection that holds several skill folders.
|
|
62
|
+
*/
|
|
63
|
+
export declare function installSkill(source: string, opts?: {
|
|
64
|
+
force?: boolean;
|
|
65
|
+
}): Promise<InstallResult>;
|
|
66
|
+
/** Remove a user-installed skill by name. Returns true if it was removed. */
|
|
67
|
+
export declare function removeSkill(name: string): boolean;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { homedir, tmpdir } from 'os';
|
|
2
|
+
import { join, resolve, basename } from 'path';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, rmSync, cpSync } from 'fs';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
/** Parse minimal YAML frontmatter (--- delimited) into a flat string map + body. */
|
|
8
|
+
function parseFrontmatter(raw) {
|
|
9
|
+
const text = raw.replace(/^/, '');
|
|
10
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
11
|
+
if (!m)
|
|
12
|
+
return { meta: {}, body: text.trim() };
|
|
13
|
+
const meta = {};
|
|
14
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
15
|
+
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
|
|
16
|
+
if (!kv)
|
|
17
|
+
continue;
|
|
18
|
+
let value = kv[2].trim();
|
|
19
|
+
// Strip matching surrounding quotes.
|
|
20
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
21
|
+
value = value.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
meta[kv[1].toLowerCase()] = value;
|
|
24
|
+
}
|
|
25
|
+
return { meta, body: (m[2] ?? '').trim() };
|
|
26
|
+
}
|
|
27
|
+
function skillRoots(cwd = process.cwd()) {
|
|
28
|
+
const home = homedir();
|
|
29
|
+
return [
|
|
30
|
+
// Project skills win over user skills; .ikie wins over .claude within a tier.
|
|
31
|
+
{ dir: join(cwd, '.ikie', 'skills'), source: 'project', origin: '.ikie' },
|
|
32
|
+
{ dir: join(cwd, '.claude', 'skills'), source: 'project', origin: '.claude' },
|
|
33
|
+
{ dir: join(home, '.ikie', 'skills'), source: 'user', origin: '.ikie' },
|
|
34
|
+
{ dir: join(home, '.claude', 'skills'), source: 'user', origin: '.claude' },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function findSkillFile(skillDir) {
|
|
38
|
+
// Standard layout: <skillDir>/SKILL.md (case-insensitive on the filename).
|
|
39
|
+
for (const entry of ['SKILL.md', 'Skill.md', 'skill.md']) {
|
|
40
|
+
const p = join(skillDir, entry);
|
|
41
|
+
if (existsSync(p))
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Discover all available skills across project and user roots.
|
|
48
|
+
* The first occurrence of a given (lowercased) name wins, so a project skill
|
|
49
|
+
* shadows a user skill with the same name.
|
|
50
|
+
*/
|
|
51
|
+
export function discoverSkills(cwd = process.cwd()) {
|
|
52
|
+
const byName = new Map();
|
|
53
|
+
for (const root of skillRoots(cwd)) {
|
|
54
|
+
if (!existsSync(root.dir))
|
|
55
|
+
continue;
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(root.dir);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const skillDir = join(root.dir, entry);
|
|
65
|
+
let isDir = false;
|
|
66
|
+
try {
|
|
67
|
+
isDir = statSync(skillDir).isDirectory();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!isDir)
|
|
73
|
+
continue;
|
|
74
|
+
const file = findSkillFile(skillDir);
|
|
75
|
+
if (!file)
|
|
76
|
+
continue;
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
raw = readFileSync(file, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
85
|
+
const name = (meta.name || basename(skillDir)).trim();
|
|
86
|
+
const description = (meta.description || '').trim();
|
|
87
|
+
const key = name.toLowerCase();
|
|
88
|
+
if (!name || byName.has(key))
|
|
89
|
+
continue;
|
|
90
|
+
byName.set(key, {
|
|
91
|
+
name,
|
|
92
|
+
description,
|
|
93
|
+
body,
|
|
94
|
+
dir: skillDir,
|
|
95
|
+
file,
|
|
96
|
+
source: root.source,
|
|
97
|
+
origin: root.origin,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
102
|
+
}
|
|
103
|
+
/** Look up a single skill by name (case-insensitive). */
|
|
104
|
+
export function getSkill(name, cwd = process.cwd()) {
|
|
105
|
+
const want = name.trim().toLowerCase();
|
|
106
|
+
return discoverSkills(cwd).find(s => s.name.toLowerCase() === want);
|
|
107
|
+
}
|
|
108
|
+
/** List the bundled files in a skill directory (excluding the SKILL.md itself). */
|
|
109
|
+
export function listSkillFiles(skill) {
|
|
110
|
+
const out = [];
|
|
111
|
+
const walk = (dir, prefix) => {
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = readdirSync(dir);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (entry === 'SKILL.md' && prefix === '')
|
|
121
|
+
continue;
|
|
122
|
+
if (entry === '.git' || entry === 'node_modules')
|
|
123
|
+
continue;
|
|
124
|
+
const abs = join(dir, entry);
|
|
125
|
+
let st;
|
|
126
|
+
try {
|
|
127
|
+
st = statSync(abs);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
133
|
+
if (st.isDirectory()) {
|
|
134
|
+
walk(abs, rel);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
out.push(rel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
walk(skill.dir, '');
|
|
142
|
+
return out.sort();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Render the skill catalog for the system prompt: each skill's name +
|
|
146
|
+
* description, so the model knows what exists and when to reach for one.
|
|
147
|
+
* Returns '' when no skills are installed (so the section is omitted).
|
|
148
|
+
*/
|
|
149
|
+
export function formatSkillsForPrompt(skills) {
|
|
150
|
+
if (!skills.length)
|
|
151
|
+
return '';
|
|
152
|
+
const lines = skills.map(s => {
|
|
153
|
+
const desc = s.description || '(no description)';
|
|
154
|
+
return `- **${s.name}** — ${desc}`;
|
|
155
|
+
});
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build the full text returned to the model when it loads a skill via
|
|
160
|
+
* `use_skill`: the instructions plus a manifest of bundled files and the
|
|
161
|
+
* skill's directory, so the agent can read resources / run scripts itself.
|
|
162
|
+
*/
|
|
163
|
+
export function renderSkill(skill) {
|
|
164
|
+
const files = listSkillFiles(skill);
|
|
165
|
+
const parts = [];
|
|
166
|
+
parts.push(`# Skill: ${skill.name}`);
|
|
167
|
+
if (skill.description)
|
|
168
|
+
parts.push(skill.description);
|
|
169
|
+
parts.push(`Skill directory: ${skill.dir}`);
|
|
170
|
+
if (files.length) {
|
|
171
|
+
const shown = files.slice(0, 40);
|
|
172
|
+
const more = files.length - shown.length;
|
|
173
|
+
parts.push(`Bundled files (read with read_file or run with bash, relative to the skill directory):\n` +
|
|
174
|
+
shown.map(f => ` - ${f}`).join('\n') +
|
|
175
|
+
(more > 0 ? `\n …and ${more} more` : ''));
|
|
176
|
+
}
|
|
177
|
+
parts.push('---');
|
|
178
|
+
parts.push(skill.body || '(This skill has no additional instructions.)');
|
|
179
|
+
parts.push('---');
|
|
180
|
+
parts.push('Follow these instructions for the current task. Use your normal tools to read any ' +
|
|
181
|
+
'bundled files or run any bundled scripts referenced above (resolve their paths against ' +
|
|
182
|
+
'the skill directory).');
|
|
183
|
+
return parts.join('\n\n');
|
|
184
|
+
}
|
|
185
|
+
// ─── Install / remove ─────────────────────────────────────────────────────────
|
|
186
|
+
/** Where user-installed skills live. */
|
|
187
|
+
export function userSkillsDir() {
|
|
188
|
+
return join(homedir(), '.ikie', 'skills');
|
|
189
|
+
}
|
|
190
|
+
const SKILL_FILE_NAMES = ['SKILL.md', 'Skill.md', 'skill.md'];
|
|
191
|
+
function hasSkillFile(dir) {
|
|
192
|
+
return SKILL_FILE_NAMES.some(n => existsSync(join(dir, n)));
|
|
193
|
+
}
|
|
194
|
+
/** Find every directory containing a SKILL.md within `root`, up to `maxDepth`. */
|
|
195
|
+
function findSkillDirs(root, maxDepth = 3) {
|
|
196
|
+
const found = [];
|
|
197
|
+
const walk = (dir, depth) => {
|
|
198
|
+
if (depth > maxDepth)
|
|
199
|
+
return;
|
|
200
|
+
if (hasSkillFile(dir)) {
|
|
201
|
+
found.push(dir);
|
|
202
|
+
return; // don't descend into a skill's own subfolders
|
|
203
|
+
}
|
|
204
|
+
let entries;
|
|
205
|
+
try {
|
|
206
|
+
entries = readdirSync(dir);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry === '.git' || entry === 'node_modules')
|
|
213
|
+
continue;
|
|
214
|
+
const abs = join(dir, entry);
|
|
215
|
+
let st;
|
|
216
|
+
try {
|
|
217
|
+
st = statSync(abs);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (st.isDirectory())
|
|
223
|
+
walk(abs, depth + 1);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(root, 0);
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
/** Read the declared `name:` from a skill dir's SKILL.md, falling back to the folder name. */
|
|
230
|
+
function skillDirName(dir) {
|
|
231
|
+
const file = findSkillFile(dir);
|
|
232
|
+
if (file) {
|
|
233
|
+
try {
|
|
234
|
+
const { meta } = parseFrontmatter(readFileSync(file, 'utf-8'));
|
|
235
|
+
if (meta.name)
|
|
236
|
+
return meta.name.trim();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
/* fall through */
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return basename(dir);
|
|
243
|
+
}
|
|
244
|
+
function looksLikeGitUrl(source) {
|
|
245
|
+
return (/^git@/.test(source) ||
|
|
246
|
+
/^https?:\/\/.+\.git$/.test(source) ||
|
|
247
|
+
/^(https?:\/\/)?(www\.)?(github|gitlab|bitbucket)\.com\//.test(source) ||
|
|
248
|
+
/^git\+/.test(source));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Install one or more skills from a local path or a git repository URL into the
|
|
252
|
+
* user skills directory (~/.ikie/skills). A source may be a single skill folder
|
|
253
|
+
* (contains SKILL.md) or a collection that holds several skill folders.
|
|
254
|
+
*/
|
|
255
|
+
export async function installSkill(source, opts = {}) {
|
|
256
|
+
const raw = source.trim();
|
|
257
|
+
if (!raw)
|
|
258
|
+
throw new Error('A skill source (local path or git URL) is required.');
|
|
259
|
+
const dest = userSkillsDir();
|
|
260
|
+
mkdirSync(dest, { recursive: true });
|
|
261
|
+
const localPath = resolve(raw);
|
|
262
|
+
const isLocal = existsSync(localPath);
|
|
263
|
+
let scanRoot;
|
|
264
|
+
let tempDir;
|
|
265
|
+
let from;
|
|
266
|
+
if (isLocal) {
|
|
267
|
+
scanRoot = localPath;
|
|
268
|
+
from = 'local';
|
|
269
|
+
}
|
|
270
|
+
else if (looksLikeGitUrl(raw)) {
|
|
271
|
+
const cloneUrl = raw.replace(/^git\+/, '');
|
|
272
|
+
tempDir = join(tmpdir(), `ikie-skill-${Date.now()}`);
|
|
273
|
+
try {
|
|
274
|
+
await execAsync(`git clone --depth 1 ${JSON.stringify(cloneUrl)} ${JSON.stringify(tempDir)}`, {
|
|
275
|
+
timeout: 120000,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
const e = err;
|
|
280
|
+
throw new Error(`git clone failed: ${(e.stderr ?? e.message ?? String(err)).trim()}`);
|
|
281
|
+
}
|
|
282
|
+
scanRoot = tempDir;
|
|
283
|
+
from = 'git';
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
throw new Error(`Source not found: "${raw}". Provide an existing local path or a git URL.`);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const skillDirs = findSkillDirs(scanRoot);
|
|
290
|
+
if (!skillDirs.length) {
|
|
291
|
+
throw new Error('No SKILL.md found in the source. A skill must contain a SKILL.md file.');
|
|
292
|
+
}
|
|
293
|
+
const installed = [];
|
|
294
|
+
const skipped = [];
|
|
295
|
+
for (const dir of skillDirs) {
|
|
296
|
+
const name = skillDirName(dir);
|
|
297
|
+
const target = join(dest, name);
|
|
298
|
+
if (existsSync(target) && !opts.force) {
|
|
299
|
+
skipped.push({ name, reason: 'already installed (use --force to overwrite)' });
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (existsSync(target))
|
|
303
|
+
rmSync(target, { recursive: true, force: true });
|
|
304
|
+
cpSync(dir, target, { recursive: true });
|
|
305
|
+
// Never carry version-control metadata into the skill store.
|
|
306
|
+
rmSync(join(target, '.git'), { recursive: true, force: true });
|
|
307
|
+
installed.push(name);
|
|
308
|
+
}
|
|
309
|
+
return { installed, skipped, from };
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
if (tempDir)
|
|
313
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Remove a user-installed skill by name. Returns true if it was removed. */
|
|
317
|
+
export function removeSkill(name) {
|
|
318
|
+
const want = name.trim();
|
|
319
|
+
if (!want)
|
|
320
|
+
return false;
|
|
321
|
+
const dest = userSkillsDir();
|
|
322
|
+
// Match by declared name first, then by folder name.
|
|
323
|
+
let entries;
|
|
324
|
+
try {
|
|
325
|
+
entries = readdirSync(dest);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
const dir = join(dest, entry);
|
|
332
|
+
try {
|
|
333
|
+
if (!statSync(dir).isDirectory())
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (entry === want || skillDirName(dir).toLowerCase() === want.toLowerCase()) {
|
|
340
|
+
rmSync(dir, { recursive: true, force: true });
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
package/dist/theme.js
CHANGED
|
@@ -363,6 +363,7 @@ function toolMeta(rawName) {
|
|
|
363
363
|
case 'git_log': return { verb: 'GitLog', tint: c.info };
|
|
364
364
|
case 'git_commit': return { verb: 'Commit', tint: c.success };
|
|
365
365
|
case 'git_branch': return { verb: 'GitBranch', tint: c.info };
|
|
366
|
+
case 'use_skill': return { verb: 'Skill', tint: c.secondary };
|
|
366
367
|
default: return { verb: base, tint: c.primary };
|
|
367
368
|
}
|
|
368
369
|
}
|
package/dist/tools.js
CHANGED
|
@@ -270,6 +270,49 @@ export const TOOL_DEFS = [
|
|
|
270
270
|
},
|
|
271
271
|
},
|
|
272
272
|
},
|
|
273
|
+
{
|
|
274
|
+
type: 'function',
|
|
275
|
+
function: {
|
|
276
|
+
name: 'use_skill',
|
|
277
|
+
description: 'Load a skill — a curated pack of expert instructions (and optional bundled files/scripts) for a specific kind of task. Skills are listed by name + description in your system prompt under "Available Skills". When a task matches a skill, call this FIRST with that skill\'s exact name to pull in its full instructions, then follow them. Returns the skill body plus the paths of any bundled resources you can read or run.',
|
|
278
|
+
parameters: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
name: { type: 'string', description: 'The exact name of the skill to load (as shown in "Available Skills").' },
|
|
282
|
+
},
|
|
283
|
+
required: ['name'],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: 'function',
|
|
289
|
+
function: {
|
|
290
|
+
name: 'install_skill',
|
|
291
|
+
description: 'Install a skill from a local path or git repository URL (GitHub, GitLab, Bitbucket). Skills are instruction packs that give the agent specialized expertise. Provide a git URL (e.g. https://github.com/user/repo.git) or a local path. Use --force to overwrite existing skills.',
|
|
292
|
+
parameters: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {
|
|
295
|
+
source: { type: 'string', description: 'Local path or git URL to install skill(s) from' },
|
|
296
|
+
force: { type: 'boolean', description: 'Overwrite if already installed (default: false)' },
|
|
297
|
+
},
|
|
298
|
+
required: ['source'],
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: 'function',
|
|
304
|
+
function: {
|
|
305
|
+
name: 'remove_skill',
|
|
306
|
+
description: 'Remove a previously installed skill by name. Lists installed skills if called without a name.',
|
|
307
|
+
parameters: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
properties: {
|
|
310
|
+
name: { type: 'string', description: 'Name of the skill to remove (omit to list installed skills)' },
|
|
311
|
+
},
|
|
312
|
+
required: [],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
273
316
|
{
|
|
274
317
|
type: 'function',
|
|
275
318
|
function: {
|
|
@@ -289,11 +332,11 @@ export const TOOL_DEFS = [
|
|
|
289
332
|
// ─── Safe tools (auto-approved) ───────────────────────────────────────────────
|
|
290
333
|
// spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
|
|
291
334
|
// runs go through their own approval inside the sub-agent loop.
|
|
292
|
-
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch']);
|
|
335
|
+
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill']);
|
|
293
336
|
// Tools available in PLAN mode — read-only exploration plus delegation/questions.
|
|
294
337
|
// Everything that mutates the filesystem or runs commands (write_file, edit_file,
|
|
295
338
|
// bash, memory_write) is intentionally excluded so plan mode can only research.
|
|
296
|
-
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch']);
|
|
339
|
+
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill']);
|
|
297
340
|
// Paths that may contain secrets, credentials, or system configuration.
|
|
298
341
|
// Reading these requires explicit user permission even though read_file is normally safe.
|
|
299
342
|
const RESTRICTED_PATTERNS = [
|
|
@@ -371,6 +414,12 @@ export function formatToolArgs(name, input) {
|
|
|
371
414
|
const q = String(input.query ?? '');
|
|
372
415
|
return `"${q.length > 56 ? q.slice(0, 56) + '…' : q}"`;
|
|
373
416
|
}
|
|
417
|
+
case 'use_skill':
|
|
418
|
+
return `"${p(input.name)}"`;
|
|
419
|
+
case 'install_skill':
|
|
420
|
+
return `"${p(input.source)}"${input.force ? ' --force' : ''}`;
|
|
421
|
+
case 'remove_skill':
|
|
422
|
+
return input.name ? `"${p(input.name)}"` : '(list installed)';
|
|
374
423
|
default:
|
|
375
424
|
return JSON.stringify(input).slice(0, 80);
|
|
376
425
|
}
|
|
@@ -819,6 +868,56 @@ function formatSearchResults(query, results) {
|
|
|
819
868
|
});
|
|
820
869
|
return `Results for "${query}":\n\n${lines.join('\n\n')}`;
|
|
821
870
|
}
|
|
871
|
+
// ─── Skills ─────────────────────────────────────────────────────────────────
|
|
872
|
+
async function useSkill(input) {
|
|
873
|
+
const name = (input.name ?? '').trim();
|
|
874
|
+
if (!name || name === 'undefined')
|
|
875
|
+
return 'Error: name is required for use_skill';
|
|
876
|
+
const { getSkill, discoverSkills, renderSkill } = await import('./skills.js');
|
|
877
|
+
const skill = getSkill(name);
|
|
878
|
+
if (!skill) {
|
|
879
|
+
const available = discoverSkills().map(s => s.name);
|
|
880
|
+
return available.length
|
|
881
|
+
? `Error: no skill named "${name}". Available skills: ${available.join(', ')}.`
|
|
882
|
+
: `Error: no skill named "${name}", and no skills are installed.`;
|
|
883
|
+
}
|
|
884
|
+
return renderSkill(skill);
|
|
885
|
+
}
|
|
886
|
+
async function installSkill(input) {
|
|
887
|
+
const source = (input.source ?? '').trim();
|
|
888
|
+
if (!source || source === 'undefined')
|
|
889
|
+
return 'Error: source is required for install_skill';
|
|
890
|
+
const { installSkill: doInstall } = await import('./skills.js');
|
|
891
|
+
try {
|
|
892
|
+
const result = await doInstall(source, { force: input.force ?? false });
|
|
893
|
+
const lines = [];
|
|
894
|
+
if (result.installed.length) {
|
|
895
|
+
lines.push(`Installed ${result.installed.length} skill(s): ${result.installed.join(', ')}`);
|
|
896
|
+
}
|
|
897
|
+
if (result.skipped.length) {
|
|
898
|
+
for (const s of result.skipped)
|
|
899
|
+
lines.push(`Skipped "${s.name}": ${s.reason}`);
|
|
900
|
+
}
|
|
901
|
+
return lines.length ? lines.join('\n') : 'Nothing to install.';
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function removeSkill(input) {
|
|
908
|
+
const { removeSkill: doRemove, userSkillsDir, discoverSkills } = await import('./skills.js');
|
|
909
|
+
const name = (input.name ?? '').trim();
|
|
910
|
+
if (!name || name === 'undefined') {
|
|
911
|
+
// List installed skills
|
|
912
|
+
const skills = discoverSkills();
|
|
913
|
+
const userSkills = skills.filter(s => s.source === 'user');
|
|
914
|
+
if (!userSkills.length)
|
|
915
|
+
return 'No user-installed skills found.';
|
|
916
|
+
return 'Installed skills:\n' + userSkills.map(s => ` - ${s.name}: ${s.description || '(no description)'}`).join('\n');
|
|
917
|
+
}
|
|
918
|
+
const removed = doRemove(name);
|
|
919
|
+
return removed ? `Removed skill "${name}".` : `No skill named "${name}" found.`;
|
|
920
|
+
}
|
|
822
921
|
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
823
922
|
export async function executeTool(name, input) {
|
|
824
923
|
switch (name) {
|
|
@@ -838,6 +937,9 @@ export async function executeTool(name, input) {
|
|
|
838
937
|
case 'git_branch': return gitBranch(input);
|
|
839
938
|
case 'fetch_url': return fetchUrl(input);
|
|
840
939
|
case 'web_search': return webSearch(input);
|
|
940
|
+
case 'use_skill': return useSkill(input);
|
|
941
|
+
case 'install_skill': return installSkill(input);
|
|
942
|
+
case 'remove_skill': return removeSkill(input);
|
|
841
943
|
default: return `Unknown tool: ${name}`;
|
|
842
944
|
}
|
|
843
945
|
}
|