@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 +1 -1
- package/src/config/env.mjs +2 -0
- package/src/config/settings.mjs +130 -24
- package/src/core/session.mjs +145 -4
- package/src/core/system-prompt.mjs +47 -43
- package/src/index.mjs +30 -8
- package/src/permissions/checker.mjs +79 -8
- package/src/tools/bash.mjs +40 -3
- package/src/ui/commands.mjs +70 -83
- package/src/ui/ink-app.mjs +52 -13
- package/src/ui/repl.mjs +25 -10
- package/test/test.mjs +800 -0
package/package.json
CHANGED
package/src/config/env.mjs
CHANGED
|
@@ -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' },
|
package/src/config/settings.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
}
|
package/src/core/session.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
6
|
-
* -
|
|
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
|
-
* @
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
//
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
}
|