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 +31 -0
- package/agent/agent.js +123 -5
- package/package.json +8 -3
- package/vibe.js +936 -8
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
|
|
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(
|
|
1466
|
-
console.log(
|
|
1467
|
-
|
|
1468
|
-
|
|
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.
|
|
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
|
-
//
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
:
|
|
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
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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
|
-
|
|
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();
|