minivibe 0.2.9 → 0.2.10

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/README.md CHANGED
@@ -78,6 +78,14 @@ Get token from MiniVibe iOS app: Settings > Copy Token for CLI.
78
78
 
79
79
  ## Usage Modes
80
80
 
81
+ | Mode | Command | Description |
82
+ |------|---------|-------------|
83
+ | Direct | `vibe` | WebSocket to bridge server (default when authenticated) |
84
+ | Direct | `vibe --bridge <url>` | WebSocket to custom relay server |
85
+ | Agent | `vibe --agent` | Through local vibe-agent daemon |
86
+ | Attach | `vibe --attach <id>` | Full terminal via local agent |
87
+ | Remote | `vibe --remote <id>` | Chat-style control, no local Claude needed |
88
+
81
89
  ### Direct Mode (Default)
82
90
 
83
91
  Just run `vibe` after logging in:
@@ -109,6 +117,29 @@ vibe --agent --name "Backend Work"
109
117
  - Sessions survive network hiccups
110
118
  - Cleaner process management
111
119
 
120
+ ### Attach Mode
121
+
122
+ Attach to a running session managed by the local agent:
123
+
124
+ ```bash
125
+ vibe --attach <session-id>
126
+ ```
127
+
128
+ This provides full terminal passthrough to the session, useful for debugging or taking over a session started remotely.
129
+
130
+ ### Remote Mode
131
+
132
+ Control a session running elsewhere without needing Claude installed locally:
133
+
134
+ ```bash
135
+ vibe --remote <session-id>
136
+ ```
137
+
138
+ This is useful for:
139
+ - Monitoring sessions from a different machine
140
+ - Sending messages to Claude from a lightweight client
141
+ - Reviewing and approving permissions remotely
142
+
112
143
  ## Options
113
144
 
114
145
  ### vibe
package/agent/agent.js CHANGED
@@ -38,6 +38,8 @@ const RECONNECT_DELAY_MS = 5000;
38
38
  const HEARTBEAT_INTERVAL_MS = 30000;
39
39
  const LOCAL_SERVER_PORT = 9999;
40
40
  const PORT_FILE = path.join(os.homedir(), '.vibe-agent', 'port');
41
+ const PID_FILE = path.join(os.homedir(), '.vibe-agent', 'pid');
42
+ const START_TIME_FILE = path.join(os.homedir(), '.vibe-agent', 'start_time');
41
43
  const MAX_SESSION_HISTORY_AGE_DAYS = 30;
42
44
  const DEFAULT_BRIDGE_URL = 'wss://ws.minivibeapp.com';
43
45
  const PAIRING_URL = 'https://minivibeapp.com/pair';
@@ -358,6 +360,13 @@ function startLocalServer() {
358
360
  } catch (err) {
359
361
  // Ignore
360
362
  }
363
+ // Write PID and start time for status command
364
+ try {
365
+ fs.writeFileSync(PID_FILE, process.pid.toString(), 'utf8');
366
+ fs.writeFileSync(START_TIME_FILE, Date.now().toString(), 'utf8');
367
+ } catch (err) {
368
+ // Ignore
369
+ }
361
370
  });
362
371
 
