jbai-cli 1.9.2 → 2.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,665 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+ const { ensureToken } = require('../lib/ensure-token');
6
+ const { PROXY_PORT, ensureProxy } = require('../lib/proxy');
7
+
8
+ const SESSION_NAME = 'jbai-council';
9
+
10
+ const AGENTS = [
11
+ { name: 'claude', command: 'jbai-claude', extraArgs: ['--allow-dangerously-skip-permissions'] },
12
+ { name: 'codex', command: 'jbai-codex' },
13
+ { name: 'opencode', command: 'jbai-opencode' },
14
+ ];
15
+
16
+ // Catppuccin Mocha color palette (truecolor ANSI escape sequences)
17
+ const C = {
18
+ blue: '\x1b[38;2;137;180;250m', // #89b4fa — primary
19
+ green: '\x1b[38;2;166;227;161m', // #a6e3a1 — success
20
+ red: '\x1b[38;2;243;139;168m', // #f38ba8 — error
21
+ yellow: '\x1b[38;2;249;226;175m', // #f9e2af — warning
22
+ mauve: '\x1b[38;2;203;166;247m', // #cba6f7 — claude
23
+ teal: '\x1b[38;2;148;226;213m', // #94e2d5 — opencode
24
+ peach: '\x1b[38;2;250;179;135m', // #fab387 — codex
25
+ text: '\x1b[38;2;205;214;244m', // #cdd6f4 — default text
26
+ muted: '\x1b[38;2;127;132;156m', // #7f849c — dim text
27
+ surface: '\x1b[38;2;69;71;90m', // #45475a — borders
28
+ bold: '\x1b[1m',
29
+ dim: '\x1b[2m',
30
+ reset: '\x1b[0m',
31
+ };
32
+
33
+ // Agent display colors
34
+ const AGENT_COLORS = {
35
+ claude: C.mauve,
36
+ codex: C.peach,
37
+ opencode: C.teal,
38
+ };
39
+
40
+ function tmux(args, options = {}) {
41
+ const result = spawnSync('tmux', args, { encoding: 'utf-8', ...options });
42
+ if (result.error) {
43
+ throw result.error;
44
+ }
45
+ if (result.status !== 0) {
46
+ const stderr = (result.stderr || '').toString().trim();
47
+ const stdout = (result.stdout || '').toString().trim();
48
+ const message = stderr || stdout || `tmux ${args.join(' ')} failed`;
49
+ const err = new Error(message);
50
+ err.code = result.status;
51
+ throw err;
52
+ }
53
+ return (result.stdout || '').toString().trim();
54
+ }
55
+
56
+ function tmuxSafe(args, options = {}) {
57
+ try {
58
+ return tmux(args, options);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function isTmuxInstalled() {
65
+ const result = spawnSync('tmux', ['-V'], { stdio: 'pipe' });
66
+ return result.status === 0;
67
+ }
68
+
69
+ function isSessionAlive() {
70
+ return tmuxSafe(['has-session', '-t', SESSION_NAME]) !== null;
71
+ }
72
+
73
+ function killExistingSession() {
74
+ tmuxSafe(['kill-session', '-t', SESSION_NAME]);
75
+ }
76
+
77
+ function getWindowId() {
78
+ const output = tmux(['list-windows', '-t', SESSION_NAME, '-F', '#{window_id}']);
79
+ const first = output.split('\n').find(Boolean);
80
+ if (!first) {
81
+ throw new Error('No windows found for council session.');
82
+ }
83
+ return first.trim();
84
+ }
85
+
86
+ function listPanes(windowId) {
87
+ const format = '#{pane_id}\t#{pane_left}\t#{pane_top}\t#{pane_width}\t#{pane_height}';
88
+ const output = tmux(['list-panes', '-t', windowId, '-F', format]);
89
+ return output
90
+ .split('\n')
91
+ .filter(Boolean)
92
+ .map((line) => {
93
+ const [id, left, top, width, height] = line.split('\t');
94
+ return {
95
+ id,
96
+ left: Number(left),
97
+ top: Number(top),
98
+ width: Number(width),
99
+ height: Number(height),
100
+ };
101
+ });
102
+ }
103
+
104
+ function getPaneMap() {
105
+ const windowId = getWindowId();
106
+ const panes = listPanes(windowId);
107
+ if (panes.length < 4) {
108
+ throw new Error(`Expected 4 panes, found ${panes.length}`);
109
+ }
110
+
111
+ const topRowTop = Math.min(...panes.map(p => p.top));
112
+ const topPanes = panes.filter(p => p.top === topRowTop).sort((a, b) => a.left - b.left);
113
+ const bottomPanes = panes
114
+ .filter(p => p.top !== topRowTop)
115
+ .sort((a, b) => b.top - a.top || a.left - b.left);
116
+
117
+ if (topPanes.length < 3) {
118
+ throw new Error(`Expected 3 top panes, found ${topPanes.length}`);
119
+ }
120
+
121
+ return {
122
+ windowId,
123
+ orchestratorPane: bottomPanes[0] ? bottomPanes[0].id : null,
124
+ paneMap: {
125
+ claude: topPanes[0].id,
126
+ codex: topPanes[1].id,
127
+ opencode: topPanes[2].id,
128
+ },
129
+ };
130
+ }
131
+
132
+ function sendKeys(paneId, keys) {
133
+ tmux(['send-keys', '-t', paneId, ...keys]);
134
+ }
135
+
136
+ function sendKeysLiteral(paneId, text) {
137
+ tmux(['send-keys', '-t', paneId, '-l', text]);
138
+ }
139
+
140
+ // POSIX-safe shell argument escaping. Wraps in single quotes and escapes
141
+ // embedded single quotes with the '\'' pattern. Safe for arbitrary input
142
+ // including paths with spaces, semicolons, backticks, etc.
143
+ function shellEscapeArg(value) {
144
+ if (value === '') return "''";
145
+ if (/^[A-Za-z0-9_\/.:=-]+$/.test(value)) return value;
146
+ return `'${value.replace(/'/g, "'\\''")}'`;
147
+ }
148
+
149
+ function buildAgentCommand(scriptPath, superMode, extraArgs = []) {
150
+ const args = [process.execPath, scriptPath];
151
+ if (superMode) args.push('--super');
152
+ args.push(...extraArgs);
153
+ return args.map(shellEscapeArg).join(' ');
154
+ }
155
+
156
+ function createSession(superMode) {
157
+ const cwd = process.cwd();
158
+
159
+ tmux(['new-session', '-d', '-s', SESSION_NAME, '-x', '200', '-y', '50']);
160
+
161
+ // Enable extended-keys so Shift+Enter passthrough works (kitty protocol)
162
+ tmuxSafe(['set-option', '-t', SESSION_NAME, 'extended-keys', 'on']);
163
+
164
+ const windowId = getWindowId();
165
+
166
+ // Split FULL-WIDTH bottom pane for orchestrator FIRST, before columnar splits.
167
+ // This ensures the orchestrator spans the entire terminal width.
168
+ tmux(['split-window', '-v', '-t', windowId, '-l', '10']);
169
+
170
+ // Now split the TOP pane into 3 vertical columns.
171
+ // After the vertical split: top pane is still selected, bottom is orchestrator.
172
+ // We need to target the top pane specifically.
173
+ const panesAfterV = listPanes(windowId);
174
+ const topPane = panesAfterV.reduce((a, b) => (a.top < b.top ? a : b));
175
+ tmux(['split-window', '-h', '-t', topPane.id, '-p', '66']);
176
+ // Now the top area has 2 panes: left (34%) and right (66%). Split right in half.
177
+ const panesAfterH1 = listPanes(windowId);
178
+ const topPanes = panesAfterH1
179
+ .filter(p => p.top === topPane.top)
180
+ .sort((a, b) => a.left - b.left);
181
+ const rightPane = topPanes[topPanes.length - 1];
182
+ tmux(['split-window', '-h', '-t', rightPane.id, '-p', '50']);
183
+
184
+ const { orchestratorPane, paneMap } = getPaneMap();
185
+ if (!orchestratorPane) {
186
+ throw new Error('Unable to find orchestrator pane after layout.');
187
+ }
188
+
189
+ const binDir = __dirname;
190
+ const cwdEscaped = shellEscapeArg(cwd);
191
+
192
+ for (const agent of AGENTS) {
193
+ const paneId = paneMap[agent.name];
194
+ const scriptPath = path.join(binDir, `${agent.command}.js`);
195
+ const cmd = `cd ${cwdEscaped} && ${buildAgentCommand(scriptPath, superMode, agent.extraArgs || [])}`;
196
+ sendKeysLiteral(paneId, cmd);
197
+ sendKeys(paneId, ['Enter']);
198
+ }
199
+
200
+ const orchScript = path.join(binDir, 'jbai-council.js');
201
+ const orchCmd = `cd ${cwdEscaped} && ${buildAgentCommand(orchScript, false, ['--_orchestrator'])}`;
202
+ sendKeysLiteral(orchestratorPane, orchCmd);
203
+ sendKeys(orchestratorPane, ['Enter']);
204
+
205
+ tmux(['select-pane', '-t', orchestratorPane]);
206
+
207
+ return { orchestratorPane, paneMap };
208
+ }
209
+
210
+ function runOrchestrator() {
211
+ let paneMap;
212
+ try {
213
+ paneMap = getPaneMap().paneMap;
214
+ } catch (err) {
215
+ console.error(`Failed to detect panes: ${err.message}`);
216
+ process.exit(1);
217
+ }
218
+
219
+ const agentNames = Object.keys(paneMap);
220
+
221
+ const agentList = agentNames
222
+ .map(n => `${AGENT_COLORS[n] || C.text}${n}${C.reset}`)
223
+ .join(`${C.muted}, ${C.reset}`);
224
+
225
+ console.log('');
226
+ console.log(`${C.blue}${C.bold} council${C.reset}${C.muted} — multi-agent orchestrator${C.reset}`);
227
+ console.log(`${C.surface} ${'─'.repeat(44)}${C.reset}`);
228
+ console.log(`${C.muted} agents: ${C.reset}${agentList}`);
229
+ console.log(`${C.muted} broadcast: ${C.text}type normally${C.muted} target: ${C.text}@agent <msg>${C.reset}`);
230
+ console.log(`${C.muted} multiline: ${C.text}Alt+Enter${C.muted} or ${C.text}Shift+Enter${C.muted} for new line${C.reset}`);
231
+ console.log(`${C.muted} /focus /status /quit /help${C.reset}`);
232
+ console.log('');
233
+
234
+ // --- Raw stdin multiline input handler ---
235
+ const lines = [''];
236
+ let currentLine = 0;
237
+ let renderedRowCount = 0;
238
+
239
+ const promptPrefix = `${C.blue}${C.bold}council${C.reset}${C.muted}>${C.reset} `;
240
+ const continuationPrefix = `${C.muted} │${C.reset} `;
241
+
242
+ const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
243
+
244
+ function stripAnsi(text) {
245
+ return text.replace(ANSI_PATTERN, '');
246
+ }
247
+
248
+ function visibleWidth(text, startColumn = 0) {
249
+ const clean = stripAnsi(text);
250
+ let col = startColumn;
251
+ for (const ch of clean) {
252
+ if (ch === '\t') {
253
+ const nextTab = 8 - (col % 8);
254
+ col += nextTab;
255
+ } else if (ch === '\r' || ch === '\n') {
256
+ col = 0;
257
+ } else if (ch >= '\x00' && ch <= '\x1f') {
258
+ // Control chars: ignore for width calculation
259
+ } else {
260
+ col += 1;
261
+ }
262
+ }
263
+ return col - startColumn;
264
+ }
265
+
266
+ function rowsForLine(prefixLen, lineText, cols) {
267
+ const contentWidth = visibleWidth(lineText, prefixLen);
268
+ const total = prefixLen + contentWidth;
269
+ const width = Math.max(cols, 1);
270
+ return Math.max(1, Math.ceil(total / width));
271
+ }
272
+
273
+ function renderPrompt() {
274
+ const cols = process.stdout.columns || 80;
275
+
276
+ // Move cursor up to clear previous render
277
+ if (renderedRowCount > 1) {
278
+ process.stdout.write(`\x1b[${renderedRowCount - 1}A`);
279
+ }
280
+ // Go to start of line, clear everything below
281
+ process.stdout.write('\r\x1b[J');
282
+
283
+ renderedRowCount = 0;
284
+ for (let i = 0; i < lines.length; i++) {
285
+ if (i > 0) process.stdout.write('\n');
286
+ const prefix = i === 0 ? promptPrefix : continuationPrefix;
287
+ process.stdout.write(`${prefix}${lines[i]}`);
288
+ const prefixLen = visibleWidth(prefix);
289
+ renderedRowCount += rowsForLine(prefixLen, lines[i], cols);
290
+ }
291
+ }
292
+
293
+ function resetInput() {
294
+ lines.length = 0;
295
+ lines.push('');
296
+ currentLine = 0;
297
+ renderedRowCount = 0;
298
+ }
299
+
300
+ function insertNewline() {
301
+ lines.splice(currentLine + 1, 0, '');
302
+ currentLine++;
303
+ renderPrompt();
304
+ }
305
+
306
+ function backspace() {
307
+ if (lines[currentLine].length > 0) {
308
+ lines[currentLine] = lines[currentLine].slice(0, -1);
309
+ } else if (currentLine > 0) {
310
+ lines.splice(currentLine, 1);
311
+ currentLine--;
312
+ }
313
+ renderPrompt();
314
+ }
315
+
316
+ function submit() {
317
+ const text = lines.join('\n');
318
+ const hasContent = text.trim().length > 0;
319
+ process.stdout.write('\n');
320
+ resetInput();
321
+
322
+ if (!hasContent) {
323
+ renderPrompt();
324
+ return;
325
+ }
326
+
327
+ if (text.startsWith('/')) {
328
+ handleCommand(text, paneMap);
329
+ renderPrompt();
330
+ return;
331
+ }
332
+
333
+ const atMatch = text.match(/^@(\w+)\s+([\s\S]+)$/);
334
+ if (atMatch) {
335
+ const target = atMatch[1].toLowerCase();
336
+ const message = atMatch[2];
337
+ if (paneMap[target]) {
338
+ sendToPane(paneMap[target], message, target);
339
+ const color = AGENT_COLORS[target] || C.text;
340
+ console.log(`${C.dim} → ${color}${target}${C.reset}`);
341
+ } else {
342
+ console.log(`${C.red} unknown agent: ${C.text}${target}${C.muted} — use ${agentNames.join(', ')}${C.reset}`);
343
+ }
344
+ renderPrompt();
345
+ return;
346
+ }
347
+
348
+ for (const agent of agentNames) {
349
+ sendToPane(paneMap[agent], text, agent);
350
+ }
351
+ console.log(`${C.dim} → ${C.blue}all agents${C.reset}`);
352
+ renderPrompt();
353
+ }
354
+
355
+ // Enable bracketed paste mode for reliable paste detection
356
+ process.stdout.write('\x1b[?2004h');
357
+
358
+ if (process.stdin.isTTY) {
359
+ process.stdin.setRawMode(true);
360
+ }
361
+ process.stdin.resume();
362
+ if (process.stdout.isTTY) {
363
+ process.stdout.on('resize', renderPrompt);
364
+ }
365
+
366
+ const PASTE_START = '\x1b[200~';
367
+ const PASTE_END = '\x1b[201~';
368
+
369
+ let inPaste = false;
370
+ let pasteBuffer = '';
371
+ let pending = '';
372
+ let escapeBuffer = '';
373
+ let ignoreNextLF = false;
374
+
375
+ function longestPartialSuffix(buffer, sequence) {
376
+ const max = Math.min(sequence.length - 1, buffer.length);
377
+ for (let len = max; len > 0; len--) {
378
+ if (sequence.startsWith(buffer.slice(-len))) {
379
+ return len;
380
+ }
381
+ }
382
+ return 0;
383
+ }
384
+
385
+ function handleEscapeSequence() {
386
+ if (escapeBuffer === '\x1b\r' || escapeBuffer === '\x1b\n') {
387
+ insertNewline();
388
+ return true;
389
+ }
390
+ if (escapeBuffer === '\x1b[13;2u') {
391
+ insertNewline();
392
+ return true;
393
+ }
394
+ if (/^\x1b\[[0-9;]*[A-Za-z~]$/.test(escapeBuffer)) {
395
+ return true;
396
+ }
397
+ if (escapeBuffer.length > 8) {
398
+ return true;
399
+ }
400
+ return false;
401
+ }
402
+
403
+ function handleInput(text) {
404
+ if (!text) return;
405
+ for (let i = 0; i < text.length; i++) {
406
+ const ch = text[i];
407
+
408
+ if (escapeBuffer) {
409
+ escapeBuffer += ch;
410
+ if (handleEscapeSequence()) {
411
+ escapeBuffer = '';
412
+ }
413
+ continue;
414
+ }
415
+
416
+ if (ch === '\x1b') {
417
+ escapeBuffer = '\x1b';
418
+ continue;
419
+ }
420
+
421
+ if (ignoreNextLF && ch !== '\n') {
422
+ ignoreNextLF = false;
423
+ }
424
+
425
+ if (ch === '\r') {
426
+ submit();
427
+ ignoreNextLF = true;
428
+ } else if (ch === '\n') {
429
+ if (ignoreNextLF) {
430
+ ignoreNextLF = false;
431
+ } else {
432
+ submit();
433
+ }
434
+ } else if (ch === '\x7f' || ch === '\b') {
435
+ backspace();
436
+ } else if (ch === '\x03') {
437
+ // Ctrl+C → exit
438
+ process.stdout.write('\x1b[?2004l\n'); // disable bracketed paste
439
+ process.exit(0);
440
+ } else if (ch === '\x04') {
441
+ // Ctrl+D → exit if empty
442
+ if (lines.join('').length === 0) {
443
+ process.stdout.write('\x1b[?2004l\n');
444
+ process.exit(0);
445
+ }
446
+ } else {
447
+ // Printable characters
448
+ lines[currentLine] += ch;
449
+ renderPrompt();
450
+ }
451
+ }
452
+ }
453
+
454
+ function processChunk(chunk) {
455
+ pending += chunk;
456
+
457
+ while (pending.length > 0) {
458
+ if (inPaste) {
459
+ const endIndex = pending.indexOf(PASTE_END);
460
+ if (endIndex === -1) {
461
+ const keep = longestPartialSuffix(pending, PASTE_END);
462
+ pasteBuffer += pending.slice(0, pending.length - keep);
463
+ pending = pending.slice(pending.length - keep);
464
+ return;
465
+ }
466
+ pasteBuffer += pending.slice(0, endIndex);
467
+ pending = pending.slice(endIndex + PASTE_END.length);
468
+ inPaste = false;
469
+ handlePaste(pasteBuffer);
470
+ pasteBuffer = '';
471
+ continue;
472
+ }
473
+
474
+ const startIndex = pending.indexOf(PASTE_START);
475
+ if (startIndex === -1) {
476
+ const keep = longestPartialSuffix(pending, PASTE_START);
477
+ handleInput(pending.slice(0, pending.length - keep));
478
+ pending = pending.slice(pending.length - keep);
479
+ return;
480
+ }
481
+
482
+ handleInput(pending.slice(0, startIndex));
483
+ pending = pending.slice(startIndex + PASTE_START.length);
484
+ inPaste = true;
485
+ }
486
+ }
487
+
488
+ process.stdin.on('data', (data) => {
489
+ const chunk = data.toString('utf-8');
490
+ processChunk(chunk);
491
+ });
492
+
493
+ function handlePaste(text) {
494
+ const pasteLines = text.split(/\r\n|\r|\n/);
495
+ lines[currentLine] += pasteLines[0];
496
+ for (let i = 1; i < pasteLines.length; i++) {
497
+ lines.push(pasteLines[i]);
498
+ currentLine++;
499
+ }
500
+ renderPrompt();
501
+ }
502
+
503
+ renderPrompt();
504
+ }
505
+
506
+ function sendToPane(paneId, text, agentName) {
507
+ try {
508
+ if (agentName === 'codex') {
509
+ // Codex sometimes ignores paste+Enter. Send as typed input, then Enter slightly later.
510
+ tmux(['send-keys', '-t', paneId, '-l', text]);
511
+ setTimeout(() => {
512
+ try {
513
+ sendKeys(paneId, ['Enter']);
514
+ } catch {
515
+ // Pane may be gone
516
+ }
517
+ }, 150);
518
+ return;
519
+ }
520
+
521
+ // Use tmux set-buffer + paste-buffer for reliable delivery.
522
+ // send-keys -l delivers chars individually which some TUI frameworks
523
+ // may not process atomically before the Enter arrives.
524
+ // paste-buffer delivers the full text as a single paste event.
525
+ const payload = text.endsWith('\n') ? text : `${text}\n`;
526
+ tmux(['set-buffer', '-b', 'council-input', '--', payload]);
527
+ tmux(['paste-buffer', '-b', 'council-input', '-t', paneId, '-d']);
528
+ } catch {
529
+ // Pane may be gone
530
+ }
531
+ }
532
+
533
+ function handleCommand(input, paneMap) {
534
+ const parts = input.split(/\s+/);
535
+ const cmd = parts[0].toLowerCase();
536
+
537
+ switch (cmd) {
538
+ case '/focus': {
539
+ const target = (parts[1] || '').toLowerCase();
540
+ if (paneMap[target]) {
541
+ const color = AGENT_COLORS[target] || C.text;
542
+ console.log(`${C.dim} → focusing ${color}${target}${C.reset}${C.muted} (Ctrl+B ↑ to return)${C.reset}`);
543
+ try {
544
+ tmux(['select-pane', '-t', paneMap[target]]);
545
+ } catch {
546
+ console.log(`${C.red} failed to focus pane${C.reset}`);
547
+ }
548
+ } else {
549
+ console.log(`${C.muted} usage: ${C.text}/focus ${C.blue}<claude|codex|opencode>${C.reset}`);
550
+ }
551
+ break;
552
+ }
553
+ case '/status': {
554
+ for (const [name, paneId] of Object.entries(paneMap)) {
555
+ const color = AGENT_COLORS[name] || C.text;
556
+ try {
557
+ const pid = tmux(['display-message', '-p', '-t', paneId, '#{pane_pid}']);
558
+ try {
559
+ process.kill(Number(pid), 0);
560
+ console.log(` ${C.green}●${C.reset} ${color}${name}${C.muted} pid ${pid}${C.reset}`);
561
+ } catch {
562
+ console.log(` ${C.red}●${C.reset} ${color}${name}${C.muted} stopped${C.reset}`);
563
+ }
564
+ } catch {
565
+ console.log(` ${C.red}●${C.reset} ${color}${name}${C.muted} pane not found${C.reset}`);
566
+ }
567
+ }
568
+ break;
569
+ }
570
+ case '/help': {
571
+ console.log(`${C.muted} broadcast: ${C.text}type normally`);
572
+ console.log(`${C.muted} target: ${C.text}@claude ${C.muted}/ ${C.text}@codex ${C.muted}/ ${C.text}@opencode ${C.muted}<msg>`);
573
+ console.log(`${C.muted} multiline: ${C.text}Alt+Enter ${C.muted}or ${C.text}Shift+Enter ${C.muted}for new line`);
574
+ console.log(`${C.muted} /focus ${C.text}switch to agent pane`);
575
+ console.log(`${C.muted} /status ${C.text}show agent status`);
576
+ console.log(`${C.muted} /quit ${C.text}kill all and exit`);
577
+ console.log(`${C.muted} /help ${C.text}show this help${C.reset}`);
578
+ break;
579
+ }
580
+ case '/quit':
581
+ case '/exit': {
582
+ console.log(`${C.muted} killing council session...${C.reset}`);
583
+ try {
584
+ tmux(['kill-session', '-t', SESSION_NAME]);
585
+ } catch {
586
+ // Session may already be dead
587
+ }
588
+ process.exit(0);
589
+ }
590
+ default:
591
+ console.log(`${C.red} unknown command: ${C.text}${cmd}${C.muted} — try /help${C.reset}`);
592
+ }
593
+ }
594
+
595
+ (async () => {
596
+ const args = process.argv.slice(2);
597
+
598
+ if (args.includes('--_orchestrator')) {
599
+ runOrchestrator();
600
+ return;
601
+ }
602
+
603
+ const superFlags = ['--super', '--yolo', '-s'];
604
+ const superMode = args.some(a => superFlags.includes(a));
605
+ const helpMode = args.some(a => ['--help', '-h'].includes(a));
606
+
607
+ if (helpMode) {
608
+ console.log(`jbai-council - Launch Claude + Codex + OpenCode in tmux council mode
609
+
610
+ Usage:
611
+ jbai-council Launch all 3 agents
612
+ jbai-council --super Launch all agents in super mode
613
+
614
+ Requires: tmux (brew install tmux)
615
+
616
+ Inside the council:
617
+ Type normally Broadcast to all agents
618
+ @claude <msg> Send only to Claude
619
+ @codex <msg> Send only to Codex
620
+ @opencode <msg> Send only to OpenCode
621
+ /focus <agent> Switch to agent pane (Ctrl+B arrows to return)
622
+ /status Show agent status
623
+ /quit or /exit Kill all agents and exit
624
+ `);
625
+ return;
626
+ }
627
+
628
+ if (!isTmuxInstalled()) {
629
+ console.error('tmux is not installed.');
630
+ console.error('Install with: brew install tmux');
631
+ process.exit(1);
632
+ }
633
+
634
+ const insideTmux = Boolean(process.env.TMUX);
635
+
636
+ if (insideTmux && isSessionAlive()) {
637
+ console.error('Council session already running. Switching to it...');
638
+ tmux(['switch-client', '-t', SESSION_NAME], { stdio: 'inherit' });
639
+ return;
640
+ }
641
+
642
+ if (isSessionAlive()) {
643
+ console.log('Existing council session found. Attaching...');
644
+ tmux(['attach', '-t', SESSION_NAME], { stdio: 'inherit' });
645
+ return;
646
+ }
647
+
648
+ await ensureToken();
649
+ await ensureProxy();
650
+
651
+ console.log(`${C.blue}${C.bold}council${C.reset}${C.muted} starting ${C.mauve}claude${C.muted} + ${C.peach}codex${C.muted} + ${C.teal}opencode${C.muted}...${C.reset}`);
652
+ createSession(superMode);
653
+
654
+ if (insideTmux) {
655
+ tmux(['switch-client', '-t', SESSION_NAME], { stdio: 'inherit' });
656
+ } else {
657
+ tmux(['attach', '-t', SESSION_NAME], { stdio: 'inherit' });
658
+ }
659
+ })().catch((err) => {
660
+ console.error(`Error: ${err.message}`);
661
+ if (isSessionAlive()) {
662
+ killExistingSession();
663
+ }
664
+ process.exit(1);
665
+ });
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ require('../lib/shortcut').run({
3
+ tool: 'gemini',
4
+ model: 'gemini-3.1-pro-preview',
5
+ label: 'Gemini + 3.1 Pro Preview',
6
+ });
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ require('../lib/shortcut').run({
3
+ tool: 'gemini',
4
+ model: 'supernova',
5
+ label: 'Gemini + Supernova (Google EAP)',
6
+ });