genbox 1.0.214 → 1.0.217

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,201 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renameCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const confirm_1 = __importDefault(require("@inquirer/confirm"));
10
+ const select_1 = __importDefault(require("@inquirer/select"));
11
+ const input_1 = __importDefault(require("@inquirer/input"));
12
+ const ora_1 = __importDefault(require("ora"));
13
+ const api_1 = require("../api");
14
+ const genbox_selector_1 = require("../genbox-selector");
15
+ const ssh_config_1 = require("../ssh-config");
16
+ // Name validation regex - must be lowercase alphanumeric with optional hyphens
17
+ const NAME_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
18
+ exports.renameCommand = new commander_1.Command('rename')
19
+ .description('Rename a Genbox environment')
20
+ .argument('<name>', 'Current name of the Genbox')
21
+ .argument('<new-name>', 'New name for the Genbox')
22
+ .option('-y, --yes', 'Skip confirmation prompt')
23
+ .addHelpText('after', `
24
+ Examples:
25
+ $ gb rename old-feature new-feature
26
+ $ gb rename my-env my-new-env -y
27
+ $ gb rename feat-1 feat-2
28
+
29
+ Notes:
30
+ - The genbox name must be unique within the workspace
31
+ - Subdomain URLs will automatically update to use the new name
32
+ - SSH config will be updated (old alias removed, new alias added)
33
+ `)
34
+ .action(async (name, newName, options) => {
35
+ try {
36
+ // Normalize and validate new name format
37
+ const normalizedNewName = newName.toLowerCase();
38
+ if (!NAME_REGEX.test(normalizedNewName)) {
39
+ console.log(chalk_1.default.red('Invalid name format'));
40
+ console.log(chalk_1.default.dim(' Name must be lowercase alphanumeric with optional hyphens'));
41
+ console.log(chalk_1.default.dim(' Cannot start or end with a hyphen'));
42
+ console.log(chalk_1.default.dim(' Examples: my-feature, feat-1, api'));
43
+ return;
44
+ }
45
+ if (normalizedNewName.length < 2 || normalizedNewName.length > 63) {
46
+ console.log(chalk_1.default.red('Name must be between 2 and 63 characters'));
47
+ return;
48
+ }
49
+ // Find the genbox by name
50
+ const { genbox, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
51
+ selectMessage: 'Select genbox to rename:',
52
+ });
53
+ if (cancelled || !genbox) {
54
+ console.log(chalk_1.default.dim('Cancelled.'));
55
+ return;
56
+ }
57
+ // Check if same name
58
+ if (genbox.name === normalizedNewName) {
59
+ console.log(chalk_1.default.yellow('New name is the same as current name'));
60
+ return;
61
+ }
62
+ // Check if terminated
63
+ if (genbox.status === 'terminated') {
64
+ console.log(chalk_1.default.red('Cannot rename a terminated genbox'));
65
+ return;
66
+ }
67
+ // Check name availability BEFORE confirmation
68
+ let finalNewName = normalizedNewName;
69
+ const checkResult = await (0, api_1.fetchApi)(`/genboxes/check-name?name=${encodeURIComponent(normalizedNewName)}&workspace=${encodeURIComponent(genbox.workspace || 'default')}`);
70
+ if (!checkResult.available) {
71
+ console.log(chalk_1.default.red(`Name '${normalizedNewName}' is already in use in workspace '${genbox.workspace || 'default'}'`));
72
+ // Interactive selection of alternatives
73
+ if (checkResult.suggestions && checkResult.suggestions.length > 0) {
74
+ console.log('');
75
+ const choices = [
76
+ ...checkResult.suggestions.map((s) => ({
77
+ name: s,
78
+ value: s,
79
+ })),
80
+ {
81
+ name: chalk_1.default.dim('Enter custom name...'),
82
+ value: '__custom__',
83
+ },
84
+ {
85
+ name: chalk_1.default.dim('Cancel'),
86
+ value: '__cancel__',
87
+ },
88
+ ];
89
+ const selected = await (0, select_1.default)({
90
+ message: 'Select an alternative name:',
91
+ choices,
92
+ });
93
+ if (selected === '__cancel__') {
94
+ console.log(chalk_1.default.dim('Cancelled.'));
95
+ return;
96
+ }
97
+ if (selected === '__custom__') {
98
+ const customName = await (0, input_1.default)({
99
+ message: 'Enter new name:',
100
+ validate: (value) => {
101
+ const normalized = value.toLowerCase();
102
+ if (!NAME_REGEX.test(normalized)) {
103
+ return 'Name must be lowercase alphanumeric with optional hyphens';
104
+ }
105
+ if (normalized.length < 2 || normalized.length > 63) {
106
+ return 'Name must be between 2 and 63 characters';
107
+ }
108
+ return true;
109
+ },
110
+ });
111
+ // Check if custom name is available
112
+ const customCheck = await (0, api_1.fetchApi)(`/genboxes/check-name?name=${encodeURIComponent(customName.toLowerCase())}&workspace=${encodeURIComponent(genbox.workspace || 'default')}`);
113
+ if (!customCheck.available) {
114
+ console.log(chalk_1.default.red(`Name '${customName}' is also taken. Please try again.`));
115
+ return;
116
+ }
117
+ finalNewName = customName.toLowerCase();
118
+ }
119
+ else {
120
+ finalNewName = selected;
121
+ }
122
+ }
123
+ else {
124
+ // No suggestions available
125
+ return;
126
+ }
127
+ }
128
+ // Show current info
129
+ console.log('');
130
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
131
+ console.log(` ${chalk_1.default.bold('Current name:')} ${genbox.name}`);
132
+ console.log(` ${chalk_1.default.bold('New name:')} ${chalk_1.default.cyan(finalNewName)}`);
133
+ console.log(` ${chalk_1.default.bold('Workspace:')} ${genbox.workspace || 'default'}`);
134
+ console.log(` ${chalk_1.default.bold('Status:')} ${genbox.status}`);
135
+ console.log(chalk_1.default.dim('───────────────────────────────────────────────'));
136
+ console.log('');
137
+ // Confirm rename
138
+ let confirmed = options.yes;
139
+ if (!confirmed) {
140
+ confirmed = await (0, confirm_1.default)({
141
+ message: `Rename genbox from '${genbox.name}' to '${finalNewName}'?`,
142
+ default: true,
143
+ });
144
+ }
145
+ if (!confirmed) {
146
+ console.log(chalk_1.default.dim('Cancelled.'));
147
+ return;
148
+ }
149
+ // Perform rename via API
150
+ const spinner = (0, ora_1.default)(`Renaming '${genbox.name}' to '${finalNewName}'...`).start();
151
+ const result = await (0, api_1.fetchApi)(`/genboxes/${genbox._id}/rename`, {
152
+ method: 'PATCH',
153
+ body: JSON.stringify({ newName: finalNewName }),
154
+ });
155
+ // Check for error response
156
+ if (result.error) {
157
+ spinner.fail(chalk_1.default.red(result.message || 'Failed to rename genbox'));
158
+ return;
159
+ }
160
+ // Update SSH config - remove old, add new
161
+ const oldName = result.oldName || genbox.name;
162
+ (0, ssh_config_1.removeSshConfigEntry)(oldName);
163
+ if (genbox.ipAddress) {
164
+ (0, ssh_config_1.addSshConfigEntry)({
165
+ name: finalNewName,
166
+ ipAddress: genbox.ipAddress,
167
+ });
168
+ }
169
+ spinner.succeed(chalk_1.default.green(`Renamed '${oldName}' to '${finalNewName}'`));
170
+ // Display new URLs
171
+ if (result.urls && Object.keys(result.urls).length > 0) {
172
+ console.log('');
173
+ console.log(chalk_1.default.bold('New URLs:'));
174
+ for (const [service, url] of Object.entries(result.urls)) {
175
+ console.log(` ${chalk_1.default.cyan(service.padEnd(12))} ${url}`);
176
+ }
177
+ }
178
+ // Display SSH info
179
+ console.log('');
180
+ console.log(chalk_1.default.bold('SSH Access:'));
181
+ console.log(` ${chalk_1.default.dim('Old alias:')} ${chalk_1.default.strikethrough((0, ssh_config_1.getSshAlias)(oldName))}`);
182
+ console.log(` ${chalk_1.default.dim('New alias:')} ${chalk_1.default.cyan((0, ssh_config_1.getSshAlias)(finalNewName))}`);
183
+ // Display helpful commands
184
+ console.log('');
185
+ console.log(chalk_1.default.bold('Commands:'));
186
+ console.log(` Connect: ${chalk_1.default.cyan(`gb connect ${finalNewName}`)}`);
187
+ console.log(` Status: ${chalk_1.default.cyan(`gb status ${finalNewName}`)}`);
188
+ }
189
+ catch (error) {
190
+ // Handle user cancellation (Ctrl+C)
191
+ if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
192
+ console.log(chalk_1.default.dim('\nCancelled.'));
193
+ return;
194
+ }
195
+ if (error instanceof api_1.AuthenticationError) {
196
+ (0, api_1.handleApiError)(error);
197
+ return;
198
+ }
199
+ console.error(chalk_1.default.red(`Error: ${error.message}`));
200
+ }
201
+ });
@@ -50,11 +50,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.sessionLogsCommand = void 0;
51
51
  const commander_1 = require("commander");
