@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.
Files changed (63) hide show
  1. package/package.json +27 -0
  2. package/src/agents/loader.mjs +89 -0
  3. package/src/agents/parser.mjs +98 -0
  4. package/src/agents/teams.mjs +123 -0
  5. package/src/auth/oauth.mjs +220 -0
  6. package/src/config/cli-args.mjs +139 -0
  7. package/src/config/env.mjs +261 -0
  8. package/src/config/settings.mjs +119 -0
  9. package/src/core/agent-loop.mjs +462 -0
  10. package/src/core/cache.mjs +105 -0
  11. package/src/core/checkpoints.mjs +141 -0
  12. package/src/core/context-manager.mjs +198 -0
  13. package/src/core/providers.mjs +219 -0
  14. package/src/core/rate-limiter.mjs +119 -0
  15. package/src/core/scheduler.mjs +173 -0
  16. package/src/core/session.mjs +143 -0
  17. package/src/core/streaming.mjs +182 -0
  18. package/src/core/system-prompt.mjs +135 -0
  19. package/src/hooks/engine.mjs +162 -0
  20. package/src/index.mjs +220 -0
  21. package/src/mcp/client.mjs +253 -0
  22. package/src/mcp/transport-shttp.mjs +130 -0
  23. package/src/mcp/transport-sse.mjs +131 -0
  24. package/src/mcp/transport-ws.mjs +134 -0
  25. package/src/permissions/checker.mjs +57 -0
  26. package/src/permissions/injection-check.mjs +60 -0
  27. package/src/permissions/path-check.mjs +102 -0
  28. package/src/permissions/prompt.mjs +73 -0
  29. package/src/permissions/sandbox.mjs +112 -0
  30. package/src/plugins/loader.mjs +138 -0
  31. package/src/skills/loader.mjs +147 -0
  32. package/src/skills/runner.mjs +55 -0
  33. package/src/telemetry/index.mjs +96 -0
  34. package/src/tools/agent.mjs +127 -0
  35. package/src/tools/ask-user.mjs +61 -0
  36. package/src/tools/bash.mjs +148 -0
  37. package/src/tools/cron-create.mjs +120 -0
  38. package/src/tools/cron-delete.mjs +49 -0
  39. package/src/tools/cron-list.mjs +37 -0
  40. package/src/tools/edit.mjs +82 -0
  41. package/src/tools/enter-worktree.mjs +69 -0
  42. package/src/tools/exit-worktree.mjs +57 -0
  43. package/src/tools/glob.mjs +117 -0
  44. package/src/tools/grep.mjs +127 -0
  45. package/src/tools/ls.mjs +58 -0
  46. package/src/tools/lsp.mjs +115 -0
  47. package/src/tools/multi-edit.mjs +94 -0
  48. package/src/tools/notebook-edit.mjs +96 -0
  49. package/src/tools/read-mcp-resource.mjs +57 -0
  50. package/src/tools/read.mjs +138 -0
  51. package/src/tools/registry.mjs +114 -0
  52. package/src/tools/remote-trigger.mjs +84 -0
  53. package/src/tools/send-message.mjs +64 -0
  54. package/src/tools/skill.mjs +52 -0
  55. package/src/tools/todo-write.mjs +68 -0
  56. package/src/tools/tool-search.mjs +77 -0
  57. package/src/tools/web-fetch.mjs +65 -0
  58. package/src/tools/web-search.mjs +89 -0
  59. package/src/tools/write.mjs +55 -0
  60. package/src/ui/commands.mjs +499 -0
  61. package/src/ui/ink-app.mjs +231 -0
  62. package/src/ui/repl.mjs +144 -0
  63. 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
+ };