363
372
  localServer.on('connection', (clientWs) => {
@@ -450,11 +459,17 @@ function stopLocalServer() {
450
459
  localServer.close();
451
460
  localServer = null;
452
461
  }
453
- // Remove port file
462
+ // Remove port, PID, and start time files
454
463
  try {
455
464
  if (fs.existsSync(PORT_FILE)) {
456
465
  fs.unlinkSync(PORT_FILE);
457
466
  }
467
+ if (fs.existsSync(PID_FILE)) {
468
+ fs.unlinkSync(PID_FILE);
469
+ }
470
+ if (fs.existsSync(START_TIME_FILE)) {
471
+ fs.unlinkSync(START_TIME_FILE);
472
+ }
458
473
  } catch (err) {
459
474
  // Ignore
460
475
  }
@@ -1462,10 +1477,113 @@ async function main() {
1462
1477
 
1463
1478
  // Status check
1464
1479
  if (options.status) {
1465
- console.log(`Bridge URL: ${bridgeUrl}`);
1466
- console.log(`Host Name: ${hostName}`);
1467
- console.log(`Auth Token: ${authToken ? 'Configured' : 'Not configured'}`);
1468
- console.log(`Agent ID: ${agentId || 'Will be assigned on first connect'}`);
1480
+ console.log(`${colors.cyan}${colors.bold}vibe-agent${colors.reset}`);
1481
+ console.log();
1482
+
1483
+ // Check if agent is running
1484
+ let isRunning = false;
1485
+ let pid = null;
1486
+ let uptime = null;
1487
+
1488
+ try {
1489
+ if (fs.existsSync(PID_FILE)) {
1490
+ pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
1491
+ // Check if process is actually running
1492
+ try {
1493
+ process.kill(pid, 0); // Signal 0 just checks if process exists
1494
+ isRunning = true;
1495
+
1496
+ // Calculate uptime
1497
+ if (fs.existsSync(START_TIME_FILE)) {
1498
+ const startTime = parseInt(fs.readFileSync(START_TIME_FILE, 'utf8').trim(), 10);
1499
+ const uptimeMs = Date.now() - startTime;
1500
+ // Only show uptime if it's positive and reasonable
1501
+ if (uptimeMs > 0 && !isNaN(startTime)) {
1502
+ const hours = Math.floor(uptimeMs / (1000 * 60 * 60));
1503
+ const minutes = Math.floor((uptimeMs % (1000 * 60 * 60)) / (1000 * 60));
1504
+ uptime = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
1505
+ }
1506
+ }
1507
+ } catch (err) {
1508
+ // Process not running, stale PID file
1509
+ isRunning = false;
1510
+ }
1511
+ }
1512
+ } catch (err) {
1513
+ // Ignore
1514
+ }
1515
+
1516
+ // Get active session count by connecting to local server
1517
+ let sessionCount = null;
1518
+ if (isRunning && fs.existsSync(PORT_FILE)) {
1519
+ try {
1520
+ const portStr = fs.readFileSync(PORT_FILE, 'utf8').trim();
1521
+ const port = parseInt(portStr, 10);
1522
+
1523
+ if (!isNaN(port) && port > 0) {
1524
+ // Quick check via local WebSocket
1525
+ const checkPromise = new Promise((resolve) => {
1526
+ const checkWs = new WebSocket(`ws://localhost:${port}`);
1527
+ const timeout = setTimeout(() => {
1528
+ try { checkWs.close(); } catch {}
1529
+ resolve(null);
1530
+ }, 1000);
1531
+
1532
+ checkWs.on('open', () => {
1533
+ checkWs.send(JSON.stringify({ type: 'list_sessions' }));
1534
+ });
1535
+
1536
+ checkWs.on('message', (data) => {
1537
+ try {
1538
+ const msg = JSON.parse(data.toString());
1539
+ if (msg.type === 'sessions_list') {
1540
+ clearTimeout(timeout);
1541
+ resolve(msg.sessions?.length || 0);
1542
+ try { checkWs.close(); } catch {}
1543
+ }
1544
+ } catch (err) {
1545
+ // Ignore
1546
+ }
1547
+ });
1548
+
1549
+ checkWs.on('error', () => {
1550
+ clearTimeout(timeout);
1551
+ resolve(null);
1552
+ });
1553
+
1554
+ checkWs.on('close', () => {
1555
+ clearTimeout(timeout);
1556
+ });
1557
+ });
1558
+
1559
+ sessionCount = await checkPromise;
1560
+ }
1561
+ } catch (err) {
1562
+ // Ignore
1563
+ }
1564
+ }
1565
+
1566
+ // Display status
1567
+ if (isRunning) {
1568
+ console.log(`Status: ${colors.green}running${colors.reset} (pid ${pid})`);
1569
+ if (uptime) {
1570
+ console.log(`Uptime: ${uptime}`);
1571
+ }
1572
+ } else {
1573
+ console.log(`Status: ${colors.dim}not running${colors.reset}`);
1574
+ }
1575
+
1576
+ console.log(`Host: ${hostName}`);
1577
+ console.log(`Bridge: ${bridgeUrl}`);
1578
+
1579
+ if (sessionCount !== null) {
1580
+ console.log(`Sessions: ${sessionCount} active`);
1581
+ }
1582
+
1583
+ console.log();
1584
+ console.log(`Auth: ${authToken ? colors.green + 'configured' + colors.reset : colors.yellow + 'not configured' + colors.reset}`);
1585
+ console.log(`Agent ID: ${agentId || colors.dim + 'will be assigned on first connect' + colors.reset}`);
1586
+
1469
1587
  process.exit(0);
1470
1588
  }
1471
1589
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minivibe",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "CLI wrapper for Claude Code with mobile remote control via MiniVibe iOS app",
5
5
  "author": "MiniVibe <hello@minivibeapp.com>",
6
6
  "homepage": "https://github.com/minivibeapp/minivibe",
@@ -30,7 +30,9 @@
30
30
  },
31
31
  "scripts": {
32
32
  "start": "node vibe.js",
33
- "agent": "node agent/agent.js"
33
+ "agent": "node agent/agent.js",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
34
36
  },
35
37
  "dependencies": {
36
38
  "uuid": "^9.0.0",
@@ -50,5 +52,8 @@
50
52
  "minivibe",
51
53
  "ios"
52
54
  ],
53
- "license": "MIT"
55
+ "license": "MIT",
56
+ "devDependencies": {
57
+ "vitest": "^4.0.17"
58
+ }
54
59
  }
package/vibe.js CHANGED
@@ -336,10 +336,32 @@ const subcommands = {
336
336
  'help': '--help'
337
337
  };
338
338
 
339
- // Transform first arg if it's a subcommand
340
- const args = rawArgs.length > 0 && subcommands[rawArgs[0]]
341
- ? [subcommands[rawArgs[0]], ...rawArgs.slice(1)]
342
- : rawArgs;
339
+ // Multi-level subcommand groups (vibe file upload, vibe session list, etc.)
340
+ const subcommandGroups = {
341
+ 'file': ['upload', 'list', 'download', 'delete'],
342
+ 'session': ['list', 'rename', 'info'],
343
+ 'note': ['create', 'list'],
344
+ };
345
+
346
+ // Check for multi-level subcommands first
347
+ let subcommandMode = null; // e.g., { group: 'file', action: 'upload' }
348
+ let subcommandArgs = []; // remaining args after subcommand
349
+
350
+ if (rawArgs.length >= 2 && subcommandGroups[rawArgs[0]]) {
351
+ const group = rawArgs[0];
352
+ const action = rawArgs[1];
353
+ if (subcommandGroups[group].includes(action)) {
354
+ subcommandMode = { group, action };
355
+ subcommandArgs = rawArgs.slice(2);
356
+ }
357
+ }
358
+
359
+ // Transform first arg if it's a simple subcommand (and not a multi-level one)
360
+ const args = subcommandMode ? rawArgs : (
361
+ rawArgs.length > 0 && subcommands[rawArgs[0]]
362
+ ? [subcommands[rawArgs[0]], ...rawArgs.slice(1)]
363
+ : rawArgs
364
+ );
343
365
 
344
366
  let initialPrompt = null;
345
367
  let resumeSessionId = null;
@@ -355,6 +377,56 @@ let remoteAttachMode = false; // --remote with --bridge: pure remote control (n
355
377
  let e2eEnabled = false; // --e2e mode: enable end-to-end encryption
356
378
  let verboseMode = false; // --verbose mode: show [vibe] debug messages
357
379
 
380
+ // Subcommand mode options
381
+ let subcommandOpts = {
382
+ folderId: null, // -f, --folder
383
+ fileType: null, // -t, --type (all, files, notes)
384
+ outputPath: null, // -o, --output
385
+ fileName: null, // -n, --name
386
+ content: null, // -c, --content
387
+ force: false, // --force
388
+ json: false, // --json
389
+ running: false, // --running
390
+ recent: false, // --recent
391
+ targetPath: null, // positional: file path or session ID
392
+ newName: null, // positional: new name for rename
393
+ };
394
+
395
+ // Parse subcommand options if in subcommand mode
396
+ if (subcommandMode) {
397
+ for (let i = 0; i < subcommandArgs.length; i++) {
398
+ const arg = subcommandArgs[i];
399
+ if (arg === '-f' || arg === '--folder') {
400
+ subcommandOpts.folderId = subcommandArgs[++i];
401
+ } else if (arg === '-t' || arg === '--type') {
402
+ subcommandOpts.fileType = subcommandArgs[++i];
403
+ } else if (arg === '-o' || arg === '--output') {
404
+ subcommandOpts.outputPath = subcommandArgs[++i];
405
+ } else if (arg === '-n' || arg === '--name') {
406
+ subcommandOpts.fileName = subcommandArgs[++i];
407
+ } else if (arg === '-c' || arg === '--content') {
408
+ subcommandOpts.content = subcommandArgs[++i];
409
+ } else if (arg === '--force') {
410
+ subcommandOpts.force = true;
411
+ } else if (arg === '--json') {
412
+ subcommandOpts.json = true;
413
+ } else if (arg === '--running') {
414
+ subcommandOpts.running = true;
415
+ } else if (arg === '--recent') {
416
+ subcommandOpts.recent = true;
417
+ } else if (arg === '--bridge' && subcommandArgs[i + 1]) {
418
+ bridgeUrl = subcommandArgs[++i];
419
+ } else if (!arg.startsWith('-')) {
420
+ // Positional arguments
421
+ if (!subcommandOpts.targetPath) {
422
+ subcommandOpts.targetPath = arg;
423
+ } else if (!subcommandOpts.newName) {
424
+ subcommandOpts.newName = arg;
425
+ }
426
+ }
427
+ }
428
+ }
429
+
358
430
  for (let i = 0; i < args.length; i++) {
359
431
  if (args[i] === '--help' || args[i] === '-h') {
360
432
  console.log(`
@@ -371,6 +443,27 @@ Commands:
371
443
  logout Sign out and remove stored auth
372
444
  help Show this help message
373
445
 
446
+ File Commands:
447
+ vibe file list [-f folder] [-t type] [--json]
448
+ List files in folder (type: all, files, notes)
449
+ vibe file upload <path> [-f folder] [-n name]
450
+ Upload file to cloud storage
451
+
452
+ Session Commands:
453
+ vibe session list [--running] [--recent] [--json]
454
+ List sessions (bridge + history)
455
+ vibe session rename <id> <name>
456
+ Rename a session
457
+
458
+ In-Session Commands (type during a session):
459
+ /name <name> Rename current session
460
+ /upload <path> Upload file to cloud
461
+ /download <id> Download file by ID
462
+ /files List uploaded files
463
+ /status Show connection status
464
+ /info Show session details
465
+ /help Show available commands
466
+
374
467
  Options:
375
468
  --headless Use device code flow for servers (no browser)
376
469
  --agent [url] Connect via local vibe-agent (default: auto-discover)
@@ -393,6 +486,9 @@ Examples:
393
486
  vibe login Sign in (one-time setup)
394
487
  vibe Start session
395
488
  vibe "Fix the bug" Start with prompt
489
+ vibe file list List uploaded files
490
+ vibe file upload ./log.txt Upload a file
491
+ vibe session list List all sessions
396
492
  vibe --e2e Enable encryption
397
493
  vibe --agent Use local agent
398
494
 
@@ -584,6 +680,10 @@ let lastCapturedPrompt = null; // Last permission prompt captured from CLI
584
680
  const mobileMessageHashes = new Set(); // Track messages from mobile to avoid duplicate echo
585
681
  const MAX_MOBILE_MESSAGES = 100; // Limit Set size
586
682
 
683
+ // In-session slash command support
684
+ let inputBuffer = ''; // Buffer for detecting slash commands
685
+ const SLASH_COMMANDS = ['/name', '/upload', '/download', '/files', '/status', '/info', '/help'];
686
+
587
687
  // Colors for terminal output
588
688
  const colors = {
589
689
  reset: '\x1b[0m',
@@ -2160,6 +2260,369 @@ function startClaude() {
2160
2260
  });
2161
2261
  }
2162
2262
 
2263
+ // ====================
2264
+ // In-Session Slash Commands
2265
+ // ====================
2266
+
2267
+ function handleSlashCommand(input) {
2268
+ const parts = input.trim().split(/\s+/);
2269
+ const cmd = parts[0];
2270
+ const args = parts.slice(1);
2271
+
2272
+ switch (cmd) {
2273
+ case '/name':
2274
+ slashCmdName(args.join(' '));
2275
+ break;
2276
+ case '/upload':
2277
+ slashCmdUpload(args[0]);
2278
+ break;
2279
+ case '/download':
2280
+ slashCmdDownload(args[0], args.slice(1));
2281
+ break;
2282
+ case '/files':
2283
+ slashCmdFiles();
2284
+ break;
2285
+ case '/status':
2286
+ slashCmdStatus();
2287
+ break;
2288
+ case '/info':
2289
+ slashCmdInfo();
2290
+ break;
2291
+ case '/help':
2292
+ slashCmdHelp();
2293
+ break;
2294
+ default:
2295
+ console.log(`\n${colors.yellow}Unknown command: ${cmd}${colors.reset}`);
2296
+ slashCmdHelp();
2297
+ }
2298
+ }
2299
+
2300
+ // /name <new-name> - Rename current session
2301
+ function slashCmdName(newName) {
2302
+ if (!newName) {
2303
+ console.log(`\n${colors.yellow}Usage: /name <new-name>${colors.reset}`);
2304
+ return;
2305
+ }
2306
+
2307
+ if (!bridgeSocket || bridgeSocket.readyState !== WebSocket.OPEN) {
2308
+ console.log(`\n${colors.yellow}Not connected to bridge${colors.reset}`);
2309
+ return;
2310
+ }
2311
+
2312
+ console.log(`\n${colors.dim}Renaming session...${colors.reset}`);
2313
+
2314
+ bridgeSocket.send(JSON.stringify({
2315
+ type: 'rename_session',
2316
+ sessionId: sessionId,
2317
+ name: newName
2318
+ }));
2319
+
2320
+ // The response will come via the bridge message handler
2321
+ // For now, optimistically update local state
2322
+ sessionName = newName;
2323
+ console.log(`${colors.green}Session renamed to "${newName}"${colors.reset}\n`);
2324
+ }
2325
+
2326
+ // /upload <path> - Upload file to cloud
2327
+ async function slashCmdUpload(filePath) {
2328
+ if (!filePath) {
2329
+ console.log(`\n${colors.yellow}Usage: /upload <path>${colors.reset}`);
2330
+ return;
2331
+ }
2332
+
2333
+ // Resolve path relative to cwd
2334
+ const fullPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
2335
+
2336
+ if (!fs.existsSync(fullPath)) {
2337
+ console.log(`\n${colors.red}File not found: ${filePath}${colors.reset}`);
2338
+ return;
2339
+ }
2340
+
2341
+ const stats = fs.statSync(fullPath);
2342
+ if (stats.isDirectory()) {
2343
+ console.log(`\n${colors.red}Cannot upload directories${colors.reset}`);
2344
+ return;
2345
+ }
2346
+
2347
+ const fileName = path.basename(fullPath);
2348
+ const fileSize = stats.size;
2349
+ const mimeType = getMimeType(fileName);
2350
+
2351
+ console.log(`\n${colors.dim}Uploading ${fileName} (${formatSize(fileSize)})...${colors.reset}`);
2352
+
2353
+ if (!bridgeSocket || bridgeSocket.readyState !== WebSocket.OPEN) {
2354
+ console.log(`${colors.yellow}Not connected to bridge${colors.reset}`);
2355
+ return;
2356
+ }
2357
+
2358
+ // Request upload URL from bridge
2359
+ const uploadPromise = new Promise((resolve, reject) => {
2360
+ const timeout = setTimeout(() => reject(new Error('Upload timeout')), 30000);
2361
+
2362
+ const handler = (data) => {
2363
+ try {
2364
+ const msg = JSON.parse(data.toString());
2365
+ if (msg.type === 'upload_url') {
2366
+ clearTimeout(timeout);
2367
+ bridgeSocket.off('message', handler);
2368
+ resolve(msg);
2369
+ } else if (msg.type === 'error' && msg.context === 'upload') {
2370
+ clearTimeout(timeout);
2371
+ bridgeSocket.off('message', handler);
2372
+ reject(new Error(msg.message || 'Upload failed'));
2373
+ }
2374
+ } catch (err) {
2375
+ // Ignore parse errors
2376
+ }
2377
+ };
2378
+
2379
+ bridgeSocket.on('message', handler);
2380
+
2381
+ bridgeSocket.send(JSON.stringify({
2382
+ type: 'get_upload_url',
2383
+ name: fileName,
2384
+ mimeType: mimeType,
2385
+ size: fileSize
2386
+ }));
2387
+ });
2388
+
2389
+ try {
2390
+ const { url: uploadUrl, fileId } = await uploadPromise;
2391
+
2392
+ // Upload to S3
2393
+ const fileData = fs.readFileSync(fullPath);
2394
+ const https = require('https');
2395
+ const { URL } = require('url');
2396
+ const parsedUrl = new URL(uploadUrl);
2397
+
2398
+ await new Promise((resolve, reject) => {
2399
+ const req = https.request({
2400
+ hostname: parsedUrl.hostname,
2401
+ path: parsedUrl.pathname + parsedUrl.search,
2402
+ method: 'PUT',
2403
+ headers: {
2404
+ 'Content-Type': mimeType,
2405
+ 'Content-Length': fileSize
2406
+ }
2407
+ }, (res) => {
2408
+ if (res.statusCode >= 200 && res.statusCode < 300) {
2409
+ resolve();
2410
+ } else {
2411
+ reject(new Error(`Upload failed: HTTP ${res.statusCode}`));
2412
+ }
2413
+ });
2414
+
2415
+ req.on('error', reject);
2416
+ req.write(fileData);
2417
+ req.end();
2418
+ });
2419
+
2420
+ // Confirm upload
2421
+ bridgeSocket.send(JSON.stringify({
2422
+ type: 'confirm_upload',
2423
+ fileId: fileId
2424
+ }));
2425
+
2426
+ console.log(`${colors.green}Uploaded ${fileName}${colors.reset}`);
2427
+ console.log(`${colors.dim}File ID: ${fileId}${colors.reset}\n`);
2428
+ } catch (err) {
2429
+ console.log(`${colors.red}Upload failed: ${err.message}${colors.reset}\n`);
2430
+ }
2431
+ }
2432
+
2433
+ // /download <file-id> [-o path] - Download file
2434
+ async function slashCmdDownload(fileId, args) {
2435
+ if (!fileId) {
2436
+ console.log(`\n${colors.yellow}Usage: /download <file-id> [-o path]${colors.reset}`);
2437
+ return;
2438
+ }
2439
+
2440
+ let outputPath = null;
2441
+ for (let i = 0; i < args.length; i++) {
2442
+ if (args[i] === '-o' && args[i + 1]) {
2443
+ outputPath = args[i + 1];
2444
+ break;
2445
+ }
2446
+ }
2447
+
2448
+ if (!bridgeSocket || bridgeSocket.readyState !== WebSocket.OPEN) {
2449
+ console.log(`\n${colors.yellow}Not connected to bridge${colors.reset}`);
2450
+ return;
2451
+ }
2452
+
2453
+ console.log(`\n${colors.dim}Downloading...${colors.reset}`);
2454
+
2455
+ // Request download URL from bridge
2456
+ const downloadPromise = new Promise((resolve, reject) => {
2457
+ const timeout = setTimeout(() => reject(new Error('Download timeout')), 30000);
2458
+
2459
+ const handler = (data) => {
2460
+ try {
2461
+ const msg = JSON.parse(data.toString());
2462
+ if (msg.type === 'download_url') {
2463
+ clearTimeout(timeout);
2464
+ bridgeSocket.off('message', handler);
2465
+ resolve(msg);
2466
+ } else if (msg.type === 'error' && msg.context === 'download') {
2467
+ clearTimeout(timeout);
2468
+ bridgeSocket.off('message', handler);
2469
+ reject(new Error(msg.message || 'Download failed'));
2470
+ }
2471
+ } catch (err) {
2472
+ // Ignore parse errors
2473
+ }
2474
+ };
2475
+
2476
+ bridgeSocket.on('message', handler);
2477
+
2478
+ bridgeSocket.send(JSON.stringify({
2479
+ type: 'get_download_url',
2480
+ fileId: fileId
2481
+ }));
2482
+ });
2483
+
2484
+ try {
2485
+ const response = await downloadPromise;
2486
+ const downloadUrl = response.url;
2487
+ const fileName = response.name || `download_${fileId}`;
2488
+
2489
+ if (!downloadUrl) {
2490
+ console.log(`${colors.red}No download URL received${colors.reset}\n`);
2491
+ return;
2492
+ }
2493
+
2494
+ // Download from URL
2495
+ const https = require('https');
2496
+ const { URL } = require('url');
2497
+ const parsedUrl = new URL(downloadUrl);
2498
+
2499
+ const fileData = await new Promise((resolve, reject) => {
2500
+ https.get({
2501
+ hostname: parsedUrl.hostname,
2502
+ path: parsedUrl.pathname + parsedUrl.search
2503
+ }, (res) => {
2504
+ if (res.statusCode >= 200 && res.statusCode < 300) {
2505
+ const chunks = [];
2506
+ res.on('data', chunk => chunks.push(chunk));
2507
+ res.on('end', () => resolve(Buffer.concat(chunks)));
2508
+ } else {
2509
+ reject(new Error(`Download failed: HTTP ${res.statusCode}`));
2510
+ }
2511
+ }).on('error', reject);
2512
+ });
2513
+
2514
+ // Write to file
2515
+ const outPath = outputPath || path.join(process.cwd(), fileName);
2516
+ fs.writeFileSync(outPath, fileData);
2517
+
2518
+ console.log(`${colors.green}Downloaded ${fileName}${colors.reset}`);
2519
+ console.log(`${colors.dim}Saved to: ${outPath}${colors.reset}\n`);
2520
+ } catch (err) {
2521
+ console.log(`${colors.red}Download failed: ${err.message}${colors.reset}\n`);
2522
+ }
2523
+ }
2524
+
2525
+ // /files - List uploaded files
2526
+ async function slashCmdFiles() {
2527
+ if (!bridgeSocket || bridgeSocket.readyState !== WebSocket.OPEN) {
2528
+ console.log(`\n${colors.yellow}Not connected to bridge${colors.reset}`);
2529
+ return;
2530
+ }
2531
+
2532
+ console.log(`\n${colors.dim}Fetching files...${colors.reset}`);
2533
+
2534
+ const listPromise = new Promise((resolve, reject) => {
2535
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 10000);
2536
+
2537
+ const handler = (data) => {
2538
+ try {
2539
+ const msg = JSON.parse(data.toString());
2540
+ if (msg.type === 'folder_contents') {
2541
+ clearTimeout(timeout);
2542
+ bridgeSocket.off('message', handler);
2543
+ resolve(msg.contents || msg.files || []);
2544
+ } else if (msg.type === 'error') {
2545
+ clearTimeout(timeout);
2546
+ bridgeSocket.off('message', handler);
2547
+ reject(new Error(msg.message || 'Failed to list files'));
2548
+ }
2549
+ } catch (err) {
2550
+ // Ignore parse errors
2551
+ }
2552
+ };
2553
+
2554
+ bridgeSocket.on('message', handler);
2555
+
2556
+ bridgeSocket.send(JSON.stringify({
2557
+ type: 'list_folder_contents'
2558
+ }));
2559
+ });
2560
+
2561
+ try {
2562
+ const files = await listPromise;
2563
+
2564
+ if (files.length === 0) {
2565
+ console.log(`${colors.dim}No files uploaded${colors.reset}\n`);
2566
+ return;
2567
+ }
2568
+
2569
+ console.log();
2570
+ console.log(`${'ID'.padEnd(14)} ${'NAME'.padEnd(30)} ${'SIZE'.padEnd(10)} UPLOADED`);
2571
+ console.log(`${'-'.repeat(14)} ${'-'.repeat(30)} ${'-'.repeat(10)} ${'-'.repeat(15)}`);
2572
+
2573
+ for (const file of files) {
2574
+ const id = (file.id || '').slice(0, 12);
2575
+ const name = (file.name || '').slice(0, 28);
2576
+ const size = formatSize(file.size || 0);
2577
+ const date = file.createdAt ? new Date(file.createdAt).toLocaleDateString() : '';
2578
+ console.log(`${id.padEnd(14)} ${name.padEnd(30)} ${size.padEnd(10)} ${date}`);
2579
+ }
2580
+ console.log();
2581
+ } catch (err) {
2582
+ console.log(`${colors.red}Failed to list files: ${err.message}${colors.reset}\n`);
2583
+ }
2584
+ }
2585
+
2586
+ // /status - Show connection status
2587
+ function slashCmdStatus() {
2588
+ console.log();
2589
+ console.log(`${colors.cyan}${colors.bright}Session Status${colors.reset}`);
2590
+ console.log();
2591
+ const sessionDisplay = sessionId ? `${sessionId.slice(0, 8)}...` : colors.dim + '(no session)' + colors.reset;
2592
+ console.log(`Session: ${sessionDisplay}${sessionName ? ` (${sessionName})` : ''}`);
2593
+ console.log(`Bridge: ${bridgeSocket && bridgeSocket.readyState === WebSocket.OPEN ? colors.green + 'connected' + colors.reset : colors.yellow + 'disconnected' + colors.reset}`);
2594
+ console.log(`E2E: ${e2eEnabled ? colors.green + 'enabled' + colors.reset : colors.dim + 'disabled' + colors.reset}`);
2595
+ console.log();
2596
+ }
2597
+
2598
+ // /info - Show session details
2599
+ function slashCmdInfo() {
2600
+ console.log();
2601
+ console.log(`${colors.cyan}${colors.bright}Session Info${colors.reset}`);
2602
+ console.log();
2603
+ console.log(`Session ID: ${sessionId || colors.dim + '(no session)' + colors.reset}`);
2604
+ console.log(`Name: ${sessionName || colors.dim + '(unnamed)' + colors.reset}`);
2605
+ console.log(`Path: ${process.cwd()}`);
2606
+ console.log(`Bridge: ${bridgeUrl || DEFAULT_BRIDGE_URL}`);
2607
+ console.log(`E2E: ${e2eEnabled ? 'enabled' : 'disabled'}`);
2608
+ console.log();
2609
+ }
2610
+
2611
+ // /help - Show available slash commands
2612
+ function slashCmdHelp() {
2613
+ console.log();
2614
+ console.log(`${colors.cyan}${colors.bright}Available Commands${colors.reset}`);
2615
+ console.log();
2616
+ console.log(` /name <name> Rename this session`);
2617
+ console.log(` /upload <path> Upload a file to cloud storage`);
2618
+ console.log(` /download <id> Download a file by ID`);
2619
+ console.log(` /files List uploaded files`);
2620
+ console.log(` /status Show connection status`);
2621
+ console.log(` /info Show session details`);
2622
+ console.log(` /help Show this help`);
2623
+ console.log();
2624
+ }
2625
+
2163
2626
  // ====================
2164
2627
  // Terminal Input
2165
2628
  // ====================
@@ -2170,9 +2633,74 @@ function setupTerminalInput() {
2170
2633
  }
2171
2634
  process.stdin.resume();
2172
2635
  process.stdin.on('data', (data) => {
2173
- // Pass all input directly to Claude - no command interception
2174
- if (claudeProcess && isRunning && claudeProcess.stdin && claudeProcess.stdin.writable) {
2175
- claudeProcess.stdin.write(data);
2636
+ if (!claudeProcess || !isRunning || !claudeProcess.stdin || !claudeProcess.stdin.writable) {
2637
+ return;
2638
+ }
2639
+
2640
+ const str = data.toString();
2641
+
2642
+ // Handle each character for slash command detection
2643
+ for (let i = 0; i < str.length; i++) {
2644
+ const char = str[i];
2645
+ const code = str.charCodeAt(i);
2646
+
2647
+ // Enter key (CR or LF)
2648
+ if (code === 13 || code === 10) {
2649
+ const trimmed = inputBuffer.trim();
2650
+
2651
+ // Check if it's one of our slash commands
2652
+ const matchedCmd = SLASH_COMMANDS.find(cmd =>
2653
+ trimmed === cmd || trimmed.startsWith(cmd + ' ')
2654
+ );
2655
+
2656
+ if (matchedCmd) {
2657
+ // Clear Claude's input line (Ctrl+U) and move to new line
2658
+ claudeProcess.stdin.write('\x15\n');
2659
+
2660
+ // Handle the slash command
2661
+ handleSlashCommand(trimmed);
2662
+
2663
+ // Clear buffer
2664
+ inputBuffer = '';
2665
+ } else {
2666
+ // Not our command - pass through to Claude and clear buffer
2667
+ claudeProcess.stdin.write(char);
2668
+ inputBuffer = '';
2669
+ }
2670
+ }
2671
+ // Backspace (0x7f) or Delete (0x08)
2672
+ else if (code === 127 || code === 8) {
2673
+ // Remove last char from buffer
2674
+ if (inputBuffer.length > 0) {
2675
+ inputBuffer = inputBuffer.slice(0, -1);
2676
+ }
2677
+ // Pass through to Claude
2678
+ claudeProcess.stdin.write(char);
2679
+ }
2680
+ // Ctrl+C (0x03) - clear buffer
2681
+ else if (code === 3) {
2682
+ inputBuffer = '';
2683
+ claudeProcess.stdin.write(char);
2684
+ }
2685
+ // Ctrl+U (0x15) - clear line, clear buffer
2686
+ else if (code === 21) {
2687
+ inputBuffer = '';
2688
+ claudeProcess.stdin.write(char);
2689
+ }
2690
+ // Escape character (0x1b) - start of escape sequence, clear buffer
2691
+ else if (code === 27) {
2692
+ inputBuffer = '';
2693
+ claudeProcess.stdin.write(char);
2694
+ }
2695
+ // Regular printable character (space to tilde in ASCII)
2696
+ else if (code >= 32 && code <= 126) {
2697
+ inputBuffer += char;
2698
+ claudeProcess.stdin.write(char);
2699
+ }
2700
+ // Other control characters - pass through but don't buffer
2701
+ else {
2702
+ claudeProcess.stdin.write(char);
2703
+ }
2176
2704
  }
2177
2705
  });
2178
2706
  }
@@ -2725,9 +3253,409 @@ function listSessionsMode() {
2725
3253
  });
2726
3254
  }
2727
3255
 
3256
+ // ====================
3257
+ // Subcommand Modes
3258
+ // ====================
3259
+
3260
+ // Helper: Create bridge connection for subcommand modes
3261
+ function createSubcommandBridgeConnection(onMessage) {
3262
+ return new Promise((resolve, reject) => {
3263
+ const WebSocket = require('ws');
3264
+ const url = bridgeUrl || DEFAULT_BRIDGE_URL;
3265
+ const socket = new WebSocket(url);
3266
+
3267
+ const timeout = setTimeout(() => {
3268
+ socket.close();
3269
+ reject(new Error('Connection timeout'));
3270
+ }, 15000);
3271
+
3272
+ socket.on('open', () => {
3273
+ // Authenticate
3274
+ socket.send(JSON.stringify({ type: 'authenticate', token: authToken }));
3275
+ });
3276
+
3277
+ socket.on('message', (data) => {
3278
+ try {
3279
+ const msg = JSON.parse(data.toString());
3280
+ if (msg.type === 'authenticated') {
3281
+ clearTimeout(timeout);
3282
+ resolve({ socket, send: (m) => socket.send(JSON.stringify(m)) });
3283
+ } else if (msg.type === 'auth_error') {
3284
+ clearTimeout(timeout);
3285
+ socket.close();
3286
+ reject(new Error(msg.message || 'Authentication failed'));
3287
+ } else {
3288
+ onMessage(msg);
3289
+ }
3290
+ } catch (e) {}
3291
+ });
3292
+
3293
+ socket.on('error', (err) => {
3294
+ clearTimeout(timeout);
3295
+ reject(err);
3296
+ });
3297
+
3298
+ socket.on('close', () => {
3299
+ clearTimeout(timeout);
3300
+ });
3301
+ });
3302
+ }
3303
+
3304
+ // File List Mode
3305
+ async function fileListMode() {
3306
+ if (!authToken) {
3307
+ log('❌ Not authenticated. Run: vibe login', colors.red);
3308
+ process.exit(1);
3309
+ }
3310
+
3311
+ let gotResponse = false;
3312
+
3313
+ try {
3314
+ const { socket, send } = await createSubcommandBridgeConnection((msg) => {
3315
+ if (msg.type === 'folder_contents') {
3316
+ gotResponse = true;
3317
+
3318
+ if (subcommandOpts.json) {
3319
+ console.log(JSON.stringify({ folders: msg.folders, files: msg.files }, null, 2));
3320
+ } else {
3321
+ console.log('');
3322
+ log('📁 Files', colors.bright + colors.cyan);
3323
+ log('══════════════════════════════════════', colors.dim);
3324
+
3325
+ if (msg.folders && msg.folders.length > 0) {
3326
+ for (const folder of msg.folders) {
3327
+ log(`📁 ${folder.name}`, colors.yellow);
3328
+ log(` ID: ${folder.id}`, colors.dim);
3329
+ }
3330
+ }
3331
+
3332
+ if (msg.files && msg.files.length > 0) {
3333
+ for (const file of msg.files) {
3334
+ const icon = file.mimeType?.startsWith('image/') ? '🖼️' :
3335
+ file.mimeType?.startsWith('video/') ? '🎬' :
3336
+ file.mimeType?.startsWith('audio/') ? '🎵' :
3337
+ file.isNote ? '📝' : '📄';
3338
+ const size = formatSize(file.size);
3339
+ log(`${icon} ${file.name} (${size})`, colors.green);
3340
+ log(` ID: ${file.id}`, colors.dim);
3341
+ }
3342
+ }
3343
+
3344
+ if ((!msg.folders || msg.folders.length === 0) && (!msg.files || msg.files.length === 0)) {
3345
+ log(' No files found', colors.dim);
3346
+ }
3347
+
3348
+ console.log('');
3349
+ }
3350
+
3351
+ socket.close();
3352
+ process.exit(0);
3353
+ } else if (msg.type === 'error') {
3354
+ gotResponse = true;
3355
+ log(`❌ Error: ${msg.message}`, colors.red);
3356
+ socket.close();
3357
+ process.exit(1);
3358
+ }
3359
+ });
3360
+
3361
+ // Send list request
3362
+ send({
3363
+ type: 'list_folder_contents',
3364
+ folderId: subcommandOpts.folderId || null,
3365
+ filter: subcommandOpts.fileType || 'all'
3366
+ });
3367
+
3368
+ } catch (err) {
3369
+ log(`❌ ${err.message}`, colors.red);
3370
+ process.exit(1);
3371
+ }
3372
+ }
3373
+
3374
+ // Format file size
3375
+ function formatSize(bytes) {
3376
+ if (!bytes) return '0 B';
3377
+ const units = ['B', 'KB', 'MB', 'GB'];
3378
+ let i = 0;
3379
+ while (bytes >= 1024 && i < units.length - 1) {
3380
+ bytes /= 1024;
3381
+ i++;
3382
+ }
3383
+ return `${bytes.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
3384
+ }
3385
+
3386
+ // File Upload Mode
3387
+ async function fileUploadMode() {
3388
+ if (!authToken) {
3389
+ log('❌ Not authenticated. Run: vibe login', colors.red);
3390
+ process.exit(1);
3391
+ }
3392
+
3393
+ const filePath = subcommandOpts.targetPath;
3394
+ if (!filePath) {
3395
+ log('❌ No file path provided', colors.red);
3396
+ log(' Usage: vibe file upload <path> [-f folder] [-n name]', colors.dim);
3397
+ process.exit(1);
3398
+ }
3399
+
3400
+ // Resolve and check file
3401
+ const resolvedPath = path.resolve(filePath);
3402
+ if (!fs.existsSync(resolvedPath)) {
3403
+ log(`❌ File not found: ${resolvedPath}`, colors.red);
3404
+ process.exit(1);
3405
+ }
3406
+
3407
+ const stats = fs.statSync(resolvedPath);
3408
+ if (stats.isDirectory()) {
3409
+ log('❌ Cannot upload directory', colors.red);
3410
+ process.exit(1);
3411
+ }
3412
+
3413
+ const fileName = subcommandOpts.fileName || path.basename(resolvedPath);
3414
+ const fileSize = stats.size;
3415
+ const mimeType = getMimeType(fileName);
3416
+
3417
+ log(`📤 Uploading: ${fileName} (${formatSize(fileSize)})`, colors.cyan);
3418
+
3419
+ try {
3420
+ const { socket, send } = await createSubcommandBridgeConnection((msg) => {
3421
+ if (msg.type === 'upload_url') {
3422
+ // Got presigned URL, now upload to S3
3423
+ uploadToS3(msg.uploadUrl, resolvedPath, mimeType).then(() => {
3424
+ // Confirm upload
3425
+ send({
3426
+ type: 'confirm_upload',
3427
+ fileId: msg.fileId,
3428
+ s3Key: msg.s3Key,
3429
+ folderId: subcommandOpts.folderId || null,
3430
+ name: fileName,
3431
+ originalName: fileName,
3432
+ mimeType: mimeType,
3433
+ size: fileSize
3434
+ });
3435
+ }).catch((err) => {
3436
+ log(`❌ Upload failed: ${err.message}`, colors.red);
3437
+ socket.close();
3438
+ process.exit(1);
3439
+ });
3440
+ } else if (msg.type === 'file_uploaded') {
3441
+ if (subcommandOpts.json) {
3442
+ console.log(JSON.stringify({ file: msg.file }, null, 2));
3443
+ } else {
3444
+ log(`✅ Uploaded: ${msg.file.name}`, colors.green);
3445
+ log(` ID: ${msg.file.id}`, colors.dim);
3446
+ }
3447
+ socket.close();
3448
+ process.exit(0);
3449
+ } else if (msg.type === 'error') {
3450
+ log(`❌ Error: ${msg.message}`, colors.red);
3451
+ socket.close();
3452
+ process.exit(1);
3453
+ }
3454
+ });
3455
+
3456
+ // Request upload URL
3457
+ send({
3458
+ type: 'get_upload_url',
3459
+ name: fileName,
3460
+ mimeType: mimeType,
3461
+ size: fileSize,
3462
+ folderId: subcommandOpts.folderId || null
3463
+ });
3464
+
3465
+ } catch (err) {
3466
+ log(`❌ ${err.message}`, colors.red);
3467
+ process.exit(1);
3468
+ }
3469
+ }
3470
+
3471
+ // Upload file to S3 using presigned URL
3472
+ async function uploadToS3(url, filePath, mimeType) {
3473
+ const fileData = fs.readFileSync(filePath);
3474
+ const https = require('https');
3475
+ const http = require('http');
3476
+
3477
+ return new Promise((resolve, reject) => {
3478
+ const parsedUrl = new URL(url);
3479
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
3480
+
3481
+ const req = protocol.request(url, {
3482
+ method: 'PUT',
3483
+ headers: {
3484
+ 'Content-Type': mimeType,
3485
+ 'Content-Length': fileData.length
3486
+ }
3487
+ }, (res) => {
3488
+ if (res.statusCode >= 200 && res.statusCode < 300) {
3489
+ resolve();
3490
+ } else {
3491
+ reject(new Error(`S3 upload failed: ${res.statusCode}`));
3492
+ }
3493
+ });
3494
+
3495
+ req.on('error', reject);
3496
+ req.write(fileData);
3497
+ req.end();
3498
+ });
3499
+ }
3500
+
3501
+ // Get MIME type from filename
3502
+ function getMimeType(filename) {
3503
+ const ext = path.extname(filename).toLowerCase();
3504
+ const mimeTypes = {
3505
+ '.txt': 'text/plain',
3506
+ '.html': 'text/html',
3507
+ '.css': 'text/css',
3508
+ '.js': 'application/javascript',
3509
+ '.json': 'application/json',
3510
+ '.xml': 'application/xml',
3511
+ '.pdf': 'application/pdf',
3512
+ '.zip': 'application/zip',
3513
+ '.tar': 'application/x-tar',
3514
+ '.gz': 'application/gzip',
3515
+ '.png': 'image/png',
3516
+ '.jpg': 'image/jpeg',
3517
+ '.jpeg': 'image/jpeg',
3518
+ '.gif': 'image/gif',
3519
+ '.svg': 'image/svg+xml',
3520
+ '.webp': 'image/webp',
3521
+ '.mp3': 'audio/mpeg',
3522
+ '.wav': 'audio/wav',
3523
+ '.mp4': 'video/mp4',
3524
+ '.webm': 'video/webm',
3525
+ '.mov': 'video/quicktime',
3526
+ '.md': 'text/markdown',
3527
+ '.log': 'text/plain',
3528
+ };
3529
+ return mimeTypes[ext] || 'application/octet-stream';
3530
+ }
3531
+
3532
+ // Session List Mode (via bridge, includes history)
3533
+ async function sessionListMode() {
3534
+ if (!authToken) {
3535
+ log('❌ Not authenticated. Run: vibe login', colors.red);
3536
+ process.exit(1);
3537
+ }
3538
+
3539
+ try {
3540
+ const { socket, send } = await createSubcommandBridgeConnection((msg) => {
3541
+ if (msg.type === 'sessions_list') {
3542
+ let sessions = msg.sessions || [];
3543
+
3544
+ // Filter if requested
3545
+ if (subcommandOpts.running) {
3546
+ sessions = sessions.filter(s => s.status === 'active' || s.isLive);
3547
+ } else if (subcommandOpts.recent) {
3548
+ sessions = sessions.filter(s => s.status !== 'active' && !s.isLive);
3549
+ }
3550
+
3551
+ if (subcommandOpts.json) {
3552
+ console.log(JSON.stringify({ sessions }, null, 2));
3553
+ } else {
3554
+ console.log('');
3555
+ log('📋 Sessions', colors.bright + colors.cyan);
3556
+ log('══════════════════════════════════════', colors.dim);
3557
+
3558
+ if (sessions.length === 0) {
3559
+ log(' No sessions found', colors.dim);
3560
+ } else {
3561
+ for (const session of sessions) {
3562
+ const status = session.isLive || session.status === 'active' ? '🟢' : '⚪';
3563
+ const name = session.name || 'Unnamed';
3564
+ console.log('');
3565
+ log(`${status} ${name}`, session.isLive ? colors.green : colors.dim);
3566
+ log(` ID: ${session.sessionId || session.id}`, colors.dim);
3567
+ if (session.path) log(` Path: ${session.path}`, colors.dim);
3568
+ if (session.messageCount) log(` Msgs: ${session.messageCount}`, colors.dim);
3569
+ }
3570
+ }
3571
+
3572
+ console.log('');
3573
+ log('══════════════════════════════════════', colors.dim);
3574
+ console.log('');
3575
+ }
3576
+
3577
+ socket.close();
3578
+ process.exit(0);
3579
+ } else if (msg.type === 'error') {
3580
+ log(`❌ Error: ${msg.message}`, colors.red);
3581
+ socket.close();
3582
+ process.exit(1);
3583
+ }
3584
+ });
3585
+
3586
+ // Request sessions list
3587
+ send({ type: 'list_sessions' });
3588
+
3589
+ } catch (err) {
3590
+ log(`❌ ${err.message}`, colors.red);
3591
+ process.exit(1);
3592
+ }
3593
+ }
3594
+
3595
+ // Session Rename Mode
3596
+ async function sessionRenameMode() {
3597
+ if (!authToken) {
3598
+ log('❌ Not authenticated. Run: vibe login', colors.red);
3599
+ process.exit(1);
3600
+ }
3601
+
3602
+ const targetSessionId = subcommandOpts.targetPath;
3603
+ const newName = subcommandOpts.newName;
3604
+
3605
+ if (!targetSessionId || !newName) {
3606
+ log('❌ Missing arguments', colors.red);
3607
+ log(' Usage: vibe session rename <session-id> <new-name>', colors.dim);
3608
+ process.exit(1);
3609
+ }
3610
+
3611
+ try {
3612
+ const { socket, send } = await createSubcommandBridgeConnection((msg) => {
3613
+ if (msg.type === 'session_renamed') {
3614
+ if (subcommandOpts.json) {
3615
+ console.log(JSON.stringify({ sessionId: msg.sessionId, name: msg.name }, null, 2));
3616
+ } else {
3617
+ log(`✅ Session renamed to: ${msg.name}`, colors.green);
3618
+ }
3619
+ socket.close();
3620
+ process.exit(0);
3621
+ } else if (msg.type === 'error') {
3622
+ log(`❌ Error: ${msg.message}`, colors.red);
3623
+ socket.close();
3624
+ process.exit(1);
3625
+ }
3626
+ });
3627
+
3628
+ // Send rename request
3629
+ send({
3630
+ type: 'rename_session',
3631
+ sessionId: targetSessionId,
3632
+ name: newName
3633
+ });
3634
+
3635
+ } catch (err) {
3636
+ log(`❌ ${err.message}`, colors.red);
3637
+ process.exit(1);
3638
+ }
3639
+ }
3640
+
2728
3641
  // Only start Claude if not in login mode
2729
3642
  if (!loginMode) {
2730
- if (listSessions) {
3643
+ // Handle subcommand modes first
3644
+ if (subcommandMode) {
3645
+ const { group, action } = subcommandMode;
3646
+ if (group === 'file' && action === 'list') {
3647
+ fileListMode();
3648
+ } else if (group === 'file' && action === 'upload') {
3649
+ fileUploadMode();
3650
+ } else if (group === 'session' && action === 'list') {
3651
+ sessionListMode();
3652
+ } else if (group === 'session' && action === 'rename') {
3653
+ sessionRenameMode();
3654
+ } else {
3655
+ log(`❌ Unknown command: ${group} ${action}`, colors.red);
3656
+ process.exit(1);
3657
+ }
3658
+ } else if (listSessions) {
2731
3659
  listSessionsMode();
2732
3660
  } else if (remoteAttachMode) {
2733
3661
  remoteAttachMain();