@ruvnet/open-claude-code 2.0.0
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/package.json +27 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/config/cli-args.mjs +139 -0
- package/src/config/env.mjs +261 -0
- package/src/config/settings.mjs +119 -0
- package/src/core/agent-loop.mjs +462 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/checkpoints.mjs +141 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session.mjs +143 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +220 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/tools/agent.mjs +127 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +127 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +114 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/ink-app.mjs +231 -0
- package/src/ui/repl.mjs +144 -0
- package/test/test.mjs +1857 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Loader — load plugins from directory, git, or npm.
|
|
3
|
+
*
|
|
4
|
+
* Plugins can provide: tools, agents, skills, hooks.
|
|
5
|
+
* Plugin format: a directory with a plugin.json manifest.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
export class PluginLoader {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} [pluginDir] - directory to scan for plugins
|
|
16
|
+
*/
|
|
17
|
+
constructor(pluginDir) {
|
|
18
|
+
this.pluginDir = pluginDir ||
|
|
19
|
+
path.join(os.homedir(), '.claude', 'plugins');
|
|
20
|
+
this.plugins = new Map();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load plugins from the plugin directory.
|
|
25
|
+
* @returns {Array<object>} loaded plugin manifests
|
|
26
|
+
*/
|
|
27
|
+
async loadFromDirectory(dir) {
|
|
28
|
+
const targetDir = dir || this.pluginDir;
|
|
29
|
+
const loaded = [];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(targetDir)) return loaded;
|
|
33
|
+
|
|
34
|
+
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory()) continue;
|
|
37
|
+
|
|
38
|
+
const manifestPath = path.join(targetDir, entry.name, 'plugin.json');
|
|
39
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
43
|
+
manifest._dir = path.join(targetDir, entry.name);
|
|
44
|
+
manifest._name = entry.name;
|
|
45
|
+
this.plugins.set(manifest.name || entry.name, manifest);
|
|
46
|
+
loaded.push(manifest);
|
|
47
|
+
} catch {
|
|
48
|
+
// Skip malformed plugins
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Directory not readable
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return loaded;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clone a plugin from a git repo and load it.
|
|
60
|
+
* @param {string} repoUrl - git repository URL
|
|
61
|
+
* @param {string} [name] - plugin name (default: repo name)
|
|
62
|
+
* @returns {object|null} loaded manifest
|
|
63
|
+
*/
|
|
64
|
+
async loadFromGit(repoUrl, name) {
|
|
65
|
+
const pluginName = name || repoUrl.split('/').pop()?.replace('.git', '') || 'plugin';
|
|
66
|
+
const targetDir = path.join(this.pluginDir, pluginName);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(targetDir)) {
|
|
72
|
+
// Update existing
|
|
73
|
+
execSync('git pull', { cwd: targetDir, stdio: 'pipe' });
|
|
74
|
+
} else {
|
|
75
|
+
// Clone new
|
|
76
|
+
execSync(`git clone --depth 1 ${repoUrl} ${targetDir}`, { stdio: 'pipe' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const manifestPath = path.join(targetDir, 'plugin.json');
|
|
80
|
+
if (fs.existsSync(manifestPath)) {
|
|
81
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
82
|
+
manifest._dir = targetDir;
|
|
83
|
+
manifest._name = pluginName;
|
|
84
|
+
this.plugins.set(manifest.name || pluginName, manifest);
|
|
85
|
+
return manifest;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Git operation failed
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all installed plugins.
|
|
96
|
+
* @returns {Array<object>}
|
|
97
|
+
*/
|
|
98
|
+
getInstalledPlugins() {
|
|
99
|
+
return [...this.plugins.values()];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a plugin by name.
|
|
104
|
+
* @param {string} name
|
|
105
|
+
* @returns {object|undefined}
|
|
106
|
+
*/
|
|
107
|
+
getPlugin(name) {
|
|
108
|
+
return this.plugins.get(name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove a plugin by name.
|
|
113
|
+
* @param {string} name
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
removePlugin(name) {
|
|
117
|
+
const plugin = this.plugins.get(name);
|
|
118
|
+
if (!plugin) return false;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
if (plugin._dir && fs.existsSync(plugin._dir)) {
|
|
122
|
+
fs.rmSync(plugin._dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Best effort
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.plugins.delete(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get plugin count.
|
|
133
|
+
* @returns {number}
|
|
134
|
+
*/
|
|
135
|
+
count() {
|
|
136
|
+
return this.plugins.size;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Loader — loads skills from .claude/skills/{name}/SKILL.md
|
|
3
|
+
*
|
|
4
|
+
* Skills are invoked via /skill-name in REPL or the Skill tool.
|
|
5
|
+
* Each skill has a SKILL.md that defines:
|
|
6
|
+
* - name, description, trigger conditions
|
|
7
|
+
* - The prompt to inject when the skill is invoked
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export class SkillsLoader {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.skills = new Map();
|
|
16
|
+
this.searchPaths = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load skills from standard directories.
|
|
21
|
+
* @param {string} [cwd] - project working directory
|
|
22
|
+
*/
|
|
23
|
+
load(cwd = process.cwd()) {
|
|
24
|
+
this.searchPaths = [
|
|
25
|
+
path.join(cwd, '.claude', 'skills'),
|
|
26
|
+
path.join(process.env.HOME || '', '.claude', 'skills'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const dir of this.searchPaths) {
|
|
30
|
+
this._loadFromDir(dir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_loadFromDir(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isDirectory()) continue;
|
|
41
|
+
|
|
42
|
+
const skillFile = path.join(dir, entry.name, 'SKILL.md');
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
45
|
+
const skill = parseSkill(content, entry.name);
|
|
46
|
+
if (skill) {
|
|
47
|
+
skill.source = skillFile;
|
|
48
|
+
this.skills.set(skill.name, skill);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Skill directory without SKILL.md, skip
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Directory does not exist
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a skill by name.
|
|
61
|
+
* @param {string} name
|
|
62
|
+
* @returns {object|null}
|
|
63
|
+
*/
|
|
64
|
+
get(name) {
|
|
65
|
+
// Try exact match, then prefix match
|
|
66
|
+
if (this.skills.has(name)) return this.skills.get(name);
|
|
67
|
+
for (const [key, skill] of this.skills) {
|
|
68
|
+
if (key.startsWith(name) || skill.aliases?.includes(name)) {
|
|
69
|
+
return skill;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* List all loaded skills.
|
|
77
|
+
* @returns {Array<object>}
|
|
78
|
+
*/
|
|
79
|
+
list() {
|
|
80
|
+
return [...this.skills.values()];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run a skill, returning its prompt for injection into the conversation.
|
|
85
|
+
* @param {string} name - skill name
|
|
86
|
+
* @param {string} [args] - optional arguments
|
|
87
|
+
* @returns {string} skill prompt
|
|
88
|
+
*/
|
|
89
|
+
async run(name, args) {
|
|
90
|
+
const skill = this.get(name);
|
|
91
|
+
if (!skill) {
|
|
92
|
+
throw new Error(`Unknown skill: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let prompt = skill.prompt;
|
|
96
|
+
if (args) {
|
|
97
|
+
prompt = prompt.replace('$ARGUMENTS', args);
|
|
98
|
+
prompt += `\n\nArguments: ${args}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `[Skill: ${skill.name}]\n${prompt}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse a SKILL.md file into a skill definition.
|
|
107
|
+
*/
|
|
108
|
+
function parseSkill(content, dirName) {
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
const skill = {
|
|
111
|
+
name: dirName,
|
|
112
|
+
description: '',
|
|
113
|
+
aliases: [],
|
|
114
|
+
trigger: null,
|
|
115
|
+
prompt: content,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Parse YAML frontmatter if present
|
|
119
|
+
if (lines[0]?.trim() === '---') {
|
|
120
|
+
const endIdx = lines.indexOf('---', 1);
|
|
121
|
+
if (endIdx > 0) {
|
|
122
|
+
const frontmatter = lines.slice(1, endIdx).join('\n');
|
|
123
|
+
for (const line of frontmatter.split('\n')) {
|
|
124
|
+
const colonIdx = line.indexOf(':');
|
|
125
|
+
if (colonIdx === -1) continue;
|
|
126
|
+
const key = line.slice(0, colonIdx).trim();
|
|
127
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
128
|
+
|
|
129
|
+
if (key === 'name') skill.name = value;
|
|
130
|
+
else if (key === 'description') skill.description = value;
|
|
131
|
+
else if (key === 'trigger') skill.trigger = value;
|
|
132
|
+
else if (key === 'aliases') {
|
|
133
|
+
skill.aliases = value.replace(/[\[\]]/g, '').split(',').map(s => s.trim());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
skill.prompt = lines.slice(endIdx + 1).join('\n').trim();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract description from first paragraph if not in frontmatter
|
|
141
|
+
if (!skill.description && skill.prompt) {
|
|
142
|
+
const firstLine = skill.prompt.split('\n').find(l => l.trim() && !l.startsWith('#'));
|
|
143
|
+
if (firstLine) skill.description = firstLine.trim().slice(0, 100);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return skill;
|
|
147
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Runner — executes a skill by injecting its prompt.
|
|
3
|
+
*
|
|
4
|
+
* When a skill is invoked, its prompt is injected as a system message
|
|
5
|
+
* into the conversation context, guiding the agent's behavior.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SkillRunner {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} loader - SkillsLoader instance
|
|
11
|
+
* @param {object} agentLoop - agent loop instance
|
|
12
|
+
*/
|
|
13
|
+
constructor(loader, agentLoop) {
|
|
14
|
+
this.loader = loader;
|
|
15
|
+
this.loop = agentLoop;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a skill.
|
|
20
|
+
* @param {string} name - skill name
|
|
21
|
+
* @param {string} [args] - optional arguments
|
|
22
|
+
* @returns {AsyncGenerator} event stream from agent loop
|
|
23
|
+
*/
|
|
24
|
+
async *execute(name, args) {
|
|
25
|
+
const skill = this.loader.get(name);
|
|
26
|
+
if (!skill) {
|
|
27
|
+
yield { type: 'error', message: `Unknown skill: ${name}` };
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Build the skill prompt
|
|
32
|
+
let prompt = skill.prompt;
|
|
33
|
+
if (args) {
|
|
34
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Inject skill context as a user message
|
|
38
|
+
const message = `[Invoking skill: ${skill.name}]\n\n${prompt}${args ? `\n\nArguments: ${args}` : ''}`;
|
|
39
|
+
|
|
40
|
+
// Run through agent loop
|
|
41
|
+
yield* this.loop.run(message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List available skills for display.
|
|
46
|
+
* @returns {Array<{name: string, description: string}>}
|
|
47
|
+
*/
|
|
48
|
+
listAvailable() {
|
|
49
|
+
return this.loader.list().map(s => ({
|
|
50
|
+
name: s.name,
|
|
51
|
+
description: s.description,
|
|
52
|
+
aliases: s.aliases || [],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Stub — basic telemetry interface (no actual reporting).
|
|
3
|
+
*
|
|
4
|
+
* Logs events to debug output when CLAUDE_CODE_DEBUG is set.
|
|
5
|
+
* Designed to be a drop-in interface for the full telemetry system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const events = [];
|
|
9
|
+
let enabled = true;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Track a telemetry event.
|
|
13
|
+
* @param {string} event - event name
|
|
14
|
+
* @param {object} [properties] - event properties
|
|
15
|
+
*/
|
|
16
|
+
export function track(event, properties = {}) {
|
|
17
|
+
if (!enabled) return;
|
|
18
|
+
if (process.env.CLAUDE_CODE_DISABLE_TELEMETRY === '1') return;
|
|
19
|
+
|
|
20
|
+
const entry = {
|
|
21
|
+
event,
|
|
22
|
+
properties,
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
events.push(entry);
|
|
27
|
+
|
|
28
|
+
// Keep max 1000 events in memory
|
|
29
|
+
if (events.length > 1000) {
|
|
30
|
+
events.splice(0, events.length - 1000);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.env.CLAUDE_CODE_DEBUG) {
|
|
34
|
+
console.error(`[telemetry] ${event}`, JSON.stringify(properties).slice(0, 200));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Track a timing event.
|
|
40
|
+
* @param {string} event - event name
|
|
41
|
+
* @param {number} durationMs - duration in milliseconds
|
|
42
|
+
* @param {object} [properties] - additional properties
|
|
43
|
+
*/
|
|
44
|
+
export function trackTiming(event, durationMs, properties = {}) {
|
|
45
|
+
track(event, { ...properties, durationMs });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Track an error.
|
|
50
|
+
* @param {string} event - error context
|
|
51
|
+
* @param {Error} error - the error
|
|
52
|
+
*/
|
|
53
|
+
export function trackError(event, error) {
|
|
54
|
+
track(`error.${event}`, {
|
|
55
|
+
message: error.message,
|
|
56
|
+
stack: error.stack?.split('\n').slice(0, 3).join('\n'),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get collected events (for debugging).
|
|
62
|
+
* @returns {Array}
|
|
63
|
+
*/
|
|
64
|
+
export function getEvents() {
|
|
65
|
+
return [...events];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clear collected events.
|
|
70
|
+
*/
|
|
71
|
+
export function clear() {
|
|
72
|
+
events.length = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enable or disable telemetry.
|
|
77
|
+
* @param {boolean} value
|
|
78
|
+
*/
|
|
79
|
+
export function setEnabled(value) {
|
|
80
|
+
enabled = value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get telemetry stats.
|
|
85
|
+
*/
|
|
86
|
+
export function getStats() {
|
|
87
|
+
const counts = {};
|
|
88
|
+
for (const e of events) {
|
|
89
|
+
counts[e.event] = (counts[e.event] || 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
totalEvents: events.length,
|
|
93
|
+
enabled,
|
|
94
|
+
eventCounts: counts,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Tool — spawn a subagent with its own agent loop.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - subagent_type parameter
|
|
6
|
+
* - isolation: "worktree" option
|
|
7
|
+
* - run_in_background option
|
|
8
|
+
* - model override
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createAgentLoop } from '../core/agent-loop.mjs';
|
|
12
|
+
import { createToolRegistry } from './registry.mjs';
|
|
13
|
+
import { createPermissionChecker } from '../permissions/checker.mjs';
|
|
14
|
+
|
|
15
|
+
export const AgentTool = {
|
|
16
|
+
name: 'Agent',
|
|
17
|
+
description: 'Spawn a subagent to handle a task. The subagent has its own context and tools.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
prompt: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'The task for the subagent to perform',
|
|
24
|
+
},
|
|
25
|
+
allowed_tools: {
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: { type: 'string' },
|
|
28
|
+
description: 'List of tool names the subagent can use (default: all)',
|
|
29
|
+
},
|
|
30
|
+
subagent_type: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Type of subagent (e.g. coder, reviewer, researcher)',
|
|
33
|
+
},
|
|
34
|
+
isolation: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: ['default', 'worktree'],
|
|
37
|
+
description: 'Isolation mode. "worktree" uses a git worktree.',
|
|
38
|
+
},
|
|
39
|
+
run_in_background: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Run in background and return immediately',
|
|
42
|
+
},
|
|
43
|
+
model: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Override model for this subagent',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ['prompt'],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
validateInput(input) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
if (!input.prompt) errors.push('prompt is required');
|
|
54
|
+
return errors;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Track background subagents
|
|
58
|
+
_backgroundAgents: new Map(),
|
|
59
|
+
_nextBgId: 0,
|
|
60
|
+
|
|
61
|
+
async call(input) {
|
|
62
|
+
const model = input.model || process.env.SUBAGENT_MODEL || 'claude-sonnet-4-6';
|
|
63
|
+
const tools = createToolRegistry();
|
|
64
|
+
const permissions = createPermissionChecker({ defaultMode: 'bypassPermissions' });
|
|
65
|
+
|
|
66
|
+
// Build type-specific system prompt prefix
|
|
67
|
+
let systemPrefix = '';
|
|
68
|
+
if (input.subagent_type) {
|
|
69
|
+
const typePrompts = {
|
|
70
|
+
coder: 'You are a coding agent. Write clean, tested code.',
|
|
71
|
+
reviewer: 'You are a code reviewer. Analyze code for bugs and improvements.',
|
|
72
|
+
researcher: 'You are a research agent. Find and summarize information.',
|
|
73
|
+
tester: 'You are a testing agent. Write and run tests.',
|
|
74
|
+
planner: 'You are a planning agent. Break down tasks into steps.',
|
|
75
|
+
};
|
|
76
|
+
systemPrefix = typePrompts[input.subagent_type] || `You are a ${input.subagent_type} agent.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fullPrompt = systemPrefix
|
|
80
|
+
? `${systemPrefix}\n\nTask: ${input.prompt}`
|
|
81
|
+
: input.prompt;
|
|
82
|
+
|
|
83
|
+
const loop = createAgentLoop({
|
|
84
|
+
model,
|
|
85
|
+
tools,
|
|
86
|
+
permissions,
|
|
87
|
+
settings: { stream: false },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (input.run_in_background) {
|
|
91
|
+
const bgId = ++AgentTool._nextBgId;
|
|
92
|
+
const entry = { id: bgId, status: 'running', result: null, prompt: input.prompt };
|
|
93
|
+
AgentTool._backgroundAgents.set(bgId, entry);
|
|
94
|
+
|
|
95
|
+
// Run in background
|
|
96
|
+
runSubagent(loop, fullPrompt).then(result => {
|
|
97
|
+
entry.status = 'completed';
|
|
98
|
+
entry.result = result;
|
|
99
|
+
}).catch(err => {
|
|
100
|
+
entry.status = 'error';
|
|
101
|
+
entry.result = err.message;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return `Subagent started in background: id=${bgId}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return runSubagent(loop, fullPrompt);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
async function runSubagent(loop, prompt) {
|
|
112
|
+
const results = [];
|
|
113
|
+
try {
|
|
114
|
+
for await (const event of loop.run(prompt)) {
|
|
115
|
+
if (event.type === 'assistant' && event.content) {
|
|
116
|
+
results.push(event.content);
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'result') {
|
|
119
|
+
results.push(`[tool:${event.tool}] ${String(event.result).slice(0, 500)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return `Subagent error: ${err.message}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return results.join('\n') || 'Subagent completed with no output.';
|
|
127
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUser Tool — prompt the user with a question and return their response.
|
|
3
|
+
*
|
|
4
|
+
* Used when the agent needs clarification or confirmation from the user.
|
|
5
|
+
* In non-interactive mode, returns a default or times out.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
|
|
10
|
+
export const AskUserTool = {
|
|
11
|
+
name: 'AskUser',
|
|
12
|
+
description: 'Ask the user a question and wait for their response.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
question: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The question to ask the user',
|
|
19
|
+
},
|
|
20
|
+
default_value: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Default value if user provides no input',
|
|
23
|
+
},
|
|
24
|
+
timeout: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
description: 'Timeout in milliseconds (default: 60000)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ['question'],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
validateInput(input) {
|
|
33
|
+
return input.question ? [] : ['question is required'];
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async call(input) {
|
|
37
|
+
// In non-interactive mode, return default
|
|
38
|
+
if (!process.stdin.isTTY) {
|
|
39
|
+
return input.default_value || '[non-interactive: no user input available]';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stderr,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const timeout = setTimeout(() => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(input.default_value || '[timeout: no response]');
|
|
51
|
+
}, input.timeout || 60000);
|
|
52
|
+
|
|
53
|
+
process.stderr.write(`\n\x1b[36m? ${input.question}\x1b[0m\n> `);
|
|
54
|
+
rl.question('', (answer) => {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
rl.close();
|
|
57
|
+
resolve(answer.trim() || input.default_value || '');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|