@ruvnet/open-claude-code 2.0.1 → 2.1.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.
@@ -2,9 +2,8 @@
2
2
  * System Prompt Builder — loads and merges CLAUDE.md files.
3
3
  *
4
4
  * Features:
5
- * - Loads CLAUDE.md from: ~/.claude/CLAUDE.md -> parent dirs -> project root -> .claude/CLAUDE.md
6
- * - Supports --add-dir for extra directories
7
- * - Merges in order (global -> parents -> project -> local)
5
+ * - Loads CLAUDE.md from: ~/.claude/CLAUDE.md, project root, parent dirs
6
+ * - Merges in order (global -> project -> local)
8
7
  * - Splits at cache boundary (static prefix cached, dynamic suffix not)
9
8
  * - Includes tool schemas in the system prompt
10
9
  */
@@ -14,56 +13,42 @@ import os from 'os';
14
13
 
15
14
  /**
16
15
  * Load all CLAUDE.md files and merge them in order.
17
- * Walk up parent directories to find all relevant files.
18
16
  * @param {string} [cwd] - current working directory
19
- * @param {string[]} [addDirs] - additional directories to search
20
- * @returns {Array<{source: string, content: string, path?: string}>}
17
+ * @returns {string[]} Array of CLAUDE.md contents in merge order
21
18
  */
