@ruvnet/open-claude-code 2.0.0 → 2.0.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruvnet/open-claude-code",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Open source Claude Code CLI — based on ruDevolution decompilation of v2.1.91",
5
5
  "type": "module",
6
6
  "bin": {
@@ -79,6 +79,8 @@ export const ENV_SCHEMA = {
79
79
  ANTHROPIC_AUTH_TOKEN: { type: 'string', description: 'Anthropic auth token (OAuth)' },
80
80
 
81
81
  // Extended: Sandbox & Security
82
+ CLAUDE_CODE_FORCE_SANDBOX: { type: 'boolean', default: false, description: 'Force sandbox for all commands' },
83
+ CLAUDE_CODE_BUBBLEWRAP: { type: 'boolean', default: false, description: 'Enable bubblewrap sandbox on Linux' },
82
84
  CLAUDE_CODE_SANDBOX_PLATFORM: { type: 'string', description: 'Override sandbox platform (linux/darwin)' },
83
85
  CLAUDE_CODE_INJECTION_CHECK: { type: 'boolean', default: true, description: 'Enable command injection checks' },
84
86
  CLAUDE_CODE_PATH_CHECK: { type: 'boolean', default: true, description: 'Enable file path validation' },
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Supports all 76 properties from Claude Code's settings schema.
5
5
  * Five-layer hierarchy: user > project > local > managed > feature flags.
6
+ * Deep merge with source tracking and write-back.
6
7
  */
7
8
  import fs from 'fs';
8
9
  import path from 'path';
@@ -60,28 +61,101 @@ export const SETTINGS_SCHEMA = {
60
61
  featureFlags: {},
61
62
  };
62
63
 
63
- export async function loadSettings() {
64
+ /** Sources in load order (first = lowest priority, last = highest) */
65
+ const SCOPE_NAMES = ['managed', 'user', 'project', 'local'];
66
+
67
+ /**
68
+ * Get file paths for each settings scope.
69
+ * @param {string} [cwd]
70
+ * @returns {Record<string, string>}
71
+ */
72
+ export function getSettingsPaths(cwd = process.cwd()) {
73
+ return {
74
+ user: path.join(os.homedir(), '.claude', 'settings.json'),
75
+ project: path.join(cwd, '.claude', 'settings.json'),
76
+ local: path.join(cwd, '.claude', 'settings.local.json'),
77
+ managed: path.join(os.homedir(), '.claude', 'settings.managed.json'),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Load settings with 4-source deep merge and source tracking.
83
+ * Order: defaults -> managed -> user -> project -> local -> env overrides
84
+ * @param {string} [cwd]
85
+ * @returns {Promise<object>} merged settings with _sources metadata
86
+ */
87
+ export async function loadSettings(cwd) {
88
+ const paths = getSettingsPaths(cwd);
64
89
  const chain = [
65
- path.join(os.homedir(), '.claude', 'settings.json'),
66
- path.join(process.cwd(), '.claude', 'settings.json'),
67
- path.join(process.cwd(), '.claude', 'settings.local.json'),
90
+ { scope: 'managed', file: paths.managed },
91
+ { scope: 'user', file: paths.user },
92
+ { scope: 'project', file: paths.project },
93
+ { scope: 'local', file: paths.local },
68
94
  ];
69
95
 
70
96
  let merged = deepClone(SETTINGS_SCHEMA);
97
+ const sources = {};
71
98
 
72
- for (const file of chain) {
99
+ // Track defaults
100
+ trackSources(sources, SETTINGS_SCHEMA, 'default', '');
101
+
102
+ for (const { scope, file } of chain) {
73
103
  try {
74
104
  const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
75
105
  merged = deepMerge(merged, data);
106
+ trackSources(sources, data, scope, '');
76
107
  } catch {
77
- // File not found or invalid
108
+ // File not found or invalid — skip
78
109
  }
79
110
  }
80
111
 
81
- applyEnvOverrides(merged);
112
+ applyEnvOverrides(merged, sources);
113
+
114
+ // Attach metadata (non-enumerable so it does not interfere with tests)
115
+ Object.defineProperty(merged, '_sources', { value: sources, enumerable: false, writable: true });
116
+ Object.defineProperty(merged, '_paths', { value: paths, enumerable: false, writable: true });
117
+
82
118
  return merged;
83
119
  }
84
120
 
121
+ /**
122
+ * Save a single setting back to the correct scope file.
123
+ * @param {'user'|'project'|'local'} scope
124
+ * @param {string} key — dot-separated path (e.g. "permissions.defaultMode")
125
+ * @param {*} value
126
+ * @param {string} [cwd]
127
+ */
128
+ export function saveSettings(scope, key, value, cwd) {
129
+ const paths = getSettingsPaths(cwd);
130
+ const file = paths[scope];
131
+ if (!file) throw new Error(`Unknown scope: ${scope}`);
132
+
133
+ let existing = {};
134
+ try {
135
+ existing = JSON.parse(fs.readFileSync(file, 'utf-8'));
136
+ } catch {
137
+ // New file
138
+ }
139
+
140
+ setNestedValue(existing, key, value);
141
+
142
+ const dir = path.dirname(file);
143
+ fs.mkdirSync(dir, { recursive: true });
144
+ fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
145
+ }
146
+
147
+ /**
148
+ * Get the source scope for a specific setting key.
149
+ * @param {object} settings — settings object returned by loadSettings
150
+ * @param {string} key — dot-separated path
151
+ * @returns {string} scope name ('default', 'user', 'project', 'local', 'managed', 'env')
152
+ */
153
+ export function getSettingSource(settings, key) {
154
+ return settings._sources?.[key] || 'default';
155
+ }
156
+
157
+ // ---------- Helpers ----------
158
+
85
159
  function deepClone(obj) {
86
160
  return JSON.parse(JSON.stringify(obj));
87
161
  }
@@ -89,7 +163,8 @@ function deepClone(obj) {
89
163
  function deepMerge(target, source) {
90
164
  const result = { ...target };
91
165
  for (const [key, value] of Object.entries(source)) {
92
- if (value && typeof value === 'object' && !Array.isArray(value) && typeof result[key] === 'object' && !Array.isArray(result[key])) {
166
+ if (value && typeof value === 'object' && !Array.isArray(value)
167
+ && typeof result[key] === 'object' && !Array.isArray(result[key])) {
93
168
  result[key] = deepMerge(result[key] || {}, value);
94
169
  } else {
95
170
  result[key] = value;
@@ -98,22 +173,53 @@ function deepMerge(target, source) {
98
173
  return result;
99
174
  }
100
175
 
101
- function applyEnvOverrides(settings) {
102
- if (process.env.ANTHROPIC_MODEL) settings.model = process.env.ANTHROPIC_MODEL;
103
- if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) settings.subagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL;
104
- if (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
105
- const n = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10);
106
- if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
176
+ function trackSources(sources, obj, scope, prefix) {
177
+ for (const [key, value] of Object.entries(obj)) {
178
+ const fullKey = prefix ? `${prefix}.${key}` : key;
179
+ sources[fullKey] = scope;
180
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
181
+ trackSources(sources, value, scope, fullKey);
182
+ }
183
+ }
184
+ }
185
+
186
+ function setNestedValue(obj, key, value) {
187
+ const parts = key.split('.');
188
+ let cur = obj;
189
+ for (let i = 0; i < parts.length - 1; i++) {
190
+ if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
191
+ cur[parts[i]] = {};
192
+ }
193
+ cur = cur[parts[i]];
107
194
  }
108
- if (process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) {
109
- const n = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10);
110
- if (!isNaN(n)) settings.maxContextTokens = n;
195
+ cur[parts[parts.length - 1]] = value;
196
+ }
197
+
198
+ function applyEnvOverrides(settings, sources) {
199
+ const envMap = [
200
+ ['ANTHROPIC_MODEL', (v) => { settings.model = v; }, 'model'],
201
+ ['CLAUDE_CODE_SUBAGENT_MODEL', (v) => { settings.subagentModel = v; }, 'subagentModel'],
202
+ ['CLAUDE_CODE_MAX_OUTPUT_TOKENS', (v) => {
203
+ const n = parseInt(v, 10);
204
+ if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
205
+ }, 'maxOutputTokens'],
206
+ ['CLAUDE_CODE_MAX_CONTEXT_TOKENS', (v) => {
207
+ const n = parseInt(v, 10);
208
+ if (!isNaN(n)) settings.maxContextTokens = n;
209
+ }, 'maxContextTokens'],
210
+ ['CLAUDE_CODE_BRIEF', (v) => { if (v === '1') settings.briefMode = true; }, 'briefMode'],
211
+ ['CLAUDE_CODE_DEBUG', (v) => { if (v === '1') settings.debugMode = true; }, 'debugMode'],
212
+ ['CLAUDE_CODE_PERMISSION_MODE', (v) => { settings.permissions.defaultMode = v; }, 'permissions.defaultMode'],
213
+ ['CLAUDE_CODE_STREAMING', (v) => { if (v === '0') settings.stream = false; }, 'stream'],
214
+ ['CLAUDE_CODE_THINKING', (v) => { if (v === '1') settings.alwaysThinkingEnabled = true; }, 'alwaysThinkingEnabled'],
215
+ ['CLAUDE_CODE_DISABLE_CRON', (v) => { if (v === '1') settings.cronEnabled = false; }, 'cronEnabled'],
216
+ ['CLAUDE_CODE_ENABLE_TASKS', (v) => { if (v === '1') settings.enableTeams = true; }, 'enableTeams'],
217
+ ];
218
+
219
+ for (const [envKey, apply, settingsKey] of envMap) {
220
+ if (process.env[envKey]) {
221
+ apply(process.env[envKey]);
222
+ if (sources) sources[settingsKey] = 'env';
223
+ }
111
224
  }
112
- if (process.env.CLAUDE_CODE_BRIEF === '1') settings.briefMode = true;
113
- if (process.env.CLAUDE_CODE_DEBUG === '1') settings.debugMode = true;
114
- if (process.env.CLAUDE_CODE_PERMISSION_MODE) settings.permissions.defaultMode = process.env.CLAUDE_CODE_PERMISSION_MODE;
115
- if (process.env.CLAUDE_CODE_STREAMING === '0') settings.stream = false;
116
- if (process.env.CLAUDE_CODE_THINKING === '1') settings.alwaysThinkingEnabled = true;
117
- if (process.env.CLAUDE_CODE_DISABLE_CRON === '1') settings.cronEnabled = false;
118
- if (process.env.CLAUDE_CODE_ENABLE_TASKS === '1') settings.enableTeams = true;
119
225
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Session Manager — save, resume, and teleport sessions.
2
+ * Session Manager — save, resume, list, and teleport sessions.
3
3
  *
4
4
  * Sessions are stored at ~/.claude/projects/<hash>/session.json
5
5
  * and contain conversation history, token usage, and metadata.
6
+ * Supports listing recent sessions and resuming by ID.
6
7
  */
7
8
 
8
9
  import fs from 'fs';
@@ -31,8 +32,16 @@ export class SessionManager {
31
32
  }
32
33
 
33
34
  /**
34
- * Save the current session state.
35
+ * Get the base projects directory (for listing all sessions).
36
+ */
37
+ static getProjectsDir() {
38
+ return path.join(os.homedir(), '.claude', 'projects');
39
+ }
40
+
41
+ /**
42
+ * Save the current session state (messages + full state).
35
43
  * @param {object} state - agent loop state to save
44
+ * @returns {string} path to saved session file
36
45
  */
37
46
  save(state) {
38
47
  const dir = this.getSessionDir();
@@ -51,28 +60,101 @@ export class SessionManager {
51
60
  systemPrompt: state.systemPrompt,
52
61
  };
53
62
 
63
+ // Save current session
54
64
  const filePath = path.join(dir, 'session.json');
55
65
  fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
66
+
67
+ // Also save to sessions history for listing
68
+ this._saveToHistory(dir, session);
69
+
56
70
  return filePath;
57
71
  }
58
72
 
59
73
  /**
60
- * Resume a saved session.
74
+ * Save a minimal entry to sessions history for listing.
75
+ */
76
+ _saveToHistory(dir, session) {
77
+ const historyFile = path.join(dir, 'sessions-history.json');
78
+ let history = [];
79
+ try {
80
+ history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
81
+ } catch { /* new file */ }
82
+
83
+ // Remove old entry with same ID
84
+ history = history.filter(h => h.id !== session.id);
85
+
86
+ history.push({
87
+ id: session.id,
88
+ projectDir: session.projectDir,
89
+ startedAt: session.startedAt,
90
+ savedAt: session.savedAt,
91
+ model: session.model,
92
+ turnCount: session.turnCount,
93
+ tokenUsage: session.tokenUsage,
94
+ messageCount: (session.messages || []).length,
95
+ });
96
+
97
+ // Keep last 50
98
+ if (history.length > 50) history = history.slice(-50);
99
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
100
+ }
101
+
102
+ /**
103
+ * Load and restore a saved session into agent loop state.
104
+ * Fully restores messages so conversation can continue.
61
105
  * @param {object} state - agent loop state to restore into
106
+ * @param {string} [sessionId] - optional specific session ID to load
62
107
  * @returns {boolean} true if session was restored
63
108
  */
64
- resume(state) {
109
+ resume(state, sessionId) {
65
110
  const dir = this.getSessionDir();
111
+
112
+ if (sessionId) {
113
+ // Look up specific session from history
114
+ return this._resumeById(state, dir, sessionId);
115
+ }
116
+
117
+ // Load most recent session
66
118
  const filePath = path.join(dir, 'session.json');
119
+ return this._loadSessionFile(state, filePath);
120
+ }
67
121
 
122
+ /**
123
+ * Resume a specific session by ID.
124
+ */
125
+ _resumeById(state, dir, sessionId) {
126
+ // Check if it is the current session.json
127
+ const mainFile = path.join(dir, 'session.json');
128
+ try {
129
+ const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
130
+ if (data.id === sessionId) {
131
+ return this._loadSessionFile(state, mainFile);
132
+ }
133
+ } catch { /* continue searching */ }
134
+
135
+ // Check archived sessions
136
+ const archiveFile = path.join(dir, `session-${sessionId}.json`);
137
+ try {
138
+ return this._loadSessionFile(state, archiveFile);
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load a session from a file path.
146
+ */
147
+ _loadSessionFile(state, filePath) {
68
148
  try {
69
149
  const raw = fs.readFileSync(filePath, 'utf-8');
70
150
  const session = JSON.parse(raw);
71
151
 
152
+ // Restore all messages so the conversation continues
72
153
  state.messages = session.messages || [];
73
154
  state.turnCount = session.turnCount || 0;
74
155
  state.tokenUsage = session.tokenUsage || { input: 0, output: 0 };
75
156
  if (session.model) state.model = session.model;
157
+ if (session.systemPrompt) state.systemPrompt = session.systemPrompt;
76
158
 
77
159
  this.sessionId = session.id;
78
160
  this.conversationId = session.conversationId;
@@ -84,6 +166,65 @@ export class SessionManager {
84
166
  }
85
167
  }
86
168
 
169
+ /**
170
+ * List recent sessions for the current project.
171
+ * @param {number} [limit=10]
172
+ * @returns {Array<{id, projectDir, startedAt, savedAt, model, turnCount, messageCount}>}
173
+ */
174
+ listSessions(limit = 10) {
175
+ const dir = this.getSessionDir();
176
+ const historyFile = path.join(dir, 'sessions-history.json');
177
+ try {
178
+ const history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
179
+ return history.slice(-limit).reverse();
180
+ } catch {
181
+ // Try loading the single session.json as fallback
182
+ const mainFile = path.join(dir, 'session.json');
183
+ try {
184
+ const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
185
+ return [{
186
+ id: data.id,
187
+ projectDir: data.projectDir,
188
+ startedAt: data.startedAt,
189
+ savedAt: data.savedAt,
190
+ model: data.model,
191
+ turnCount: data.turnCount,
192
+ messageCount: (data.messages || []).length,
193
+ }];
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Check if there is a resumable session for the current project.
202
+ * @returns {{ resumable: boolean, session?: object }}
203
+ */
204
+ checkResumable() {
205
+ const dir = this.getSessionDir();
206
+ const filePath = path.join(dir, 'session.json');
207
+ try {
208
+ const raw = fs.readFileSync(filePath, 'utf-8');
209
+ const session = JSON.parse(raw);
210
+ const age = Date.now() - new Date(session.savedAt).getTime();
211
+ // Only resumable if saved within 24h
212
+ if (age < 86400000) {
213
+ return {
214
+ resumable: true,
215
+ session: {
216
+ id: session.id,
217
+ savedAt: session.savedAt,
218
+ turnCount: session.turnCount,
219
+ messageCount: (session.messages || []).length,
220
+ model: session.model,
221
+ },
222
+ };
223
+ }
224
+ } catch { /* no session */ }
225
+ return { resumable: false };
226
+ }
227
+
87
228
  /**
88
229
  * Export session for teleport (transfer between machines).
89
230
  * @param {object} state - agent loop state
@@ -2,8 +2,9 @@
2
2
  * System Prompt Builder — loads and merges CLAUDE.md files.
3
3
  *
4
4
  * Features:
5
- * - Loads CLAUDE.md from: ~/.claude/CLAUDE.md, project root, parent dirs
6
- * - Merges in order (global -> project -> local)
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)
7
8
  * - Splits at cache boundary (static prefix cached, dynamic suffix not)
8
9
  * - Includes tool schemas in the system prompt
9
10
  */
@@ -13,42 +14,56 @@ import os from 'os';
13
14
 
14
15
  /**
15
16
  * Load all CLAUDE.md files and merge them in order.
17
+ * Walk up parent directories to find all relevant files.
16
18
  * @param {string} [cwd] - current working directory
17
- * @returns {string[]} Array of CLAUDE.md contents in merge order
19
+ * @param {string[]} [addDirs] - additional directories to search
20
+ * @returns {Array<{source: string, content: string, path?: string}>}
18
21
  */
19
- export function loadClaudeMdFiles(cwd = process.cwd()) {
22
+ export function loadClaudeMdFiles(cwd = process.cwd(), addDirs = []) {
20
23
  const files = [];
21
-
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 */ }
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
+ }
28
37
  }
29
38
 
30
- // 2. Walk from cwd up to root, collecting CLAUDE.md files
31
- const projectFiles = [];
39
+ // 1. Global: ~/.claude/CLAUDE.md (lowest priority in the project chain)
40
+ tryAdd(path.join(os.homedir(), '.claude', 'CLAUDE.md'), 'global');
41
+
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 = [];
32
45
  let dir = path.resolve(cwd);
33
46
  const root = path.parse(dir).root;
47
+
34
48
  while (dir !== root) {
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
- }
49
+ parentPaths.push(dir);
46
50
  dir = path.dirname(dir);
47
51
  }
48
52
 
49
- // Reverse so parent dirs come first (global -> project -> local)
50
- projectFiles.reverse();
51
- files.push(...projectFiles);
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
+ }
52
67
 
53
68
  return files;
54
69
  }
@@ -60,29 +75,17 @@ export function loadClaudeMdFiles(cwd = process.cwd()) {
60
75
  * @param {Array} [options.tools] - tool definitions for schema inclusion
61
76
  * @param {string} [options.override] - override system prompt entirely
62
77
  * @param {string[]} [options.addDirs] - additional directories to search for CLAUDE.md
63
- * @returns {{ staticPrefix: string, dynamicSuffix: string, full: string }}
78
+ * @returns {{ staticPrefix: string, dynamicSuffix: string, full: string, mdFiles: Array }}
64
79
  */
65
80
  export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
66
81
  if (override) {
67
- return { staticPrefix: override, dynamicSuffix: '', full: override };
82
+ return { staticPrefix: override, dynamicSuffix: '', full: override, mdFiles: [] };
68
83
  }
69
84
 
70
85
  const parts = ['You are an AI coding assistant.'];
71
86
 
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
- }
87
+ // Load CLAUDE.md files (multi-directory merge)
88
+ const mdFiles = loadClaudeMdFiles(cwd, addDirs || []);
86
89
 
87
90
  for (const f of mdFiles) {
88
91
  parts.push(f.content);
@@ -104,6 +107,7 @@ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
104
107
  staticPrefix,
105
108
  dynamicSuffix,
106
109
  full: staticPrefix + dynamicSuffix,
110
+ mdFiles,
107
111
  };
108
112
  }
109
113
 
package/src/index.mjs CHANGED
@@ -12,12 +12,14 @@
12
12
  * - 6 permission modes + sandbox
13
13
  * - Context compaction + auto-compaction
14
14
  * - Hooks system (7 events)
15
- * - Settings chain (5 layers, 76 properties)
15
+ * - Settings chain (4 sources, 76 properties, deep merge, write-back)
16
16
  * - Multi-provider support (Anthropic, OpenAI, Google)
17
17
  * - Custom agents and skills
18
- * - Session management and checkpoints
18
+ * - Session management with save/resume/list
19
+ * - File checkpoints with undo
19
20
  * - Prompt caching
20
- * - 39 slash commands
21
+ * - 39+ slash commands (all functional)
22
+ * - Rich TUI with cost display
21
23
  * - Telemetry stub
22
24
  */
23
25
 
@@ -34,6 +36,7 @@ import { SessionManager } from './core/session.mjs';
34
36
  import { CheckpointManager } from './core/checkpoints.mjs';
35
37
  import { PromptCache } from './core/cache.mjs';
36
38
  import { readEnv } from './config/env.mjs';
39
+ import { renderCostInline } from './ui/ink-app.mjs';
37
40
  import * as telemetry from './telemetry/index.mjs';
38
41
 
39
42
  async function main() {
@@ -41,7 +44,7 @@ async function main() {
41
44
 
42
45
  // Handle --version
43
46
  if (args.showVersion) {
44
- console.log('open-claude-code v2.0.0-alpha.1');
47
+ console.log('open-claude-code v2.0.1');
45
48
  process.exit(0);
46
49
  }
47
50
 
@@ -63,7 +66,12 @@ async function main() {
63
66
  if (args.debug) settings.debug = true;
64
67
 
65
68
  const tools = createToolRegistry();
66
- const permissions = createPermissionChecker(settings.permissions);
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);
67
75
  const hooks = new HookEngine(settings.hooks);
68
76
 
69
77
  // Apply tool allow/deny lists
@@ -127,8 +135,14 @@ async function main() {
127
135
 
128
136
  telemetry.track('session.start', { model: loop.state.model });
129
137
 
130
- // Graceful shutdown
138
+ // Graceful shutdown — save session before exit
131
139
  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
+ }
132
146
  telemetry.track('session.end', {
133
147
  turns: loop.state.turnCount,
134
148
  tokens: loop.state.tokenUsage,
@@ -142,6 +156,7 @@ async function main() {
142
156
 
143
157
  if (args.prompt) {
144
158
  // Non-interactive: run prompt and exit
159
+ // In print mode, permissions auto-deny (no rl passed)
145
160
  const outputFormat = args.outputFormat || 'text';
146
161
  const results = [];
147
162
 
@@ -172,9 +187,16 @@ async function main() {
172
187
 
173
188
  await cleanup();
174
189
  } else {
175
- // Interactive REPL
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`);
196
+ }
197
+
176
198
  const { startRepl } = await import('./ui/repl.mjs');
177
- await startRepl(loop, settings);
199
+ await startRepl(loop, settings, permissions);
178
200
  await cleanup();
179
201
  }
180
202
  }