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.
- package/README.md +3 -4
- package/bin/jbai-claude-opus.js +6 -0
- package/bin/jbai-claude-sonnet.js +6 -0
- package/bin/jbai-claude.js +16 -9
- package/bin/jbai-codex-5.2.js +6 -0
- package/bin/jbai-codex-5.3.js +6 -0
- package/bin/jbai-codex-rockhopper.js +6 -0
- package/bin/jbai-codex.js +12 -39
- package/bin/jbai-continue.js +27 -43
- package/bin/jbai-council.js +665 -0
- package/bin/jbai-gemini-3.1.js +6 -0
- package/bin/jbai-gemini-supernova.js +6 -0
- package/bin/jbai-gemini.js +17 -6
- package/bin/jbai-goose.js +11 -39
- package/bin/jbai-opencode-deepseek.js +6 -0
- package/bin/jbai-opencode-grok.js +6 -0
- package/bin/jbai-opencode-rockhopper.js +6 -0
- package/bin/jbai-opencode.js +122 -20
- package/bin/jbai-proxy.js +1110 -66
- package/bin/jbai.js +99 -42
- package/bin/test-cli-tictactoe.js +279 -0
- package/bin/test-clients.js +38 -6
- package/bin/test-model-lists.js +100 -0
- package/lib/completions.js +258 -0
- package/lib/config.js +46 -8
- package/lib/model-list.js +117 -0
- package/lib/postinstall.js +3 -0
- package/lib/proxy.js +46 -0
- package/lib/shortcut.js +47 -0
- package/package.json +13 -2
|
@@ -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
|
+
});
|