22
- export function loadClaudeMdFiles(cwd = process.cwd(), addDirs = []) {
19
+ export function loadClaudeMdFiles(cwd = process.cwd()) {
23
20
  const files = [];
24
- const seen = new Set();
25
-
26
- // Helper to add a file if it exists and hasn't been seen
27
- function tryAdd(filePath, source) {
28
- const resolved = path.resolve(filePath);
29
- if (seen.has(resolved)) return;
30
- if (fs.existsSync(resolved)) {
31
- try {
32
- const content = fs.readFileSync(resolved, 'utf-8');
33
- files.push({ source, content, path: resolved });
34
- seen.add(resolved);
35
- } catch { /* skip unreadable */ }
36
- }
37
- }
38
21
 
39
- // 1. Global: ~/.claude/CLAUDE.md (lowest priority in the project chain)
40
- tryAdd(path.join(os.homedir(), '.claude', 'CLAUDE.md'), 'global');
22
+ // 1. Global: ~/.claude/CLAUDE.md
23
+ const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
24
+ if (fs.existsSync(globalPath)) {
25
+ try {
26
+ files.push({ source: 'global', content: fs.readFileSync(globalPath, 'utf-8') });
27
+ } catch { /* skip */ }
28
+ }
41
29
 
42
- // 2. Walk from root down to cwd collecting CLAUDE.md files
43
- // We walk up first to collect paths, then reverse for parent-first order
44
- const parentPaths = [];
30
+ // 2. Walk from cwd up to root, collecting CLAUDE.md files
31
+ const projectFiles = [];
45
32
  let dir = path.resolve(cwd);
46
33
  const root = path.parse(dir).root;
47
-
48
34
  while (dir !== root) {
49
- parentPaths.push(dir);
35
+ const candidates = [
36
+ path.join(dir, 'CLAUDE.md'),
37
+ path.join(dir, '.claude', 'CLAUDE.md'),
38
+ ];
39
+ for (const f of candidates) {
40
+ if (fs.existsSync(f)) {
41
+ try {
42
+ projectFiles.push({ source: dir, content: fs.readFileSync(f, 'utf-8'), path: f });
43
+ } catch { /* skip */ }
44
+ }
45
+ }
50
46
  dir = path.dirname(dir);
51
47
  }
52
48
 
53
- // Reverse: parent dirs first, project root last (so project overrides parents)
54
- parentPaths.reverse();
55
-
56
- for (const d of parentPaths) {
57
- tryAdd(path.join(d, 'CLAUDE.md'), d);
58
- tryAdd(path.join(d, '.claude', 'CLAUDE.md'), d);
59
- }
60
-
61
- // 3. Additional directories from --add-dir
62
- for (const addDir of addDirs) {
63
- const resolved = path.resolve(addDir);
64
- tryAdd(path.join(resolved, 'CLAUDE.md'), `add-dir:${resolved}`);
65
- tryAdd(path.join(resolved, '.claude', 'CLAUDE.md'), `add-dir:${resolved}`);
66
- }
49
+ // Reverse so parent dirs come first (global -> project -> local)
50
+ projectFiles.reverse();
51
+ files.push(...projectFiles);
67
52
 
68
53
  return files;
69
54
  }
@@ -75,17 +60,29 @@ export function loadClaudeMdFiles(cwd = process.cwd(), addDirs = []) {
75
60
  * @param {Array} [options.tools] - tool definitions for schema inclusion
76
61
  * @param {string} [options.override] - override system prompt entirely
77
62
  * @param {string[]} [options.addDirs] - additional directories to search for CLAUDE.md
78
- * @returns {{ staticPrefix: string, dynamicSuffix: string, full: string, mdFiles: Array }}
63
+ * @returns {{ staticPrefix: string, dynamicSuffix: string, full: string }}
79
64
  */
80
65
  export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
81
66
  if (override) {
82
- return { staticPrefix: override, dynamicSuffix: '', full: override, mdFiles: [] };
67
+ return { staticPrefix: override, dynamicSuffix: '', full: override };
83
68
  }
84
69
 
85
70
  const parts = ['You are an AI coding assistant.'];
86
71
 
87
- // Load CLAUDE.md files (multi-directory merge)
88
- const mdFiles = loadClaudeMdFiles(cwd, addDirs || []);
72
+ // Load CLAUDE.md files
73
+ const mdFiles = loadClaudeMdFiles(cwd);
74
+
75
+ // Add additional directories
76
+ if (addDirs) {
77
+ for (const dir of addDirs) {
78
+ const p = path.join(dir, 'CLAUDE.md');
79
+ if (fs.existsSync(p)) {
80
+ try {
81
+ mdFiles.push({ source: dir, content: fs.readFileSync(p, 'utf-8') });
82
+ } catch { /* skip */ }
83
+ }
84
+ }
85
+ }
89
86
 
90
87
  for (const f of mdFiles) {
91
88
  parts.push(f.content);
@@ -107,7 +104,6 @@ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
107
104
  staticPrefix,
108
105
  dynamicSuffix,
109
106
  full: staticPrefix + dynamicSuffix,
110
- mdFiles,
111
107
  };
112
108
  }
113
109
 
package/src/index.mjs CHANGED
@@ -12,14 +12,12 @@
12
12
  * - 6 permission modes + sandbox
13
13
  * - Context compaction + auto-compaction
14
14
  * - Hooks system (7 events)
15
- * - Settings chain (4 sources, 76 properties, deep merge, write-back)
15
+ * - Settings chain (5 layers, 76 properties)
16
16
  * - Multi-provider support (Anthropic, OpenAI, Google)
17
17
  * - Custom agents and skills
18
- * - Session management with save/resume/list
19
- * - File checkpoints with undo
18
+ * - Session management and checkpoints
20
19
  * - Prompt caching
21
- * - 39+ slash commands (all functional)
22
- * - Rich TUI with cost display
20
+ * - 39 slash commands
23
21
  * - Telemetry stub
24
22
  */
25
23
 
@@ -36,7 +34,6 @@ import { SessionManager } from './core/session.mjs';
36
34
  import { CheckpointManager } from './core/checkpoints.mjs';
37
35
  import { PromptCache } from './core/cache.mjs';
38
36
  import { readEnv } from './config/env.mjs';
39
- import { renderCostInline } from './ui/ink-app.mjs';
40
37
  import * as telemetry from './telemetry/index.mjs';
41
38
 
42
39
  async function main() {
@@ -44,7 +41,7 @@ async function main() {
44
41
 
45
42
  // Handle --version
46
43
  if (args.showVersion) {
47
- console.log('open-claude-code v2.0.1');
44
+ console.log('open-claude-code v2.0.0-alpha.1');
48
45
  process.exit(0);
49
46
  }
50
47
 
@@ -66,12 +63,7 @@ async function main() {
66
63
  if (args.debug) settings.debug = true;
67
64
 
68
65
  const tools = createToolRegistry();
69
-
70
- // In print mode (-p), auto-deny by not passing rl (fail-safe)
71
- const permConfig = { ...settings.permissions };
72
- // rl will be set below for REPL mode
73
-
74
- const permissions = createPermissionChecker(permConfig);
66
+ const permissions = createPermissionChecker(settings.permissions);
75
67
  const hooks = new HookEngine(settings.hooks);
76
68
 
77
69
  // Apply tool allow/deny lists
@@ -135,14 +127,8 @@ async function main() {
135
127
 
136
128
  telemetry.track('session.start', { model: loop.state.model });
137
129
 
138
- // Graceful shutdown — save session before exit
130
+ // Graceful shutdown
139
131
  const cleanup = async () => {
140
- // Auto-save session if there are messages
141
- if (loop.state.messages.length > 0) {
142
- try {
143
- sessionManager.save(loop.state);
144
- } catch { /* best effort */ }
145
- }
146
132
  telemetry.track('session.end', {
147
133
  turns: loop.state.turnCount,
148
134
  tokens: loop.state.tokenUsage,
@@ -155,8 +141,7 @@ async function main() {
155
141
  process.on('SIGTERM', async () => { await cleanup(); process.exit(0); });
156
142
 
157
143
  if (args.prompt) {
158
- // Non-interactive: run prompt and exit
159
- // In print mode, permissions auto-deny (no rl passed)
144
+ // Non-interactive: run prompt and exit (no Ink — plain stdout)
160
145
  const outputFormat = args.outputFormat || 'text';
161
146
  const results = [];
162
147
 
@@ -187,16 +172,21 @@ async function main() {
187
172
 
188
173
  await cleanup();
189
174
  } else {
190
- // Interactive REPL — check for resumable session
191
- const resumable = sessionManager.checkResumable();
192
- if (resumable.resumable) {
193
- const s = resumable.session;
194
- console.log(`\x1b[2mResumable session found: ${s.id} (${s.messageCount} messages, ${s.turnCount} turns)\x1b[0m`);
195
- console.log(`\x1b[2mType /resume to restore, or start fresh.\x1b[0m`);
175
+ // Interactive: use Ink React TUI
176
+ try {
177
+ const { startInkApp } = await import('./ui/app.mjs');
178
+ const inkInstance = startInkApp(loop, settings);
179
+
180
+ // Wait for Ink to exit (user pressed Ctrl+C or /quit)
181
+ await inkInstance.waitUntilExit();
182
+ } catch (err) {
183
+ // Fallback to readline REPL if Ink fails (e.g. no TTY, missing deps)
184
+ if (settings.debug) {
185
+ console.error(`Ink UI unavailable (${err.message}), falling back to readline REPL`);
186
+ }
187
+ const { startRepl } = await import('./ui/repl.mjs');
188
+ await startRepl(loop, settings);
196
189
  }
197
-
198
- const { startRepl } = await import('./ui/repl.mjs');
199
- await startRepl(loop, settings, permissions);
200
190
  await cleanup();
201
191
  }
202
192
  }
@@ -3,10 +3,9 @@
3
3
  *
4
4
  * Integrates with prompt system for interactive permission in default mode,
5
5
  * injection checking for Bash commands, and path validation for file ops.
6
- * Caches "always allow" decisions per tool per session.
7
6
  */
8
7
 
9
- import { requiresPermission, promptPermission, formatToolSummary } from './prompt.mjs';
8
+ import { requiresPermission } from './prompt.mjs';
10
9
  import { checkInjection } from './injection-check.mjs';
11
10
  import { validatePath } from './path-check.mjs';
12
11
 
@@ -14,18 +13,8 @@ export function createPermissionChecker(config = {}) {
14
13
  const mode = config.defaultMode || process.env.CLAUDE_CODE_PERMISSION_MODE || 'default';
15
14
  const rl = config.rl || null; // readline interface for prompts
16
15
 
17
- // Cache for "always allow" decisions: tool -> true
18
- const alwaysAllowed = new Set();
19
-
20
16
  return {
21
17
  mode,
22
- alwaysAllowed,
23
-
24
- /**
25
- * Check if a tool call is permitted.
26
- * In default mode with readline, prompts the user interactively.
27
- * Caches "always allow" decisions per tool per session.
28
- */
29
18
  async check(toolName, input) {
30
19
  // Always run injection check on Bash commands
31
20
  if (toolName === 'Bash' && input?.command) {
@@ -46,83 +35,23 @@ export function createPermissionChecker(config = {}) {
46
35
  switch (mode) {
47
36
  case 'bypassPermissions': return true;
48
37
  case 'acceptEdits':
49
- // Allow file ops, prompt for Bash/Agent
50
- if (['Edit', 'Write', 'MultiEdit', 'Read', 'Glob', 'Grep'].includes(toolName)) {
51
- return true;
52
- }
38
+ // Allow file ops, block Bash/Agent unless rl available
53
39
  if (toolName === 'Bash' || toolName === 'Agent') {
54
- if (config.bypassBash) return true;
55
- if (alwaysAllowed.has(toolName)) return true;
56
- if (rl) return this._promptUser(toolName, input);
57
- return false;
40
+ return !requiresPermission(toolName) || !!config.bypassBash;
58
41
  }
59
42
  return true;
60
43
  case 'auto': return true; // AI decides
61
44
  case 'dontAsk': return false; // deny everything not pre-approved
62
- case 'plan':
63
- return ['Read', 'Glob', 'Grep', 'LS', 'ToolSearch'].includes(toolName);
45
+ case 'plan': return toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep';
64
46
  case 'default':
65
47
  default:
66
- // Safe tools always pass
48
+ // In default mode, safe tools pass through
67
49
  if (!requiresPermission(toolName)) return true;
68
- // Check cached "always allow"
69
- if (alwaysAllowed.has(toolName)) return true;
70
- // Without readline, allow (headless/print mode)
50
+ // Without a readline interface, allow (headless mode)
71
51
  if (!rl) return true;
72
- // Interactive prompt
73
- return this._promptUser(toolName, input);
52
+ // With rl, would call promptPermission — but that's async/interactive
53
+ return true;
74
54
  }
75
55
  },
76
-
77
- /**
78
- * Prompt user for permission, with options: y/n/a (always).
79
- * @param {string} toolName
80
- * @param {object} input
81
- * @returns {Promise<boolean>}
82
- */
83
- async _promptUser(toolName, input) {
84
- if (!rl || typeof rl.question !== 'function') return false;
85
-
86
- const summary = formatToolSummary(toolName, input);
87
- return new Promise(resolve => {
88
- rl.question(`\x1b[33mAllow ${summary}?\x1b[0m [y/n/a(lways)] `, answer => {
89
- const a = answer.trim().toLowerCase();
90
- if (a === 'a' || a === 'always') {
91
- alwaysAllowed.add(toolName);
92
- resolve(true);
93
- } else if (a === 'y' || a === 'yes') {
94
- resolve(true);
95
- } else {
96
- resolve(false);
97
- }
98
- });
99
- });
100
- },
101
-
102
- /**
103
- * Grant "always allow" for a tool without prompting.
104
- * @param {string} toolName
105
- */
106
- allowAlways(toolName) {
107
- alwaysAllowed.add(toolName);
108
- },
109
-
110
- /**
111
- * Reset cached allow decisions.
112
- */
113
- resetAllowed() {
114
- alwaysAllowed.clear();
115
- },
116
-
117
- /**
118
- * Get info about current permission state.
119
- */
120
- getInfo() {
121
- return {
122
- mode,
123
- alwaysAllowed: [...alwaysAllowed],
124
- hasReadline: !!rl,
125
- };
126
- },
127
56
  };
128
57
  }
@@ -7,10 +7,8 @@
7
7
  * - description parameter
8
8
  * - 1MB output limit
9
9
  * - ANSI code stripping by default
10
- * - Sandbox integration (bubblewrap/seatbelt)
11
10
  */
12
11
  import { spawn } from 'child_process';
13
- import { Sandbox } from '../permissions/sandbox.mjs';
14
12
 
15
13
  // Strip ANSI escape sequences
16
14
  function stripAnsi(str) {
@@ -20,40 +18,6 @@ function stripAnsi(str) {
20
18
 
21
19
  const MAX_OUTPUT_BYTES = 1024 * 1024; // 1MB
22
20
 
23
- // Sandbox instance — reused across calls
24
- const sandbox = new Sandbox();
25
-
26
- /**
27
- * Determine if a command should be sandboxed.
28
- * @param {string} command
29
- * @returns {boolean}
30
- */
31
- function shouldSandbox(command) {
32
- // Forced sandbox via env var
33
- if (process.env.CLAUDE_CODE_FORCE_SANDBOX === '1') return true;
34
- // Bubblewrap env var
35
- if (process.env.CLAUDE_CODE_BUBBLEWRAP === '1') return true;
36
- // Sandbox setting
37
- if (process.env.CLAUDE_CODE_SANDBOX === '1') return true;
38
- return false;
39
- }
40
-
41
- /**
42
- * Wrap a command with sandbox if needed.
43
- * @param {string} command
44
- * @returns {string} possibly-wrapped command
45
- */
46
- function applySandbox(command) {
47
- if (!shouldSandbox(command)) return command;
48
-
49
- const check = sandbox.check();
50
- if (!check.available) return command; // graceful fallback
51
-
52
- return sandbox.wrapCommand(command, {
53
- allowWrite: [process.cwd(), '/tmp'],
54
- });
55
- }
56
-
57
21
  export const BashTool = {
58
22
  name: 'Bash',
59
23
  description: 'Execute a bash command and return its output.',
@@ -74,10 +38,9 @@ export const BashTool = {
74
38
  },
75
39
  async call(input) {
76
40
  const timeout = Math.min(input.timeout || 120000, 600000);
77
- const command = applySandbox(input.command);
78
41
 
79
42
  if (input.run_in_background) {
80
- return runBackground(command);
43
+ return runBackground(input.command);
81
44
  }
82
45
 
83
46
  return new Promise((resolve) => {
@@ -86,7 +49,7 @@ export const BashTool = {
86
49
  let killed = false;
87
50
  let exitCode = null;
88
51
 
89
- const proc = spawn('bash', ['-c', command], {
52
+ const proc = spawn('bash', ['-c', input.command], {
90
53
  env: { ...process.env },
91
54
  stdio: ['pipe', 'pipe', 'pipe'],
92
55
  timeout: 0, // we handle timeout ourselves
@@ -182,4 +145,4 @@ function runBackground(command) {
182
145
  return `Background job started: id=${id}, pid=${proc.pid}`;
183
146
  }
184
147
 
185
- export { backgroundJobs, shouldSandbox, applySandbox };
148
+ export { backgroundJobs };