52
52
  const chalk_1 = __importDefault(require("chalk"));
53
+ const child_process_1 = require("child_process");
53
54
  const path = __importStar(require("path"));
54
55
  const os = __importStar(require("os"));
55
56
  const fs = __importStar(require("fs"));
56
57
  const unified_session_1 = require("../../lib/unified-session");
57
58
  const api_1 = require("../../api");
59
+ /**
60
+ * Query genbox daemon directly via SSH to get session data
61
+ */
62
+ async function queryGenboxDaemon(genboxName, sessionName) {
63
+ try {
64
+ const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
65
+ const data = JSON.parse(result);
66
+ const sessions = data.sessions || [];
67
+ if (sessionName && sessions.length > 0) {
68
+ const provider = sessionName.split('-')[0];
69
+ return sessions.find((s) => s.provider === provider) || sessions[0];
70
+ }
71
+ return sessions[0] || null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Query genbox daemon for events via SSH
79
+ */
80
+ async function queryGenboxDaemonEvents(genboxName, sessionId, limit = 50) {
81
+ try {
82
+ const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s "http://127.0.0.1:47191/api/storage/events?sessionId=${sessionId}&limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47192/api/storage/events?sessionId=${sessionId}&limit=${limit}" 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
83
+ const data = JSON.parse(result);
84
+ return data.events || [];
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
58
90
  /**
59
91
  * Get SSH key path
60
92
  */
@@ -356,10 +388,92 @@ Examples:
356
388
  process.exit(1);
357
389
  }
358
390
  if (found.remoteSession) {
359
- console.log(chalk_1.default.yellow(`\nRemote session: ${found.remoteSession.name}`));
391
+ // Query daemon for session data and events
392
+ const daemonSession = await queryGenboxDaemon(found.remoteSession.genboxName, found.remoteSession.name);
393
+ if (!daemonSession) {
394
+ console.log(chalk_1.default.yellow(`\nRemote session: ${found.remoteSession.name}`));
395
+ console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
396
+ console.log(chalk_1.default.dim('\nNo data available from daemon. Session may be starting up.'));
397
+ console.log(chalk_1.default.cyan(`\n gb ${found.remoteSession.name.split('-')[0]} attach ${found.remoteSession.name}\n`));
398
+ process.exit(0);
399
+ }
400
+ const events = await queryGenboxDaemonEvents(found.remoteSession.genboxName, daemonSession.id, parseInt(options.tail || '50'));
401
+ if (options.json) {
402
+ console.log(JSON.stringify({ session: daemonSession, events }, null, 2));
403
+ process.exit(0);
404
+ }
405
+ if (options.events) {
406
+ // Show events from daemon
407
+ console.log(chalk_1.default.bold(`\nSession Events: ${found.remoteSession.name}`));
408
+ console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
409
+ console.log(chalk_1.default.dim('─'.repeat(70)));
410
+ if (events.length === 0) {
411
+ console.log(chalk_1.default.dim('\nNo events recorded yet.'));
412
+ console.log(chalk_1.default.dim('Events will appear as Claude/Gemini processes prompts.\n'));
413
+ }
414
+ else {
415
+ for (const evt of events.reverse()) {
416
+ const icon = getEventIcon(evt.type);
417
+ const time = chalk_1.default.dim(`[${formatTime(evt.createdAt)}]`);
418
+ const eventLabel = (evt.type || '').replace(/_/g, ' ');
419
+ let output = `${time} ${icon} ${eventLabel}`;
420
+ if (evt.data) {
421
+ if (evt.data.toolName) {
422
+ output += chalk_1.default.cyan(` ${evt.data.toolName}`);
423
+ }
424
+ if (evt.data.currentState) {
425
+ output += chalk_1.default.yellow(` → ${evt.data.currentState}`);
426
+ }
427
+ }
428
+ console.log(output);
429
+ }
430
+ }
431
+ console.log('');
432
+ process.exit(0);
433
+ }
434
+ // Show session info and recent activity
435
+ console.log(chalk_1.default.bold(`\nSession: ${found.remoteSession.name}`));
360
436
  console.log(chalk_1.default.dim(`Running on: ${found.remoteSession.genboxName}`));
361
- console.log(chalk_1.default.dim('\nRemote sessions must be attached to view logs.'));
362
- console.log(chalk_1.default.cyan(`\n gb ${found.remoteSession.name.split('-')[0]} attach ${found.remoteSession.name}\n`));
437
+ console.log(chalk_1.default.dim('─'.repeat(70)));
438
+ // Session status
439
+ const statusIcon = daemonSession.status === 'active' ? chalk_1.default.green('●') :
440
+ daemonSession.status === 'idle' ? chalk_1.default.yellow('●') : chalk_1.default.green('●');
441
+ console.log(` Status: ${statusIcon} ${daemonSession.status || 'running'}`);
442
+ if (daemonSession.currentState) {
443
+ const stateLabel = daemonSession.currentState.replace(/_/g, ' ');
444
+ console.log(` State: ${stateLabel}${daemonSession.currentToolName ? ` (${daemonSession.currentToolName})` : ''}`);
445
+ }
446
+ console.log(` Messages: ${daemonSession.messageCount || 0} total (${daemonSession.userPromptCount || 0} user, ${daemonSession.assistantResponseCount || 0} assistant)`);
447
+ console.log(` Tools: ${daemonSession.toolUseCount || 0} calls`);
448
+ if (daemonSession.totalInputTokens || daemonSession.totalOutputTokens) {
449
+ console.log(` Tokens: ${daemonSession.totalInputTokens || 0} in / ${daemonSession.totalOutputTokens || 0} out`);
450
+ }
451
+ if (daemonSession.estimatedCostUsd) {
452
+ console.log(` Cost: $${daemonSession.estimatedCostUsd.toFixed(2)}`);
453
+ }
454
+ // Show recent events
455
+ if (events.length > 0) {
456
+ console.log(chalk_1.default.dim('\n─'.repeat(70)));
457
+ console.log(chalk_1.default.dim('Recent activity:'));
458
+ for (const evt of events.slice(0, 10).reverse()) {
459
+ const icon = getEventIcon(evt.type);
460
+ const time = chalk_1.default.dim(`[${formatTime(evt.createdAt)}]`);
461
+ const eventLabel = (evt.type || '').replace(/_/g, ' ');
462
+ let output = ` ${time} ${icon} ${eventLabel}`;
463
+ if (evt.data?.toolName)
464
+ output += chalk_1.default.cyan(` ${evt.data.toolName}`);
465
+ console.log(output);
466
+ }
467
+ if (events.length > 10) {
468
+ console.log(chalk_1.default.dim(` ... and ${events.length - 10} more events`));
469
+ }
470
+ }
471
+ console.log('');
472
+ console.log(chalk_1.default.dim('Commands:'));
473
+ const provider = found.remoteSession.name.split('-')[0];
474
+ console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session logs ${found.remoteSession.name} --events`) + chalk_1.default.dim(' # Full event log'));
475
+ console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb ${provider} attach ${found.remoteSession.name}`) + chalk_1.default.dim(' # Attach to session'));
476
+ console.log('');
363
477
  process.exit(0);
364
478
  }
365
479
  const session = found.session;
@@ -16,8 +16,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.sessionShowCommand = void 0;
17
17
  const commander_1 = require("commander");
18
18
  const chalk_1 = __importDefault(require("chalk"));
19
+ const child_process_1 = require("child_process");
19
20
  const unified_session_1 = require("../../lib/unified-session");
20
21
  const api_1 = require("../../api");
22
+ /**
23
+ * Query genbox daemon directly via SSH to get session data
24
+ */
25
+ async function queryGenboxDaemon(genboxName, sessionName) {
26
+ try {
27
+ const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
28
+ const data = JSON.parse(result);
29
+ const sessions = data.sessions || [];
30
+ if (sessionName && sessions.length > 0) {
31
+ // Find matching session by name prefix (e.g., "claude-e2e-test" matches provider "claude")
32
+ const provider = sessionName.split('-')[0];
33
+ return sessions.find((s) => s.provider === provider) || sessions[0];
34
+ }
35
+ return sessions[0] || null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Query genbox daemon for events
43
+ */
44
+ async function queryGenboxDaemonEvents(genboxName, limit = 50) {
45
+ try {
46
+ const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s "http://127.0.0.1:47191/api/storage/events?limit=${limit}" 2>/dev/null || curl -s "http://127.0.0.1:47192/api/storage/events?limit=${limit}" 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
47
+ const data = JSON.parse(result);
48
+ return data.events || [];
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
21
54
  /**
22
55
  * Find session by name or ID
23
56
  */
@@ -298,20 +331,86 @@ Examples:
298
331
  process.exit(1);
299
332
  }
300
333
  if (found.remoteSession) {
301
- // Remote session - limited info
334
+ // Remote session - query daemon for live data
335
+ const daemonSession = await queryGenboxDaemon(found.remoteSession.genboxName, found.remoteSession.name);
302
336
  if (options.json) {
303
- console.log(JSON.stringify(found.remoteSession, null, 2));
337
+ console.log(JSON.stringify({
338
+ ...found.remoteSession,
339
+ daemonData: daemonSession || undefined,
340
+ }, null, 2));
341
+ process.exit(0);
304
342
  }
305
- else {
306
- console.log(chalk_1.default.bold(`\nRemote Session: ${found.remoteSession.name}`));
307
- console.log(chalk_1.default.dim(''.repeat(50)));
308
- console.log(` Genbox: ${found.remoteSession.genboxName}`);
309
- console.log(` IP: ${found.remoteSession.ipAddress}`);
310
- console.log(` Status: ${chalk_1.default.green('running')}`);
343
+ console.log('');
344
+ // Header section
345
+ const headerContent = [''];
346
+ const statusIcon = daemonSession?.status === 'active' ? chalk_1.default.green('●') :
347
+ daemonSession?.status === 'idle' ? chalk_1.default.yellow('●') : chalk_1.default.green('●');
348
+ headerContent.push(` ${statusIcon} Status: ${chalk_1.default.bold(daemonSession?.status || 'running')}`);
349
+ if (daemonSession?.currentState) {
350
+ const stateLabel = daemonSession.currentState.replace(/_/g, ' ');
351
+ const stateIcon = getStateIcon(daemonSession.currentState);
352
+ headerContent.push(` ${stateIcon} State: ${chalk_1.default.cyan(stateLabel)}${daemonSession.currentToolName ? ` (${daemonSession.currentToolName})` : ''}`);
353
+ }
354
+ headerContent.push(` Provider: ${chalk_1.default.magenta(daemonSession?.provider || found.remoteSession.name.split('-')[0])}`);
355
+ headerContent.push(` Type: cloud`);
356
+ headerContent.push(` Genbox: ${chalk_1.default.cyan(found.remoteSession.genboxName)}`);
357
+ headerContent.push(` IP: ${found.remoteSession.ipAddress}`);
358
+ if (daemonSession?.projectPath) {
359
+ headerContent.push(` Project: ${chalk_1.default.dim(daemonSession.projectPath)}`);
360
+ }
361
+ headerContent.push('');
362
+ renderBox(`Session: ${found.remoteSession.name}`, headerContent);
363
+ // Metrics section if daemon data available
364
+ if (daemonSession) {
311
365
  console.log('');
312
- console.log(chalk_1.default.dim('Attach to see full details:'));
313
- console.log(chalk_1.default.cyan(` gb session attach ${found.remoteSession.name}\n`));
366
+ const metricsContent = [''];
367
+ // Duration
368
+ const duration = daemonSession.startedAt
369
+ ? formatDuration(Date.now() - daemonSession.startedAt)
370
+ : '--';
371
+ metricsContent.push(` Duration: ${duration}`);
372
+ // Tokens
373
+ const inputTokens = daemonSession.totalInputTokens || 0;
374
+ const outputTokens = daemonSession.totalOutputTokens || 0;
375
+ const cacheTokens = daemonSession.totalCacheReadTokens || 0;
376
+ const cacheRate = calculateCacheHitRate(cacheTokens, inputTokens);
377
+ metricsContent.push(` Tokens: Input: ${formatTokens(inputTokens)} Output: ${formatTokens(outputTokens)} Cache: ${cacheRate}`);
378
+ // Context usage
379
+ if (daemonSession.contextTokens && daemonSession.maxContextTokens) {
380
+ const contextPercent = ((daemonSession.contextTokens / daemonSession.maxContextTokens) * 100).toFixed(0);
381
+ metricsContent.push(` Context: ${formatTokens(daemonSession.contextTokens)} / ${formatTokens(daemonSession.maxContextTokens)} (${contextPercent}%)`);
382
+ }
383
+ // Cost
384
+ if (daemonSession.estimatedCostUsd !== undefined) {
385
+ metricsContent.push(` Cost: $${daemonSession.estimatedCostUsd.toFixed(2)} USD`);
386
+ }
387
+ // Messages
388
+ metricsContent.push(` Messages: User: ${daemonSession.userPromptCount || 0} Assistant: ${daemonSession.assistantResponseCount || 0}`);
389
+ // Tools
390
+ if (daemonSession.toolUseCount) {
391
+ metricsContent.push(` Tools: ${daemonSession.toolUseCount} total`);
392
+ }
393
+ metricsContent.push('');
394
+ renderBox('Metrics', metricsContent);
395
+ }
396
+ // Last message preview
397
+ if (daemonSession?.lastMessagePreview) {
398
+ console.log('');
399
+ const previewContent = [
400
+ '',
401
+ ` ${chalk_1.default.italic(daemonSession.lastMessagePreview.substring(0, 200))}${daemonSession.lastMessagePreview.length > 200 ? '...' : ''}`,
402
+ '',
403
+ ];
404
+ renderBox('Last Message', previewContent);
314
405
  }
406
+ // Actions hint
407
+ console.log('');
408
+ console.log(chalk_1.default.dim(' Commands:'));
409
+ const provider = found.remoteSession.name.split('-')[0];
410
+ console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb ${provider} attach ${found.remoteSession.name}`) + chalk_1.default.dim(' # Attach to session'));
411
+ console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session logs ${found.remoteSession.name} -f`) + chalk_1.default.dim(' # Follow logs'));
412
+ console.log(chalk_1.default.dim(' ') + chalk_1.default.cyan(`gb session send ${found.remoteSession.name}`) + chalk_1.default.dim(' # Send prompt'));
413
+ console.log('');
315
414
  process.exit(0);
316
415
  }
317
416
  const session = found.session;
@@ -98,17 +98,59 @@ async function fetchLiveState(sessionId) {
98
98
  }
99
99
  }
100
100
  /**
101
- * Fetch Claude sessions for a specific genbox from API
101
+ * Query genbox daemon directly via SSH to get live session data
102
+ * Uses /api/storage/sessions which returns sessions from SQLite with currentState, currentToolName, etc.
102
103
  */
103
- async function fetchGenboxSessions(genboxId) {
104
+ async function queryGenboxDaemon(genboxName) {
104
105
  try {
105
- const response = await (0, api_1.fetchApi)(`/claude/sessions/genbox/${genboxId}`);
106
- return Array.isArray(response) ? response : [];
106
+ // Query the storage sessions endpoint for richer session data
107
+ const result = (0, child_process_1.execSync)(`ssh genbox-${genboxName} 'curl -s http://127.0.0.1:47191/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47192/api/storage/sessions 2>/dev/null || curl -s http://127.0.0.1:47193/api/storage/sessions 2>/dev/null' 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
108
+ const data = JSON.parse(result);
109
+ return data.sessions || [];
107
110
  }
108
111
  catch {
109
112
  return [];
110
113
  }
111
114
  }
115
+ /**
116
+ * Fetch sessions for a specific genbox from API
117
+ * Queries both ClaudeSession (monitoring) and Session (sync) collections
118
+ */
119
+ async function fetchGenboxSessions(genboxId) {
120
+ const allSessions = [];
121
+ try {
122
+ // Try ClaudeSession (monitoring) first
123
+ const claudeSessions = await (0, api_1.fetchApi)(`/claude/sessions/genbox/${genboxId}`);
124
+ if (Array.isArray(claudeSessions)) {
125
+ allSessions.push(...claudeSessions);
126
+ }
127
+ }
128
+ catch {
129
+ // Ignore errors
130
+ }
131
+ try {
132
+ // Query Session (sync) collection filtered by genboxId
133
+ const sessions = await (0, api_1.fetchApi)(`/sessions/v2?genboxId=${encodeURIComponent(genboxId)}&limit=50`);
134
+ if (sessions && Array.isArray(sessions.sessions)) {
135
+ allSessions.push(...sessions.sessions);
136
+ }
137
+ }
138
+ catch {
139
+ // Ignore errors - try unfiltered if genboxId filter not supported
140
+ try {
141
+ const sessions = await (0, api_1.fetchApi)(`/sessions/v2?limit=50`);
142
+ if (sessions && Array.isArray(sessions.sessions)) {
143
+ // Fallback: filter locally by genboxId or type=cloud
144
+ const cloudSessions = sessions.sessions.filter((s) => s.genboxId === genboxId || (s.type === 'cloud' && !s.genboxId));
145
+ allSessions.push(...cloudSessions);
146
+ }
147
+ }
148
+ catch {
149
+ // Ignore errors
150
+ }
151
+ }
152
+ return allSessions;
153
+ }
112
154
  /**
113
155
  * Match a remote session name to an API session
114
156
  * Remote session names are like "claude-abc123" (from socket file)
@@ -198,17 +240,22 @@ async function collectSessionStates(options) {
198
240
  // Group remote sessions by genbox to batch API calls
199
241
  const remoteByGenbox = new Map();
200
242
  for (const remote of result.remoteSessions) {
201
- const key = remote.genboxId;
243
+ const key = remote.genboxName || remote.genboxId; // Use genboxName as primary key
202
244
  if (!remoteByGenbox.has(key)) {
203
245
  remoteByGenbox.set(key, []);
204
246
  }
205
247
  remoteByGenbox.get(key).push(remote);
206
248
  }
207
- // Fetch API sessions for each genbox in parallel
249
+ // Fetch API sessions and daemon sessions for each genbox in parallel
208
250
  const genboxSessionsMap = new Map();
251
+ const genboxDaemonSessionsMap = new Map();
209
252
  await Promise.all(Array.from(remoteByGenbox.keys()).map(async (genboxId) => {
210
- const apiSessions = await fetchGenboxSessions(genboxId);
253
+ const [apiSessions, daemonSessions] = await Promise.all([
254
+ fetchGenboxSessions(genboxId),
255
+ queryGenboxDaemon(genboxId),
256
+ ]);
211
257
  genboxSessionsMap.set(genboxId, apiSessions);
258
+ genboxDaemonSessionsMap.set(genboxId, daemonSessions);
212
259
  }));
213
260
  for (const remote of result.remoteSessions) {
214
261
  // Filter by genbox if specified
@@ -220,25 +267,39 @@ async function collectSessionStates(options) {
220
267
  // Remote sessions are running by definition (we found their socket)
221
268
  if (options.status === 'stopped')
222
269
  continue;
223
- // Try to match this remote session to an API session (ClaudeSession)
224
- const apiSessions = genboxSessionsMap.get(remote.genboxId) || [];
270
+ // Try to match this remote session to API or daemon sessions
271
+ const genboxKey = remote.genboxName || remote.genboxId;
272
+ const apiSessions = genboxSessionsMap.get(genboxKey) || [];
273
+ const daemonSessions = genboxDaemonSessionsMap.get(genboxKey) || [];
225
274
  const matchedApiSession = matchRemoteSessionToApiSession(remote.name, apiSessions);
226
- // ClaudeSession has status: 'active', 'idle', 'waiting_input', 'error', 'ended'
227
- // Map to our display states
228
- const apiStatus = matchedApiSession?.status;
275
+ // Find the most recent active daemon session for live state
276
+ // Daemon sessions have currentState and currentToolName which are most accurate
277
+ const activeDaemonSessions = daemonSessions.filter((s) => s.status === 'active' || s.status === 'idle' ||
278
+ s.currentState === 'executing_tool' || s.currentState === 'thinking' || s.currentState === 'waiting_for_input');
279
+ // Sort by lastActivityAt desc to get the most recent
280
+ activeDaemonSessions.sort((a, b) => (b.lastActivityAt || 0) - (a.lastActivityAt || 0));
281
+ const daemonSession = activeDaemonSessions[0] || daemonSessions[0];
229
282
  let displayStatus = 'running';
230
283
  let displayState;
231
- if (matchedApiSession) {
232
- // We have API data - use it
284
+ let currentTool;
285
+ // Prefer daemon data for live state (most accurate)
286
+ if (daemonSession?.currentState) {
287
+ displayState = daemonSession.currentState;
288
+ currentTool = daemonSession.currentToolName;
289
+ displayStatus = daemonSession.status === 'active' ? 'running' :
290
+ daemonSession.status === 'idle' ? 'idle' : 'running';
291
+ }
292
+ else if (matchedApiSession) {
293
+ // Fall back to API data
294
+ const apiStatus = matchedApiSession.status;
233
295
  switch (apiStatus) {
234
296
  case 'active':
297
+ case 'running':
235
298
  displayStatus = 'running';
236
- displayState = 'thinking';
299
+ displayState = matchedApiSession.currentState || 'thinking';
300
+ currentTool = matchedApiSession.currentToolName;
237
301
  break;
238
302
  case 'waiting_input':
239
- displayStatus = 'idle';
240
- displayState = 'waiting_for_input';
241
- break;
242
303
  case 'idle':
243
304
  displayStatus = 'idle';
244
305
  displayState = 'waiting_for_input';
@@ -247,6 +308,7 @@ async function collectSessionStates(options) {
247
308
  displayStatus = 'error';
248
309
  break;
249
310
  case 'ended':
311
+ case 'stopped':
250
312
  displayStatus = 'stopped';
251
313
  break;
252
314
  default:
@@ -254,9 +316,9 @@ async function collectSessionStates(options) {
254
316
  }
255
317
  }
256
318
  else {
257
- // No API data - session is running (we found socket) but state is unknown
319
+ // No data - session is running (we found socket) but state is unknown
258
320
  displayStatus = 'running';
259
- displayState = undefined; // Will show as "idle" indicator but no state text
321
+ displayState = undefined;
260
322
  }
261
323
  states.push({
262
324
  id: matchedApiSession?.sessionId?.substring(0, 8) || remote.name.substring(0, 8),
@@ -266,12 +328,17 @@ async function collectSessionStates(options) {
266
328
  genbox: remote.genboxName,
267
329
  status: displayStatus,
268
330
  state: displayState,
269
- currentTool: undefined, // ClaudeSession doesn't track current tool
270
- lastMessage: matchedApiSession?.lastPrompt?.substring(0, 50),
271
- duration: matchedApiSession?.createdAt ? formatDuration(matchedApiSession.createdAt) : '?',
272
- createdAt: matchedApiSession?.createdAt,
273
- toolCalls: matchedApiSession?.toolUseCount,
274
- messagesCount: matchedApiSession?.promptCount,
331
+ currentTool: currentTool, // From daemon data
332
+ lastMessage: matchedApiSession?.lastPrompt?.substring(0, 50) || daemonSession?.lastMessagePreview?.substring(0, 50),
333
+ duration: matchedApiSession?.createdAt ? formatDuration(matchedApiSession.createdAt) :
334
+ daemonSession?.startedAt ? formatDuration(new Date(daemonSession.startedAt).toISOString()) : '?',
335
+ createdAt: matchedApiSession?.createdAt || (daemonSession?.startedAt ? new Date(daemonSession.startedAt).toISOString() : undefined),
336
+ tokens: daemonSession ? Math.round((daemonSession.totalInputTokens + daemonSession.totalOutputTokens) / 1000) : undefined,
337
+ inputTokens: daemonSession?.totalInputTokens,
338
+ outputTokens: daemonSession?.totalOutputTokens,
339
+ cost: daemonSession?.estimatedCostUsd,
340
+ toolCalls: daemonSession?.toolUseCount || matchedApiSession?.toolUseCount,
341
+ messagesCount: daemonSession?.messageCount || matchedApiSession?.promptCount,
275
342
  });
276
343
  }
277
344
  // Also add cloud genboxes that might have sessions we didn't scan
@@ -288,8 +355,9 @@ async function collectSessionStates(options) {
288
355
  continue;
289
356
  if (genbox.status === 'running' && options.status === 'stopped')
290
357
  continue;
358
+ const genboxId = genbox.id || genbox._id;
291
359
  states.push({
292
- id: genbox.id.substring(0, 8),
360
+ id: genboxId?.substring(0, 8) || 'unknown',
293
361
  name: genbox.name,
294
362
  provider: 'claude', // Default, could be any
295
363
  type: 'cloud',
@@ -810,8 +878,14 @@ async function runTuiMode(options) {
810
878
  process.stdin.setEncoding('utf8');
811
879
  process.stdin.on('data', handleInput);
812
880
  }
813
- // Initial render
881
+ // Initial render with debug
882
+ if (process.env.DEBUG) {
883
+ console.log(`[DEBUG] Fetching sessions...`);
884
+ }
814
885
  await render();
886
+ if (process.env.DEBUG) {
887
+ console.log(`[DEBUG] Loaded ${allSessionsCache.length} sessions, showing ${cachedSessions.length} after filters`);
888
+ }
815
889
  // Setup refresh interval (event-loop friendly)
816
890
  const refreshInterval = setInterval(async () => {
817
891
  if (running && inputMode === 'normal') {
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ const validate_1 = require("./commands/validate");
61
61
  const migrate_1 = require("./commands/migrate");
62
62
  const ssh_setup_1 = require("./commands/ssh-setup");
63
63
  const rebuild_1 = require("./commands/rebuild");
64
+ const rename_1 = require("./commands/rename");
64
65
  const extend_1 = require("./commands/extend");
65
66
  const cleanup_ssh_1 = require("./commands/cleanup-ssh");
66
67
  const restart_1 = require("./commands/restart");
@@ -152,6 +153,7 @@ const commandCategories = {
152
153
  { name: 'start', aliases: ['resume'], description: 'Start a stopped Genbox' },
153
154
  { name: 'stop', aliases: [], description: 'Stop a running Genbox' },
154
155
  { name: 'restart', aliases: [], description: 'Restart services in a Genbox' },
156
+ { name: 'rename', aliases: [], description: 'Rename a Genbox' },
155
157
  { name: 'extend', aliases: [], description: 'Extend Genbox lifetime' },
156
158
  { name: 'rebuild', aliases: [], description: 'Rebuild a Genbox with new config' },
157
159
  ],
@@ -297,6 +299,7 @@ program
297
299
  .addCommand(migrate_1.deprecationsCommand, { hidden: true })
298
300
  .addCommand(ssh_setup_1.sshSetupCommand, { hidden: true })
299
301
  .addCommand(rebuild_1.rebuildCommand)
302
+ .addCommand(rename_1.renameCommand)
300
303
  .addCommand(extend_1.extendCommand)
301
304
  .addCommand(cleanup_ssh_1.cleanupSshCommand, { hidden: true })
302
305
  .addCommand(restart_1.restartCommand)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genbox",
3
- "version": "1.0.214",
3
+ "version": "1.0.217",
4
4
  "description": "Genbox CLI - AI-Powered Development Environments",
5
5
  "main": "dist/index.js",
6
6
  "bin": {