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.
@@ -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
+ }