sandboxbox 3.0.48 ā 3.0.49
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/package/CLAUDE.md +200 -0
- package/package/package/Dockerfile +95 -0
- package/package/package/README.md +242 -0
- package/package/package/claude-settings.json +85 -0
- package/package/package/cli.js +90 -0
- package/package/package/package.json +39 -0
- package/package/package/sandboxbox-3.0.45.tgz +0 -0
- package/package/package/sandboxbox-settings.json +40 -0
- package/package/package/test.txt +1 -0
- package/package/package/utils/claude-optimizer.js +129 -0
- package/package/package/utils/colors.js +15 -0
- package/package/package/utils/commands/claude.js +501 -0
- package/package/package/utils/commands/container.js +60 -0
- package/package/package/utils/commands/index.js +23 -0
- package/package/package/utils/sandbox.js +341 -0
- package/package/package/utils/system-optimizer.js +231 -0
- package/package/package/utils/ui.js +38 -0
- package/package/package.json +1 -1
- package/package/sandboxbox-3.0.46.tgz +0 -0
- package/package/utils/commands/claude.js +7 -6
- package/package.json +1 -1
- package/sandboxbox-3.0.48.tgz +0 -0
- package/sandboxbox-settings.json +16 -1
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, appendFileSync } from 'fs';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import { color } from '../colors.js';
|
|
5
|
+
import { createSandbox, createSandboxEnv } from '../sandbox.js';
|
|
6
|
+
import { ClaudeOptimizer } from '../claude-optimizer.js';
|
|
7
|
+
import { SystemOptimizer } from '../system-optimizer.js';
|
|
8
|
+
|
|
9
|
+
const ALLOWED_TOOLS = [
|
|
10
|
+
'Task', 'Bash', 'Glob', 'Grep', 'Read', 'Edit', 'Write', 'NotebookEdit',
|
|
11
|
+
'WebFetch', 'TodoWrite', 'WebSearch', 'BashOutput', 'KillShell',
|
|
12
|
+
'SlashCommand', 'ExitPlanMode', 'mcp__glootie__execute',
|
|
13
|
+
'mcp__glootie__ast_tool', 'mcp__glootie__caveat',
|
|
14
|
+
'mcp__playwright__browser_navigate', 'mcp__playwright__browser_snapshot',
|
|
15
|
+
'mcp__playwright__browser_click', 'mcp__playwright__browser_type',
|
|
16
|
+
'mcp__playwright__browser_evaluate', 'mcp__playwright__browser_close',
|
|
17
|
+
'mcp__vexify__search_code'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Console output configuration
|
|
21
|
+
const MAX_CONSOLE_LINES = parseInt(process.env.SANDBOX_MAX_CONSOLE_LINES) || 5;
|
|
22
|
+
const MAX_LOG_ENTRY_LENGTH = parseInt(process.env.SANDBOX_MAX_LOG_LENGTH) || 200;
|
|
23
|
+
const ENABLE_FILE_LOGGING = process.env.SANDBOX_ENABLE_FILE_LOGGING === 'true';
|
|
24
|
+
const VERBOSE_OUTPUT = process.env.SANDBOX_VERBOSE === 'true' || process.argv.includes('--verbose');
|
|
25
|
+
global.toolCallLog = [];
|
|
26
|
+
global.logFileHandle = null;
|
|
27
|
+
global.pendingToolCalls = new Map(); // Track tool calls by ID for result matching
|
|
28
|
+
global.conversationalBuffer = ''; // Track conversational text between tool calls
|
|
29
|
+
|
|
30
|
+
// Helper function to extract tool metadata without showing actual content
|
|
31
|
+
function extractToolMetadata(toolUse) {
|
|
32
|
+
const metadata = {
|
|
33
|
+
name: toolUse.name || 'unknown',
|
|
34
|
+
id: toolUse.id || 'no-id',
|
|
35
|
+
inputCount: 0,
|
|
36
|
+
inputTypes: {},
|
|
37
|
+
inputSizes: {},
|
|
38
|
+
totalInputSize: 0
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (toolUse.input && typeof toolUse.input === 'object') {
|
|
42
|
+
metadata.inputCount = Object.keys(toolUse.input).length;
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Object.entries(toolUse.input)) {
|
|
45
|
+
const type = Array.isArray(value) ? 'array' : typeof value;
|
|
46
|
+
metadata.inputTypes[key] = type;
|
|
47
|
+
|
|
48
|
+
// Calculate sizes without exposing content
|
|
49
|
+
if (type === 'string') {
|
|
50
|
+
metadata.inputSizes[key] = `${value.length} chars`;
|
|
51
|
+
metadata.totalInputSize += value.length;
|
|
52
|
+
} else if (type === 'array') {
|
|
53
|
+
metadata.inputSizes[key] = `${value.length} items`;
|
|
54
|
+
metadata.totalInputSize += JSON.stringify(value).length;
|
|
55
|
+
} else if (type === 'object' && value !== null) {
|
|
56
|
+
const objSize = JSON.stringify(value).length;
|
|
57
|
+
metadata.inputSizes[key] = `${objSize} chars`;
|
|
58
|
+
metadata.totalInputSize += objSize;
|
|
59
|
+
} else {
|
|
60
|
+
metadata.inputSizes[key] = `${type}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return metadata;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper function to extract tool result metadata
|
|
69
|
+
function extractResultMetadata(result) {
|
|
70
|
+
const metadata = {
|
|
71
|
+
type: 'unknown',
|
|
72
|
+
size: 0,
|
|
73
|
+
hasContent: false,
|
|
74
|
+
isToolResult: false,
|
|
75
|
+
isError: false
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (result && typeof result === 'object') {
|
|
79
|
+
metadata.isToolResult = result.type === 'tool_result';
|
|
80
|
+
metadata.isError = result.is_error || false;
|
|
81
|
+
|
|
82
|
+
if (result.content) {
|
|
83
|
+
metadata.hasContent = true;
|
|
84
|
+
|
|
85
|
+
if (typeof result.content === 'string') {
|
|
86
|
+
metadata.type = 'text';
|
|
87
|
+
metadata.size = result.content.length;
|
|
88
|
+
} else if (Array.isArray(result.content)) {
|
|
89
|
+
metadata.type = 'array';
|
|
90
|
+
metadata.size = result.content.length;
|
|
91
|
+
// Count items by type without showing content
|
|
92
|
+
const typeCounts = {};
|
|
93
|
+
result.content.forEach(item => {
|
|
94
|
+
const itemType = item?.type || 'unknown';
|
|
95
|
+
typeCounts[itemType] = (typeCounts[itemType] || 0) + 1;
|
|
96
|
+
});
|
|
97
|
+
metadata.itemTypes = typeCounts;
|
|
98
|
+
} else if (typeof result.content === 'object') {
|
|
99
|
+
metadata.type = 'object';
|
|
100
|
+
metadata.size = Object.keys(result.content).length;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return metadata;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// Helper function to truncate text to a sensible length
|
|
110
|
+
function truncateText(text, maxLength = MAX_LOG_ENTRY_LENGTH) {
|
|
111
|
+
if (text.length <= maxLength) return text;
|
|
112
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Enhanced tool logging with detailed metadata and length limiting
|
|
116
|
+
function logToolCall(toolName, action = 'call', toolUse = null, result = null) {
|
|
117
|
+
const shortTime = new Date().toLocaleTimeString();
|
|
118
|
+
let logEntry = `Tool: ${toolName}`;
|
|
119
|
+
|
|
120
|
+
// Add conversational text if available
|
|
121
|
+
if (global.conversationalBuffer.trim()) {
|
|
122
|
+
const truncatedText = truncateText(global.conversationalBuffer.trim(), 80);
|
|
123
|
+
logEntry += ` - "${truncatedText}"`;
|
|
124
|
+
global.conversationalBuffer = ''; // Clear buffer after using
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add compact metadata
|
|
128
|
+
if (toolUse && action === 'call') {
|
|
129
|
+
const metadata = extractToolMetadata(toolUse);
|
|
130
|
+
const metaInfo = [];
|
|
131
|
+
|
|
132
|
+
if (metadata.inputCount > 0) {
|
|
133
|
+
metaInfo.push(`${metadata.inputCount} inputs`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (metadata.totalInputSize > 0) {
|
|
137
|
+
metaInfo.push(`${metadata.totalInputSize} chars`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (metaInfo.length > 0) {
|
|
141
|
+
logEntry += ` (${metaInfo.join(', ')})`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add result metadata
|
|
146
|
+
if (result && action === 'result') {
|
|
147
|
+
const metadata = extractResultMetadata(result);
|
|
148
|
+
const resultInfo = [];
|
|
149
|
+
|
|
150
|
+
if (metadata.hasContent) {
|
|
151
|
+
if (metadata.size > 0) {
|
|
152
|
+
if (metadata.type === 'text') {
|
|
153
|
+
resultInfo.push(`${metadata.size} chars`);
|
|
154
|
+
} else if (metadata.type === 'array') {
|
|
155
|
+
resultInfo.push(`${metadata.size} items`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (metadata.isError) {
|
|
160
|
+
resultInfo.push('ERROR');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (resultInfo.length > 0) {
|
|
165
|
+
logEntry += ` ā ${resultInfo.join(', ')}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const finalLogEntry = `[${shortTime}] ${truncateText(logEntry)}`;
|
|
170
|
+
|
|
171
|
+
global.toolCallLog.push(finalLogEntry);
|
|
172
|
+
|
|
173
|
+
// Keep only the last MAX_CONSOLE_LINES for console display
|
|
174
|
+
if (global.toolCallLog.length > MAX_CONSOLE_LINES) {
|
|
175
|
+
global.toolCallLog = global.toolCallLog.slice(-MAX_CONSOLE_LINES);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Optionally log to file with full metadata
|
|
179
|
+
if (ENABLE_FILE_LOGGING) {
|
|
180
|
+
try {
|
|
181
|
+
if (!global.logFileHandle) {
|
|
182
|
+
const timestamp = new Date().toISOString();
|
|
183
|
+
const logFileName = `sandboxbox-tool-calls-${Date.now()}.log`;
|
|
184
|
+
writeFileSync(logFileName, `# SandboxBox Tool Calls Log\n# Started: ${timestamp}\n# Format: [timestamp] Tool: name - "text" (metadata)\n\n`);
|
|
185
|
+
global.logFileHandle = logFileName;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const timestamp = new Date().toISOString();
|
|
189
|
+
let fileLogEntry = `[${timestamp}] Tool: ${toolName}`;
|
|
190
|
+
|
|
191
|
+
if (global.conversationalBuffer.trim()) {
|
|
192
|
+
fileLogEntry += ` - "${global.conversationalBuffer.trim()}"`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (toolUse && action === 'call') {
|
|
196
|
+
const metadata = extractToolMetadata(toolUse);
|
|
197
|
+
fileLogEntry += `\n Input details: ${JSON.stringify(metadata.inputSizes, null, 2)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (result && action === 'result') {
|
|
201
|
+
const metadata = extractResultMetadata(result);
|
|
202
|
+
fileLogEntry += `\n Result details: ${JSON.stringify(metadata, null, 2)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
appendFileSync(global.logFileHandle, fileLogEntry + '\n');
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Don't fail if logging fails
|
|
208
|
+
console.log(color('yellow', `ā ļø Could not write to log file: ${error.message}`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Function to log conversational text
|
|
214
|
+
function logConversationalText(text) {
|
|
215
|
+
if (text && text.trim()) {
|
|
216
|
+
global.conversationalBuffer += text + ' ';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Helper function to display recent tool calls
|
|
221
|
+
function displayRecentToolCalls() {
|
|
222
|
+
if (global.toolCallLog.length > 0) {
|
|
223
|
+
console.log(color('cyan', `\nš§ Recent tool calls (showing last ${global.toolCallLog.length}):`));
|
|
224
|
+
global.toolCallLog.forEach(log => {
|
|
225
|
+
console.log(color('cyan', ` ${log}`));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
export async function claudeCommand(projectDir, prompt, flags = {}) {
|
|
232
|
+
if (!existsSync(projectDir)) {
|
|
233
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Note: We don't require git repository anymore - the sandbox will initialize it if needed
|
|
238
|
+
|
|
239
|
+
// Silent start - only show conversation unless verbose
|
|
240
|
+
|
|
241
|
+
const startTime = Date.now();
|
|
242
|
+
if (VERBOSE_OUTPUT) console.log(color('cyan', 'ā±ļø Stage 1: Creating sandbox...'));
|
|
243
|
+
|
|
244
|
+
const { useHostSettings, headlessMode } = flags;
|
|
245
|
+
const { sandboxDir, cleanup } = createSandbox(projectDir, { useHostSettings, headlessMode });
|
|
246
|
+
const sandboxCreateTime = Date.now() - startTime;
|
|
247
|
+
if (VERBOSE_OUTPUT) console.log(color('green', `ā
Sandbox created in ${sandboxCreateTime}ms`));
|
|
248
|
+
|
|
249
|
+
process.on('SIGINT', cleanup);
|
|
250
|
+
process.on('SIGTERM', cleanup);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const envStartTime = Date.now();
|
|
254
|
+
if (VERBOSE_OUTPUT) console.log(color('cyan', 'ā±ļø Stage 2: Setting up environment...'));
|
|
255
|
+
|
|
256
|
+
// Apply Claude optimizations
|
|
257
|
+
const claudeOptimizer = new ClaudeOptimizer(sandboxDir);
|
|
258
|
+
claudeOptimizer.optimizeSettings();
|
|
259
|
+
await claudeOptimizer.prewarmPlugins();
|
|
260
|
+
|
|
261
|
+
// Apply system optimizations (with sudo access)
|
|
262
|
+
const systemOptimizer = new SystemOptimizer();
|
|
263
|
+
const systemOptimizationsApplied = await systemOptimizer.optimizeSystem();
|
|
264
|
+
|
|
265
|
+
// Create optimized environment
|
|
266
|
+
const baseEnv = createSandboxEnv(sandboxDir, {
|
|
267
|
+
CLAUDECODE: '1'
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const env = systemOptimizationsApplied
|
|
271
|
+
? { ...baseEnv, ...systemOptimizer.createOptimizedContainerEnv() }
|
|
272
|
+
: claudeOptimizer.createOptimizedEnv(baseEnv);
|
|
273
|
+
|
|
274
|
+
const envCreateTime = Date.now() - envStartTime;
|
|
275
|
+
if (VERBOSE_OUTPUT) console.log(color('green', `ā
Environment configured in ${envCreateTime}ms`));
|
|
276
|
+
|
|
277
|
+
if (systemOptimizationsApplied && VERBOSE_OUTPUT) {
|
|
278
|
+
console.log(color('yellow', `š System-level optimizations applied`));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const claudeArgs = [
|
|
282
|
+
'--verbose',
|
|
283
|
+
'--output-format', 'stream-json',
|
|
284
|
+
'--permission-mode', 'bypassPermissions',
|
|
285
|
+
'--allowed-tools', ALLOWED_TOOLS.join(',')
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const claudeStartTime = Date.now();
|
|
290
|
+
if (VERBOSE_OUTPUT) console.log(color('cyan', 'ā±ļø Stage 3: Starting Claude Code...'));
|
|
291
|
+
|
|
292
|
+
const workspacePath = join(sandboxDir, 'workspace');
|
|
293
|
+
|
|
294
|
+
// Handle headless mode Playwright MCP reconfiguration
|
|
295
|
+
if (headlessMode) {
|
|
296
|
+
if (VERBOSE_OUTPUT) console.log(color('yellow', 'š Configuring headless Playwright MCP...'));
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Remove existing Playwright MCP (ignoring failures)
|
|
300
|
+
execSync('claude mcp remove playwright', {
|
|
301
|
+
cwd: workspacePath,
|
|
302
|
+
stdio: 'pipe',
|
|
303
|
+
shell: true
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// Ignore removal failures
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
// Add headless Playwright MCP
|
|
311
|
+
execSync('claude mcp add --headless --no-sandbox --scope=user playwright npx u/playwright/mcp@latest', {
|
|
312
|
+
cwd: workspacePath,
|
|
313
|
+
stdio: VERBOSE_OUTPUT ? 'inherit' : 'pipe',
|
|
314
|
+
shell: true
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (VERBOSE_OUTPUT) console.log(color('green', 'ā
Headless Playwright MCP configured'));
|
|
318
|
+
} catch (e) {
|
|
319
|
+
console.log(color('red', `ā Failed to configure headless Playwright MCP: ${e.message}`));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Modify the prompt to include directory change instruction
|
|
324
|
+
const modifiedPrompt = `You are working in a sandboxed environment. Your working directory is "${workspacePath}". All operations should be performed in this directory. ${prompt}`;
|
|
325
|
+
|
|
326
|
+
// Add network isolation environment variables for process-level isolation
|
|
327
|
+
const networkIsolatedEnv = {
|
|
328
|
+
...env,
|
|
329
|
+
// Force network isolation through environment variables
|
|
330
|
+
SANDBOX_NETWORK_ISOLATED: 'true',
|
|
331
|
+
// Add unique sandbox identifier for port isolation
|
|
332
|
+
SANDBOX_ID: Math.random().toString(36).substr(2, 9),
|
|
333
|
+
// Restrict network binding to localhost when possible
|
|
334
|
+
NODE_OPTIONS: (env.NODE_OPTIONS || '') + ' --no-force-async-hooks-checks'
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const proc = spawn('claude', claudeArgs, {
|
|
338
|
+
cwd: workspacePath, // Set working directory directly
|
|
339
|
+
env: networkIsolatedEnv, // Use network-isolated environment
|
|
340
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
341
|
+
shell: false, // Don't use shell since we're setting cwd directly
|
|
342
|
+
detached: false
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
let claudeStarted = false;
|
|
346
|
+
let jsonBuffer = ''; // Buffer for incomplete JSON lines
|
|
347
|
+
|
|
348
|
+
function handleEvent(event) {
|
|
349
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
350
|
+
if (!claudeStarted) {
|
|
351
|
+
const claudeCreateTime = Date.now() - claudeStartTime;
|
|
352
|
+
if (VERBOSE_OUTPUT) console.log(color('green', `ā
Claude Code started in ${claudeCreateTime}ms`));
|
|
353
|
+
claudeStarted = true;
|
|
354
|
+
}
|
|
355
|
+
if (VERBOSE_OUTPUT) console.log(color('green', `ā
Session started (${event.session_id.substring(0, 8)}...)`));
|
|
356
|
+
if (VERBOSE_OUTPUT) console.log(color('cyan', `š¦ Model: ${event.model}`));
|
|
357
|
+
|
|
358
|
+
// Simple tool count warning only if less than 15 tools
|
|
359
|
+
if (event.tools.length < 15) {
|
|
360
|
+
console.log(color('yellow', `ā ļø Only ${event.tools.length} tools available`));
|
|
361
|
+
}
|
|
362
|
+
} else if (event.type === 'assistant' && event.message) {
|
|
363
|
+
const content = event.message.content;
|
|
364
|
+
if (Array.isArray(content)) {
|
|
365
|
+
for (const block of content) {
|
|
366
|
+
if (block.type === 'text') {
|
|
367
|
+
// Always show conversational text immediately
|
|
368
|
+
process.stdout.write(block.text);
|
|
369
|
+
} else if (block.type === 'tool_use') {
|
|
370
|
+
// Track the tool call for later result matching
|
|
371
|
+
if (block.id) {
|
|
372
|
+
global.pendingToolCalls.set(block.id, block.name);
|
|
373
|
+
}
|
|
374
|
+
logToolCall(block.name, 'call', block);
|
|
375
|
+
if (VERBOSE_OUTPUT) {
|
|
376
|
+
console.log(color('cyan', `\nš§ Using tool: ${block.name}`));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else if (event.type === 'user' && event.message) {
|
|
382
|
+
const content = event.message.content;
|
|
383
|
+
if (Array.isArray(content)) {
|
|
384
|
+
for (const block of content) {
|
|
385
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
386
|
+
// Match the result with the original tool call
|
|
387
|
+
const toolUseId = block.tool_use_id;
|
|
388
|
+
const toolName = global.pendingToolCalls.get(toolUseId) || `unknown_tool_${toolUseId}`;
|
|
389
|
+
|
|
390
|
+
logToolCall(toolName, 'result', null, block);
|
|
391
|
+
|
|
392
|
+
// Remove from pending calls after matching
|
|
393
|
+
global.pendingToolCalls.delete(toolUseId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else if (event.type === 'result') {
|
|
398
|
+
const usage = event.usage || {};
|
|
399
|
+
const cost = event.total_cost_usd || 0;
|
|
400
|
+
if (VERBOSE_OUTPUT) {
|
|
401
|
+
console.log(color('green', `\n\nā
Completed in ${event.duration_ms}ms`));
|
|
402
|
+
console.log(color('yellow', `š° Cost: $${cost.toFixed(4)}`));
|
|
403
|
+
if (usage.input_tokens) {
|
|
404
|
+
console.log(color('cyan', `š Tokens: ${usage.input_tokens} in, ${usage.output_tokens} out`));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function handleStreamingOutput(data) {
|
|
411
|
+
jsonBuffer += data.toString();
|
|
412
|
+
|
|
413
|
+
// Split by newlines but keep the last incomplete line in buffer
|
|
414
|
+
const lines = jsonBuffer.split('\n');
|
|
415
|
+
jsonBuffer = lines.pop() || ''; // Keep last line (might be incomplete)
|
|
416
|
+
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (!line.trim()) continue; // Skip empty lines
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const event = JSON.parse(line);
|
|
422
|
+
handleEvent(event);
|
|
423
|
+
} catch (jsonError) {
|
|
424
|
+
// Log JSON parsing errors for troubleshooting
|
|
425
|
+
console.log(color('red', `š JSON parse error: ${jsonError.message}`));
|
|
426
|
+
console.log(color('yellow', `š Problematic line (${line.length} chars): ${line.substring(0, 200)}${line.length > 200 ? '...' : ''}`));
|
|
427
|
+
console.log(color('cyan', `š Buffer state: ${jsonBuffer.length} chars in buffer`));
|
|
428
|
+
|
|
429
|
+
// If we can't parse, put the line back in buffer and try to recover
|
|
430
|
+
jsonBuffer = line + '\n' + jsonBuffer;
|
|
431
|
+
console.log(color('yellow', `š Attempting to recover - ${jsonBuffer.length} chars in buffer`));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Add error handling
|
|
437
|
+
proc.on('error', (error) => {
|
|
438
|
+
console.log(color('red', `š Debug: Process error: ${error.message}`));
|
|
439
|
+
reject(error);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Write modified prompt to stdin
|
|
443
|
+
proc.stdin.write(modifiedPrompt);
|
|
444
|
+
proc.stdin.end();
|
|
445
|
+
|
|
446
|
+
let stdoutOutput = '';
|
|
447
|
+
let stderrOutput = '';
|
|
448
|
+
let lastError = '';
|
|
449
|
+
|
|
450
|
+
proc.stdout.on('data', (data) => {
|
|
451
|
+
stdoutOutput += data.toString();
|
|
452
|
+
|
|
453
|
+
handleStreamingOutput(data);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
proc.stderr.on('data', (data) => {
|
|
457
|
+
stderrOutput += data.toString();
|
|
458
|
+
process.stderr.write(data);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
proc.on('close', (code) => {
|
|
462
|
+
const sessionEndTime = Date.now();
|
|
463
|
+
const totalTime = sessionEndTime - startTime;
|
|
464
|
+
if (VERBOSE_OUTPUT) console.log(color('cyan', `\nā±ļø Stage 4: Session completed in ${totalTime}ms`));
|
|
465
|
+
|
|
466
|
+
// Try to parse any remaining data in buffer
|
|
467
|
+
if (jsonBuffer.trim()) {
|
|
468
|
+
try {
|
|
469
|
+
const event = JSON.parse(jsonBuffer);
|
|
470
|
+
handleEvent(event);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.log(color('yellow', `ā ļø Could not parse remaining buffer data: ${error.message}`));
|
|
473
|
+
console.log(color('yellow', `ā ļø Remaining buffer: ${jsonBuffer.substring(0, 100)}...`));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Display recent tool calls and performance summary only if verbose
|
|
478
|
+
if (VERBOSE_OUTPUT) {
|
|
479
|
+
displayRecentToolCalls();
|
|
480
|
+
console.log(color('cyan', `\nš Performance Summary:`));
|
|
481
|
+
console.log(color('cyan', ` ⢠Sandbox creation: ${sandboxCreateTime}ms`));
|
|
482
|
+
console.log(color('cyan', ` ⢠Environment setup: ${envCreateTime}ms`));
|
|
483
|
+
console.log(color('cyan', ` ⢠Claude Code session: ${totalTime - sandboxCreateTime - envCreateTime}ms`));
|
|
484
|
+
console.log(color('cyan', ` ⢠Total time: ${totalTime}ms`));
|
|
485
|
+
|
|
486
|
+
// Log file information if enabled
|
|
487
|
+
if (ENABLE_FILE_LOGGING && global.logFileHandle) {
|
|
488
|
+
console.log(color('yellow', `š Tool calls logged to: ${global.logFileHandle}`));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
cleanup();
|
|
493
|
+
resolve(true);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.log(color('red', `\nā Claude Code failed: ${error.message}`));
|
|
498
|
+
cleanup();
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { color } from '../colors.js';
|
|
4
|
+
import { createSandbox, createSandboxEnv, runInSandbox } from '../sandbox.js';
|
|
5
|
+
|
|
6
|
+
export function buildCommand(dockerfilePath) {
|
|
7
|
+
console.log(color('yellow', 'ā ļø Build command not yet implemented'));
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function runCommand(projectDir, cmd) {
|
|
12
|
+
if (!existsSync(projectDir)) {
|
|
13
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(color('blue', 'š Creating sandbox environment...'));
|
|
18
|
+
console.log(color('yellow', `Project: ${projectDir}`));
|
|
19
|
+
console.log(color('yellow', `Command: ${cmd}\n`));
|
|
20
|
+
|
|
21
|
+
const { sandboxDir, cleanup } = createSandbox(projectDir);
|
|
22
|
+
|
|
23
|
+
process.on('SIGINT', cleanup);
|
|
24
|
+
process.on('SIGTERM', cleanup);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const env = createSandboxEnv(sandboxDir, {
|
|
28
|
+
PLAYWRIGHT_BROWSERS_PATH: join(sandboxDir, 'browsers')
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(color('green', `ā
Sandbox created: ${sandboxDir}`));
|
|
32
|
+
console.log(color('cyan', 'š¦ Running in isolated environment...\n'));
|
|
33
|
+
|
|
34
|
+
await runInSandbox(cmd, [], sandboxDir, env);
|
|
35
|
+
|
|
36
|
+
console.log(color('green', '\nā
Command completed!'));
|
|
37
|
+
cleanup();
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log(color('red', `\nā Command failed: ${error.message}`));
|
|
41
|
+
cleanup();
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function shellCommand(projectDir) {
|
|
47
|
+
if (!existsSync(projectDir)) {
|
|
48
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(color('blue', 'š Starting interactive shell...'));
|
|
53
|
+
return runCommand(projectDir, 'bash');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function versionCommand() {
|
|
57
|
+
console.log(color('green', 'SandboxBox - Process Containment Sandbox'));
|
|
58
|
+
console.log(color('cyan', 'Using Node.js process isolation'));
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { color } from '../colors.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export { buildCommand, runCommand, shellCommand } from './container.js';
|
|
10
|
+
export { claudeCommand } from './claude.js';
|
|
11
|
+
|
|
12
|
+
export function versionCommand() {
|
|
13
|
+
try {
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
15
|
+
console.log(color('green', `SandboxBox v${packageJson.version}`));
|
|
16
|
+
console.log(color('cyan', 'Process containment sandbox for CLI tools'));
|
|
17
|
+
console.log(color('yellow', 'Supports: Playwright, Claude Code, and more'));
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.log(color('red', 'ā Could not read version'));
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|