@aitherium/shell-cli 1.1.0

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,685 @@
1
+ /**
2
+ * Agent Notebook interactive TUI for AitherShell CLI.
3
+ *
4
+ * Provides a proper interactive notebook experience in the terminal:
5
+ * /nb list — Browse notebooks with status/effort/tags
6
+ * /nb open <id> — Open interactive session (cell-by-cell execution)
7
+ * /nb run <id> — Execute entire notebook (streaming output)
8
+ * /nb plan <prompt> — Create notebook from natural language
9
+ * /nb create <name> — Create empty notebook
10
+ * /nb templates — List available templates
11
+ * /nb sessions — List active sessions
12
+ *
13
+ * The interactive session (/nb open) renders cells with box-drawing
14
+ * characters, colored output, and supports Shift+Enter / Enter to
15
+ * execute cells one at a time.
16
+ */
17
+ import chalk from 'chalk';
18
+ import ora from 'ora';
19
+ import { createInterface } from 'node:readline';
20
+ import { formatTable } from './renderer.js';
21
+ // ── Cell type styling ────────────────────────────────────────────────────────
22
+ const CELL_ICONS = {
23
+ prompt: '\u25B6', // play
24
+ tool_call: '\u2692', // hammer+pick
25
+ agent_delegate: '\u2691', // flag
26
+ service_call: '\u2601', // cloud
27
+ script: '\u2630', // trigram
28
+ checkpoint: '\u26A0', // warning
29
+ context: '\u2139', // info
30
+ plan: '\u2605', // star
31
+ note: '\u2709', // envelope
32
+ result: '\u2713', // check
33
+ condition: '\u2753', // question
34
+ parallel_block: '\u2261', // triple bar
35
+ loop: '\u21BB', // loop arrow
36
+ mcts_branch: '\u2726', // 4-pointed star
37
+ transform: '\u21C4', // left-right arrows
38
+ };
39
+ const CELL_COLORS = {
40
+ prompt: chalk.cyan,
41
+ tool_call: chalk.yellow,
42
+ agent_delegate: chalk.magenta,
43
+ service_call: chalk.blue,
44
+ script: chalk.green,
45
+ checkpoint: chalk.red,
46
+ context: chalk.dim,
47
+ plan: chalk.white,
48
+ note: chalk.gray,
49
+ result: chalk.greenBright,
50
+ };
51
+ const STATUS_BADGES = {
52
+ pending: chalk.dim('\u25CB pending'),
53
+ running: chalk.yellow('\u25CF running'),
54
+ completed: chalk.green('\u2713 done'),
55
+ failed: chalk.red('\u2717 failed'),
56
+ skipped: chalk.dim('\u2212 skipped'),
57
+ waiting: chalk.yellow('\u23F3 waiting'),
58
+ };
59
+ // ── Box drawing helpers ──────────────────────────────────────────────────────
60
+ const BOX = {
61
+ tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F',
62
+ h: '\u2500', v: '\u2502', vr: '\u251C', vl: '\u2524',
63
+ };
64
+ function boxTop(title, width) {
65
+ const inner = width - 4;
66
+ const titleStr = ` ${title} `;
67
+ const pad = Math.max(0, inner - titleStr.length);
68
+ return chalk.dim(`${BOX.tl}${BOX.h}`) + chalk.bold(titleStr) + chalk.dim(BOX.h.repeat(pad) + BOX.tr);
69
+ }
70
+ function boxLine(content, width) {
71
+ const stripped = content.replace(/\x1b\[[0-9;]*m/g, '');
72
+ const pad = Math.max(0, width - 4 - stripped.length);
73
+ return chalk.dim(BOX.v) + ' ' + content + ' '.repeat(pad) + ' ' + chalk.dim(BOX.v);
74
+ }
75
+ function boxBottom(width) {
76
+ return chalk.dim(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`);
77
+ }
78
+ function boxSeparator(width) {
79
+ return chalk.dim(`${BOX.vr}${BOX.h.repeat(width - 2)}${BOX.vl}`);
80
+ }
81
+ function renderCell(cell, index, total, width) {
82
+ const icon = CELL_ICONS[cell.type] || '\u25A1';
83
+ const colorFn = CELL_COLORS[cell.type] || chalk.white;
84
+ const status = STATUS_BADGES[cell.status || 'pending'] || '';
85
+ const cellNum = `[${index + 1}/${total}]`;
86
+ const lines = [];
87
+ const title = `${icon} ${colorFn(cell.name || cell.type)} ${chalk.dim(cellNum)} ${status}`;
88
+ lines.push(boxTop(title, width));
89
+ // Cell content preview
90
+ const contentKey = cell.type === 'prompt' ? 'prompt'
91
+ : cell.type === 'tool_call' ? 'tool'
92
+ : cell.type === 'agent_delegate' ? 'agent'
93
+ : cell.type === 'script' ? 'script'
94
+ : cell.type === 'checkpoint' ? 'message'
95
+ : 'prompt';
96
+ const content = cell.config?.[contentKey]
97
+ || cell.config?.prompt
98
+ || cell.config?.description
99
+ || '';
100
+ if (content) {
101
+ const preview = String(content).slice(0, width - 8).split('\n').slice(0, 3);
102
+ for (const line of preview) {
103
+ lines.push(boxLine(chalk.dim(line), width));
104
+ }
105
+ if (String(content).length > width - 8 || String(content).split('\n').length > 3) {
106
+ lines.push(boxLine(chalk.dim('...'), width));
107
+ }
108
+ }
109
+ // Tool-specific info
110
+ if (cell.type === 'tool_call' && cell.config?.tool) {
111
+ lines.push(boxLine(chalk.yellow(`tool: ${cell.config.tool}`), width));
112
+ if (cell.config?.arguments) {
113
+ const args = typeof cell.config.arguments === 'object'
114
+ ? JSON.stringify(cell.config.arguments).slice(0, width - 16)
115
+ : String(cell.config.arguments).slice(0, width - 16);
116
+ lines.push(boxLine(chalk.dim(`args: ${args}`), width));
117
+ }
118
+ }
119
+ if (cell.type === 'agent_delegate' && cell.config?.agent) {
120
+ lines.push(boxLine(chalk.magenta(`agent: ${cell.config.agent}`), width));
121
+ }
122
+ if (cell.type === 'checkpoint') {
123
+ lines.push(boxLine(chalk.red.bold('REQUIRES APPROVAL'), width));
124
+ }
125
+ // Output section (if executed)
126
+ if (cell.output) {
127
+ lines.push(boxSeparator(width));
128
+ lines.push(boxLine(chalk.green.bold('Output:'), width));
129
+ const outputLines = cell.output.split('\n').slice(0, 8);
130
+ for (const line of outputLines) {
131
+ lines.push(boxLine(line.slice(0, width - 6), width));
132
+ }
133
+ if (cell.output.split('\n').length > 8) {
134
+ lines.push(boxLine(chalk.dim(`... ${cell.output.split('\n').length - 8} more lines`), width));
135
+ }
136
+ }
137
+ // Metrics
138
+ if (cell.tokens || cell.cost || cell.duration_ms) {
139
+ const metrics = [];
140
+ if (cell.tokens)
141
+ metrics.push(`${cell.tokens} tok`);
142
+ if (cell.cost)
143
+ metrics.push(`$${cell.cost.toFixed(4)}`);
144
+ if (cell.duration_ms)
145
+ metrics.push(`${(cell.duration_ms / 1000).toFixed(1)}s`);
146
+ lines.push(boxLine(chalk.dim(metrics.join(' \u2502 ')), width));
147
+ }
148
+ lines.push(boxBottom(width));
149
+ return lines.join('\n');
150
+ }
151
+ // ── Subcommand handlers ──────────────────────────────────────────────────────
152
+ async function nbList(client, args) {
153
+ const spinner = ora('Loading notebooks...').start();
154
+ const data = await client.get('/notebooks/');
155
+ spinner.stop();
156
+ if (!data || !data.notebooks) {
157
+ console.log(chalk.red(' Could not fetch notebooks. Is Genesis running?'));
158
+ return;
159
+ }
160
+ const notebooks = data.notebooks;
161
+ if (notebooks.length === 0) {
162
+ console.log(chalk.dim(' No notebooks found. Create one with /nb plan <prompt>'));
163
+ return;
164
+ }
165
+ console.log(chalk.bold(`\n Agent Notebooks (${notebooks.length})\n`));
166
+ const rows = notebooks.slice(0, 30).map((nb) => {
167
+ const status = nb.status === 'completed' ? chalk.green('\u2713')
168
+ : nb.status === 'running' ? chalk.yellow('\u25CF')
169
+ : nb.status === 'failed' ? chalk.red('\u2717')
170
+ : chalk.dim('\u25CB');
171
+ const cells = nb.cell_count ?? nb.cells ?? '?';
172
+ const tags = (nb.tags || []).slice(0, 3).map((t) => chalk.dim(`#${t}`)).join(' ');
173
+ return [
174
+ ` ${status}`,
175
+ chalk.cyan(nb.id || ''),
176
+ nb.name?.slice(0, 40) || chalk.dim('(untitled)'),
177
+ String(cells),
178
+ tags,
179
+ ];
180
+ });
181
+ console.log(formatTable([' ', 'ID', 'Name', 'Cells', 'Tags'], rows));
182
+ console.log();
183
+ }
184
+ async function nbOpen(client, notebookId) {
185
+ if (!notebookId) {
186
+ console.log(chalk.red(' Usage: /nb open <notebook_id>'));
187
+ return;
188
+ }
189
+ // Start interactive session
190
+ const spinner = ora('Starting notebook session...').start();
191
+ const session = await client.post(`/notebooks/${notebookId}/sessions`);
192
+ spinner.stop();
193
+ if (!session || !session.session_id) {
194
+ console.log(chalk.red(` Failed to start session for ${notebookId}`));
195
+ return;
196
+ }
197
+ const sessionId = session.session_id;
198
+ const cells = (session.cells || []).map((c) => ({
199
+ id: c.id || c.cell_id,
200
+ type: c.type,
201
+ name: c.name || c.type,
202
+ config: c.config || {},
203
+ status: c.status || 'pending',
204
+ }));
205
+ const width = Math.min(process.stdout.columns || 100, 120);
206
+ // Header
207
+ console.log();
208
+ console.log(chalk.bold.cyan(` \u2584\u2584 Agent Notebook Session`));
209
+ console.log(chalk.dim(` Session: ${sessionId}`));
210
+ console.log(chalk.dim(` Notebook: ${notebookId}`));
211
+ console.log(chalk.dim(` Cells: ${cells.length}`));
212
+ console.log();
213
+ // Render all cells
214
+ cells.forEach((cell, i) => {
215
+ console.log(renderCell(cell, i, cells.length, width));
216
+ console.log();
217
+ });
218
+ // Interactive loop
219
+ console.log(chalk.bold(' Interactive Session Controls:'));
220
+ console.log(chalk.dim(' enter \u2500 Execute next pending cell'));
221
+ console.log(chalk.dim(' r <n> \u2500 Run cell #n'));
222
+ console.log(chalk.dim(' run-all \u2500 Execute all remaining cells'));
223
+ console.log(chalk.dim(' vars \u2500 Show session variables'));
224
+ console.log(chalk.dim(' approve \u2500 Approve current checkpoint'));
225
+ console.log(chalk.dim(' reject \u2500 Reject current checkpoint'));
226
+ console.log(chalk.dim(' save \u2500 Save session state'));
227
+ console.log(chalk.dim(' q \u2500 End session'));
228
+ console.log();
229
+ let currentCell = 0;
230
+ const rl = createInterface({
231
+ input: process.stdin,
232
+ output: process.stdout,
233
+ prompt: chalk.cyan(' nb> '),
234
+ });
235
+ rl.prompt();
236
+ for await (const line of rl) {
237
+ const input = line.trim().toLowerCase();
238
+ if (input === 'q' || input === 'quit' || input === 'exit') {
239
+ const spinner = ora('Ending session...').start();
240
+ await client.post(`/notebooks/sessions/${sessionId}/interrupt`);
241
+ spinner.stop();
242
+ console.log(chalk.dim(' Session ended.'));
243
+ rl.close();
244
+ return;
245
+ }
246
+ if (input === '' || input === 'enter') {
247
+ // Execute next pending cell
248
+ const pending = cells.find(c => c.status === 'pending');
249
+ if (!pending) {
250
+ console.log(chalk.dim(' All cells executed.'));
251
+ rl.prompt();
252
+ continue;
253
+ }
254
+ const cellId = pending.id;
255
+ console.log(chalk.yellow(` Executing: ${pending.name || pending.type}...`));
256
+ const execSpinner = ora(' Running cell...').start();
257
+ const result = await client.post(`/notebooks/sessions/${sessionId}/execute/${cellId}`);
258
+ execSpinner.stop();
259
+ if (result) {
260
+ pending.status = result.status || 'completed';
261
+ pending.output = result.output || result.content || result.result || '';
262
+ pending.tokens = result.tokens_used || result.tokens || 0;
263
+ pending.cost = result.cost || 0;
264
+ pending.duration_ms = result.duration_ms || 0;
265
+ console.log();
266
+ console.log(renderCell(pending, cells.indexOf(pending), cells.length, width));
267
+ console.log();
268
+ if (pending.status === 'completed') {
269
+ console.log(chalk.green(` \u2713 Cell completed`));
270
+ }
271
+ else if (pending.status === 'failed') {
272
+ console.log(chalk.red(` \u2717 Cell failed`));
273
+ }
274
+ else if (pending.status === 'waiting') {
275
+ console.log(chalk.yellow(` \u23F3 Checkpoint — type 'approve' or 'reject'`));
276
+ }
277
+ }
278
+ else {
279
+ console.log(chalk.red(' Cell execution failed'));
280
+ pending.status = 'failed';
281
+ }
282
+ currentCell++;
283
+ }
284
+ else if (input.startsWith('r ')) {
285
+ const num = parseInt(input.slice(2)) - 1;
286
+ if (isNaN(num) || num < 0 || num >= cells.length) {
287
+ console.log(chalk.red(` Invalid cell number. Range: 1-${cells.length}`));
288
+ rl.prompt();
289
+ continue;
290
+ }
291
+ const cell = cells[num];
292
+ console.log(chalk.yellow(` Executing: ${cell.name || cell.type}...`));
293
+ const execSpinner = ora(' Running cell...').start();
294
+ const result = await client.post(`/notebooks/sessions/${sessionId}/execute/${cell.id}`);
295
+ execSpinner.stop();
296
+ if (result) {
297
+ cell.status = result.status || 'completed';
298
+ cell.output = result.output || result.content || result.result || '';
299
+ cell.tokens = result.tokens_used || result.tokens || 0;
300
+ cell.cost = result.cost || 0;
301
+ cell.duration_ms = result.duration_ms || 0;
302
+ console.log();
303
+ console.log(renderCell(cell, num, cells.length, width));
304
+ console.log();
305
+ }
306
+ else {
307
+ console.log(chalk.red(' Cell execution failed'));
308
+ }
309
+ }
310
+ else if (input === 'run-all') {
311
+ console.log(chalk.yellow(' Running all remaining cells...'));
312
+ const runSpinner = ora(' Executing...').start();
313
+ const result = await client.post(`/notebooks/sessions/${sessionId}/run-all`);
314
+ runSpinner.stop();
315
+ if (result && result.cells) {
316
+ for (const cellResult of result.cells) {
317
+ const cell = cells.find(c => c.id === cellResult.cell_id);
318
+ if (cell) {
319
+ cell.status = cellResult.status || 'completed';
320
+ cell.output = cellResult.output || cellResult.content || '';
321
+ cell.tokens = cellResult.tokens_used || 0;
322
+ cell.cost = cellResult.cost || 0;
323
+ cell.duration_ms = cellResult.duration_ms || 0;
324
+ }
325
+ }
326
+ console.log();
327
+ cells.forEach((cell, i) => {
328
+ console.log(renderCell(cell, i, cells.length, width));
329
+ console.log();
330
+ });
331
+ const ok = cells.filter(c => c.status === 'completed').length;
332
+ const fail = cells.filter(c => c.status === 'failed').length;
333
+ console.log(chalk.bold(` Results: ${chalk.green(`${ok} passed`)} ${fail ? chalk.red(`${fail} failed`) : ''}`));
334
+ }
335
+ else {
336
+ console.log(chalk.red(' Run-all failed'));
337
+ }
338
+ }
339
+ else if (input === 'vars') {
340
+ const state = await client.get(`/notebooks/sessions/${sessionId}`);
341
+ if (state?.variables && Object.keys(state.variables).length > 0) {
342
+ console.log(chalk.bold('\n Session Variables:\n'));
343
+ for (const [key, val] of Object.entries(state.variables)) {
344
+ const valStr = typeof val === 'string' ? val.slice(0, 80) : JSON.stringify(val).slice(0, 80);
345
+ console.log(` ${chalk.cyan(key)} = ${chalk.dim(valStr)}`);
346
+ }
347
+ console.log();
348
+ }
349
+ else {
350
+ console.log(chalk.dim(' No variables set.'));
351
+ }
352
+ }
353
+ else if (input === 'approve' || input === 'reject') {
354
+ const waitingCell = cells.find(c => c.status === 'waiting' || c.type === 'checkpoint');
355
+ if (!waitingCell) {
356
+ console.log(chalk.dim(' No checkpoint waiting for approval.'));
357
+ rl.prompt();
358
+ continue;
359
+ }
360
+ const resolution = input === 'approve' ? 'approved' : 'rejected';
361
+ const result = await client.post(`/notebooks/sessions/${sessionId}/gate/${waitingCell.id}`, { resolution });
362
+ if (result) {
363
+ waitingCell.status = resolution === 'approved' ? 'completed' : 'skipped';
364
+ console.log(resolution === 'approved'
365
+ ? chalk.green(` \u2713 Checkpoint approved`)
366
+ : chalk.yellow(` \u2212 Checkpoint rejected`));
367
+ }
368
+ }
369
+ else if (input === 'save') {
370
+ const result = await client.post(`/notebooks/sessions/${sessionId}/save`);
371
+ console.log(result ? chalk.green(' \u2713 Session saved') : chalk.red(' Save failed'));
372
+ }
373
+ else if (input === 'status') {
374
+ console.log();
375
+ cells.forEach((cell, i) => {
376
+ const badge = STATUS_BADGES[cell.status || 'pending'] || '';
377
+ const icon = CELL_ICONS[cell.type] || '\u25A1';
378
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${icon} ${cell.name || cell.type} ${badge}`);
379
+ });
380
+ console.log();
381
+ }
382
+ else {
383
+ console.log(chalk.dim(` Unknown command: ${input}. Type 'q' to quit.`));
384
+ }
385
+ rl.prompt();
386
+ }
387
+ rl.close();
388
+ }
389
+ async function nbRun(client, notebookId) {
390
+ if (!notebookId) {
391
+ console.log(chalk.red(' Usage: /nb run <notebook_id>'));
392
+ return;
393
+ }
394
+ console.log(chalk.bold(`\n Running notebook ${chalk.cyan(notebookId)}...\n`));
395
+ const spinner = ora(' Executing notebook...').start();
396
+ const result = await client.post(`/notebooks/${notebookId}/execute`);
397
+ spinner.stop();
398
+ if (!result) {
399
+ console.log(chalk.red(' Execution failed. Check Genesis logs.'));
400
+ return;
401
+ }
402
+ const width = Math.min(process.stdout.columns || 100, 120);
403
+ // Show results
404
+ if (result.cells) {
405
+ result.cells.forEach((cell, i) => {
406
+ const cellInfo = {
407
+ id: cell.cell_id || cell.id,
408
+ type: cell.type || 'unknown',
409
+ name: cell.name || cell.type,
410
+ config: cell.config || {},
411
+ status: cell.status || 'completed',
412
+ output: cell.output || cell.content || '',
413
+ tokens: cell.tokens_used || 0,
414
+ cost: cell.cost || 0,
415
+ duration_ms: cell.duration_ms || 0,
416
+ };
417
+ console.log(renderCell(cellInfo, i, result.cells.length, width));
418
+ console.log();
419
+ });
420
+ }
421
+ // Summary
422
+ const status = result.status || 'unknown';
423
+ const totalTokens = result.total_tokens || 0;
424
+ const totalCost = result.total_cost || 0;
425
+ const duration = result.duration_ms || 0;
426
+ console.log(chalk.bold(' Summary'));
427
+ console.log(chalk.dim(' ' + '\u2500'.repeat(40)));
428
+ console.log(` Status: ${status === 'completed' ? chalk.green('\u2713 completed') : chalk.red('\u2717 ' + status)}`);
429
+ console.log(` Tokens: ${totalTokens.toLocaleString()}`);
430
+ console.log(` Cost: $${totalCost.toFixed(4)}`);
431
+ console.log(` Duration: ${(duration / 1000).toFixed(1)}s`);
432
+ console.log();
433
+ }
434
+ async function nbPlan(client, prompt) {
435
+ if (!prompt) {
436
+ console.log(chalk.red(' Usage: /nb plan <task description>'));
437
+ return;
438
+ }
439
+ console.log(chalk.bold(`\n Planning notebook from prompt...\n`));
440
+ console.log(chalk.dim(` "${prompt.slice(0, 80)}${prompt.length > 80 ? '...' : ''}"`));
441
+ console.log();
442
+ const spinner = ora(' LLM is decomposing task into cells...').start();
443
+ const result = await client.post('/notebooks/plan', {
444
+ prompt,
445
+ agent: 'atlas',
446
+ effort: 7,
447
+ });
448
+ spinner.stop();
449
+ if (!result || !result.id) {
450
+ console.log(chalk.red(' Planning failed. Check Genesis logs.'));
451
+ return;
452
+ }
453
+ const width = Math.min(process.stdout.columns || 100, 120);
454
+ const nbId = result.id || result.metadata?.id;
455
+ const cells = result.cells || [];
456
+ console.log(chalk.green(` \u2713 Notebook created: ${chalk.cyan(nbId)}`));
457
+ console.log(chalk.dim(` Cells: ${cells.length}`));
458
+ console.log();
459
+ // Show planned cells
460
+ cells.forEach((cell, i) => {
461
+ const cellInfo = {
462
+ id: cell.id || `cell_${i}`,
463
+ type: cell.type,
464
+ name: cell.name || cell.type,
465
+ config: cell.config || {},
466
+ status: 'pending',
467
+ };
468
+ console.log(renderCell(cellInfo, i, cells.length, width));
469
+ console.log();
470
+ });
471
+ console.log(chalk.bold(' Next steps:'));
472
+ console.log(chalk.dim(` /nb open ${nbId} \u2500 Start interactive session`));
473
+ console.log(chalk.dim(` /nb run ${nbId} \u2500 Execute all cells`));
474
+ console.log();
475
+ }
476
+ async function nbCreate(client, name) {
477
+ if (!name) {
478
+ console.log(chalk.red(' Usage: /nb create <name>'));
479
+ return;
480
+ }
481
+ const spinner = ora('Creating notebook...').start();
482
+ const result = await client.post('/notebooks/', { name, cells: [] });
483
+ spinner.stop();
484
+ if (!result || !result.id) {
485
+ console.log(chalk.red(' Creation failed.'));
486
+ return;
487
+ }
488
+ console.log(chalk.green(` \u2713 Notebook created: ${chalk.cyan(result.id)}`));
489
+ console.log(chalk.dim(` /nb open ${result.id}`));
490
+ console.log();
491
+ }
492
+ async function nbTemplates(client) {
493
+ const spinner = ora('Loading templates...').start();
494
+ const data = await client.get('/notebooks/templates');
495
+ spinner.stop();
496
+ if (!data || !data.templates || data.templates.length === 0) {
497
+ console.log(chalk.dim(' No templates available.'));
498
+ return;
499
+ }
500
+ console.log(chalk.bold(`\n Notebook Templates (${data.templates.length})\n`));
501
+ const rows = data.templates.map((t) => [
502
+ ` ${chalk.cyan(t.id)}`,
503
+ t.name || chalk.dim('(untitled)'),
504
+ (t.tags || []).slice(0, 3).join(', ') || chalk.dim('-'),
505
+ ]);
506
+ console.log(formatTable([' ID', 'Name', 'Tags'], rows));
507
+ console.log();
508
+ }
509
+ async function nbSessions(client) {
510
+ const spinner = ora('Loading sessions...').start();
511
+ const data = await client.get('/notebooks/sessions');
512
+ spinner.stop();
513
+ if (!data || !data.sessions || data.sessions.length === 0) {
514
+ console.log(chalk.dim(' No active sessions.'));
515
+ return;
516
+ }
517
+ console.log(chalk.bold(`\n Active Sessions (${data.sessions.length})\n`));
518
+ const rows = data.sessions.map((s) => [
519
+ ` ${chalk.cyan(s.session_id)}`,
520
+ s.notebook_id || '',
521
+ String(s.executed_count ?? '?'),
522
+ String(s.total_cells ?? '?'),
523
+ s.status || chalk.dim('active'),
524
+ ]);
525
+ console.log(formatTable([' Session', 'Notebook', 'Executed', 'Total', 'Status'], rows));
526
+ console.log();
527
+ }
528
+ async function nbExport(client, args) {
529
+ const parts = args.trim().split(/\s+/);
530
+ const notebookId = parts[0];
531
+ const outputPath = parts[1] || `${notebookId}.ipynb`;
532
+ if (!notebookId) {
533
+ console.log(chalk.red(' Usage: /nb export <notebook_id> [output.ipynb]'));
534
+ return;
535
+ }
536
+ const spinner = ora(`Exporting ${notebookId} to .ipynb...`).start();
537
+ try {
538
+ const response = await fetch(`${client.baseUrl}/notebooks/${notebookId}/export`, { headers: { 'X-Caller-Type': 'PLATFORM' }, signal: AbortSignal.timeout(10000) });
539
+ if (!response.ok) {
540
+ spinner.stop();
541
+ console.log(chalk.red(` Export failed: ${response.status} ${response.statusText}`));
542
+ return;
543
+ }
544
+ const ipynb = await response.text();
545
+ // Write to file
546
+ const { writeFileSync } = await import('node:fs');
547
+ const { resolve: resolvePath } = await import('node:path');
548
+ const fullPath = resolvePath(outputPath);
549
+ writeFileSync(fullPath, ipynb, 'utf-8');
550
+ spinner.stop();
551
+ console.log(chalk.green(` \u2713 Exported to ${chalk.cyan(fullPath)}`));
552
+ console.log(chalk.dim(` Open in VS Code: code ${fullPath}`));
553
+ console.log(chalk.dim(` Open in Jupyter: jupyter notebook ${fullPath}`));
554
+ console.log();
555
+ }
556
+ catch (e) {
557
+ spinner.stop();
558
+ console.log(chalk.red(` Export failed: ${e.message || e}`));
559
+ }
560
+ }
561
+ async function nbInfo(client, notebookId) {
562
+ if (!notebookId) {
563
+ console.log(chalk.red(' Usage: /nb info <notebook_id>'));
564
+ return;
565
+ }
566
+ const spinner = ora('Loading notebook...').start();
567
+ const data = await client.get(`/notebooks/${notebookId}`);
568
+ spinner.stop();
569
+ if (!data) {
570
+ console.log(chalk.red(` Notebook ${notebookId} not found.`));
571
+ return;
572
+ }
573
+ const nb = data.notebook || data;
574
+ const meta = nb.metadata || nb;
575
+ const spec = nb.spec || {};
576
+ const cells = nb.cells || [];
577
+ const prov = nb.provenance || {};
578
+ const width = Math.min(process.stdout.columns || 100, 120);
579
+ console.log();
580
+ console.log(chalk.bold.cyan(` \u2584\u2584 ${meta.name || '(untitled)'}`));
581
+ console.log(chalk.dim(` ID: ${meta.id}`));
582
+ console.log(chalk.dim(` Created: ${meta.created_at || '?'} by ${meta.created_by || '?'}`));
583
+ if (meta.goal_id)
584
+ console.log(` Goal: ${chalk.yellow(meta.goal_id)}`);
585
+ if (meta.expedition_id)
586
+ console.log(` Expedition: ${chalk.magenta(meta.expedition_id)}`);
587
+ if (prov.expedition_id)
588
+ console.log(` Expedition: ${chalk.magenta(prov.expedition_id)}`);
589
+ if (meta.description)
590
+ console.log(chalk.dim(` ${meta.description.slice(0, 100)}`));
591
+ console.log(chalk.dim(` Mode: ${spec.execution_mode || 'sequential'} | Effort: ${spec.effort_budget || '?'}`));
592
+ console.log(chalk.dim(` Tags: ${(meta.tags || []).join(', ') || '-'}`));
593
+ console.log();
594
+ // Render cells
595
+ cells.forEach((cell, i) => {
596
+ const cellInfo = {
597
+ id: cell.id,
598
+ type: cell.type,
599
+ name: cell.name || cell.type,
600
+ config: cell.config || {},
601
+ status: cell.status?.execution_status || 'pending',
602
+ };
603
+ console.log(renderCell(cellInfo, i, cells.length, width));
604
+ console.log();
605
+ });
606
+ }
607
+ // ── Main dispatcher ──────────────────────────────────────────────────────────
608
+ export async function handleNotebook(client, args, _config) {
609
+ const parts = args.trim().split(/\s+/);
610
+ const sub = (parts[0] || '').toLowerCase();
611
+ const rest = parts.slice(1).join(' ');
612
+ switch (sub) {
613
+ case 'list':
614
+ case 'ls':
615
+ return nbList(client, rest);
616
+ case 'open':
617
+ case 'session':
618
+ return nbOpen(client, rest);
619
+ case 'run':
620
+ case 'exec':
621
+ return nbRun(client, rest);
622
+ case 'plan':
623
+ return nbPlan(client, rest);
624
+ case 'create':
625
+ case 'new':
626
+ return nbCreate(client, rest);
627
+ case 'info':
628
+ case 'show':
629
+ return nbInfo(client, rest);
630
+ case 'export':
631
+ case 'ipynb':
632
+ return nbExport(client, rest);
633
+ case 'templates':
634
+ case 'tpl':
635
+ return nbTemplates(client);
636
+ case 'sessions':
637
+ return nbSessions(client);
638
+ default: {
639
+ // /nb <number> — shortcut from the menu (e.g. /nb 1 = list)
640
+ const menuNum = parseInt(sub);
641
+ if (!isNaN(menuNum) && menuNum >= 1 && menuNum <= NB_SUBCOMMANDS.length) {
642
+ const picked = NB_SUBCOMMANDS[menuNum - 1];
643
+ if (!picked.args || rest) {
644
+ return handleNotebook(client, `${picked.value} ${rest}`.trim(), _config);
645
+ }
646
+ // Needs args but none given — show usage
647
+ console.log(chalk.yellow(` /nb ${picked.label} requires <${picked.args}>`));
648
+ return;
649
+ }
650
+ if (sub) {
651
+ // If they passed an ID directly, treat as /nb info <id>
652
+ if (sub.startsWith('nb_')) {
653
+ return nbInfo(client, sub);
654
+ }
655
+ console.log(chalk.red(` Unknown subcommand: ${sub}`));
656
+ }
657
+ // Show menu
658
+ return nbPicker(client, _config);
659
+ }
660
+ }
661
+ }
662
+ // ── Interactive subcommand picker ────────────────────────────────────────────
663
+ const NB_SUBCOMMANDS = [
664
+ { value: 'list', label: 'list', desc: 'List all notebooks', args: false },
665
+ { value: 'plan', label: 'plan', desc: 'Create notebook from natural language', args: 'prompt' },
666
+ { value: 'open', label: 'open', desc: 'Open interactive session (cell-by-cell)', args: 'id' },
667
+ { value: 'run', label: 'run', desc: 'Execute entire notebook', args: 'id' },
668
+ { value: 'create', label: 'create', desc: 'Create empty notebook', args: 'name' },
669
+ { value: 'info', label: 'info', desc: 'Show notebook details + cells', args: 'id' },
670
+ { value: 'export', label: 'export', desc: 'Export as .ipynb (VS Code / Jupyter)', args: 'id' },
671
+ { value: 'templates', label: 'templates', desc: 'List available templates', args: false },
672
+ { value: 'sessions', label: 'sessions', desc: 'List active kernel sessions', args: false },
673
+ ];
674
+ async function nbPicker(_client, _config) {
675
+ console.log(chalk.bold('\n /nb \u2014 Agent Notebooks\n'));
676
+ NB_SUBCOMMANDS.forEach((cmd, i) => {
677
+ const num = chalk.dim(` ${(i + 1).toString().padStart(2)}.`);
678
+ const name = chalk.cyan(cmd.label.padEnd(12));
679
+ const argHint = cmd.args ? chalk.dim(` <${cmd.args}>`) : '';
680
+ console.log(`${num} ${name}${argHint} ${chalk.dim(cmd.desc)}`);
681
+ });
682
+ console.log();
683
+ console.log(chalk.dim(' Type: /nb <command> [args] e.g. /nb list, /nb plan "fix auth"'));
684
+ console.log();
685
+ }