@carjms/codexswitch 0.4.0 → 0.5.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.ko.md +2 -1
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +202 -51
package/README.ko.md
CHANGED
|
@@ -205,7 +205,8 @@ cxs list # 계정별 5h/week 사용량 % 확인
|
|
|
205
205
|
| `cxs add-key <이름> [키]` | OpenAI API 키 계정 등록 (키 생략 시 `$OPENAI_API_KEY` 사용) |
|
|
206
206
|
| `cxs list` | 계정 목록: 활성 표시, 이메일, 플랜, 우선순위, 한도 상태, 사용량 % |
|
|
207
207
|
| `cxs usage [이름]` | 계정별 사용량 대시보드: 5시간/주간 게이지 바, 리셋 카운트다운, 다음 로테이션 계정 (별칭: `status`) |
|
|
208
|
-
| `cxs
|
|
208
|
+
| `cxs chat` | **대화형 입력창 (Claude Code 스타일)**: 매 턴이 로테이션을 거치고 같은 codex 세션을 이어가므로, 계정이 바뀌어도 대화가 유지됨. 내부 명령: `/usage /use /next /model /new /quit` |
|
|
209
|
+
| `cxs watch` | 실시간 인터랙티브 대시보드 — 5초마다 갱신; 키: `↑/↓` 선택, `s` 전환, `e` 활성/비활성, `p` 프로브, `q` 종료 |
|
|
209
210
|
| `cxs probe [이름]` | 계정마다 최소 요청 1회를 보내 사용량 게이지 워밍업 (토큰 소량 소모) |
|
|
210
211
|
| `cxs log [개수]` | 최근 활동 기록: 계정 전환, 한도 도달, 로테이션, 프로브 |
|
|
211
212
|
| `cxs use <이름>` | 활성 계정 전환 |
|
package/README.md
CHANGED
|
@@ -207,7 +207,8 @@ cxs list # per-account 5h/week usage columns
|
|
|
207
207
|
| `cxs add-key <name> [key]` | Register an OpenAI API-key account (falls back to `$OPENAI_API_KEY`) |
|
|
208
208
|
| `cxs list` | List accounts: active marker, email, plan, priority, limit status, usage % |
|
|
209
209
|
| `cxs usage [name]` | Per-account usage dashboard: 5h/weekly gauge bars, reset countdowns, next rotation pick (alias: `status`) |
|
|
210
|
-
| `cxs
|
|
210
|
+
| `cxs chat` | **Interactive prompt loop (Claude Code-style)**: each turn runs through rotation and resumes the same codex session, so the conversation survives account switches. Slash commands inside: `/usage /use /next /model /new /quit` |
|
|
211
|
+
| `cxs watch` | Live interactive dashboard — refreshes every 5s; keys: `↑/↓` select, `s` switch, `e` enable/disable, `p` probe, `q` quit |
|
|
211
212
|
| `cxs probe [name]` | Warm up the usage gauges with one minimal request per account (costs a few tokens) |
|
|
212
213
|
| `cxs log [count]` | Recent activity: account switches, limits hit, rotations, probes |
|
|
213
214
|
| `cxs use <name>` | Switch the active account |
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -34,6 +34,10 @@ Accounts
|
|
|
34
34
|
clear-limit <name> forget a recorded rate-limit for an account
|
|
35
35
|
|
|
36
36
|
Running codex
|
|
37
|
+
chat interactive prompt loop (Claude Code-style): each turn
|
|
38
|
+
runs through rotation and resumes the same session,
|
|
39
|
+
so the conversation survives account switches
|
|
40
|
+
(/usage /use /next /model /new /quit inside)
|
|
37
41
|
run [name] [args...] run codex as <name> (or active account) in an isolated
|
|
38
42
|
per-account CODEX_HOME; config/sessions are shared
|
|
39
43
|
exec [args...] run "codex exec ..." and auto-rotate through accounts in
|
|
@@ -360,47 +364,54 @@ function cmdModel(args) {
|
|
|
360
364
|
out(`default model set to "${meta.model}" (applied to run/exec)`);
|
|
361
365
|
}
|
|
362
366
|
|
|
363
|
-
//
|
|
364
|
-
// reset countdowns
|
|
365
|
-
function
|
|
366
|
-
const meta = store.loadMeta();
|
|
367
|
-
let accounts = store.listAccounts();
|
|
368
|
-
if (args[0]) {
|
|
369
|
-
accounts = accounts.filter((a) => a.name === args[0]);
|
|
370
|
-
if (accounts.length === 0) throw new Error(`no such account: ${args[0]}`);
|
|
371
|
-
}
|
|
372
|
-
if (accounts.length === 0) {
|
|
373
|
-
out('no accounts yet — add one with "codexswitch login" or "codexswitch import"');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
367
|
+
// Render the per-account usage view (gauge bars for the 5h / weekly windows
|
|
368
|
+
// with reset countdowns). `cursor` marks the selected row in watch mode.
|
|
369
|
+
function usageLines(accounts, meta, { cursor = -1 } = {}) {
|
|
376
370
|
const now = Date.now();
|
|
371
|
+
const lines = [];
|
|
377
372
|
const bar = (pct, threshold) => {
|
|
378
373
|
const width = 20;
|
|
379
374
|
const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)));
|
|
380
375
|
const paint = pct >= threshold ? ui.red : pct >= 70 ? ui.yellow : ui.green;
|
|
381
376
|
return `[${paint('█'.repeat(filled))}${ui.dim('░'.repeat(width - filled))}]`;
|
|
382
377
|
};
|
|
383
|
-
const
|
|
378
|
+
const gauge = (label, win, threshold) => {
|
|
384
379
|
if (!win || typeof win.pct !== 'number') return ` ${label} ${ui.dim('no data')}`;
|
|
385
380
|
const pct = Math.round(win.pct);
|
|
386
381
|
const reset = win.resetAt && win.resetAt > now ? ` resets in ${fmtRemaining(win.resetAt)}` : '';
|
|
387
382
|
const overMark = win.pct >= threshold ? ` ${ui.red(`≥ threshold ${threshold}%`)}` : '';
|
|
388
383
|
return ` ${label} ${bar(win.pct, threshold)} ${String(pct).padStart(3)}%${ui.dim(reset)}${overMark}`;
|
|
389
384
|
};
|
|
390
|
-
|
|
385
|
+
accounts.forEach((a, i) => {
|
|
386
|
+
const sel = i === cursor ? ui.cyan('▶') : ' ';
|
|
391
387
|
const mark = a.active ? ui.green('●') : ' ';
|
|
392
388
|
const state = a.disabled ? ui.red(' [disabled]') : a.limitedUntil && a.limitedUntil > now ? ui.yellow(` [limited ${fmtRemaining(a.limitedUntil)}]`) : '';
|
|
393
|
-
|
|
389
|
+
lines.push(`${cursor >= 0 ? sel : ''}${mark} ${ui.bold(a.name)}${ui.dim(` (${a.plan || a.email || '-'})`)}${state}`);
|
|
394
390
|
if (!a.usage) {
|
|
395
|
-
|
|
391
|
+
lines.push(ui.dim(' no usage data yet — codex records it during runs (try "codexswitch run" once)'));
|
|
396
392
|
} else {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (a.usage.at)
|
|
393
|
+
lines.push(gauge('5h ', a.usage.p5h, meta.threshold5h));
|
|
394
|
+
lines.push(gauge('weekly', a.usage.weekly, meta.thresholdWeekly));
|
|
395
|
+
if (a.usage.at) lines.push(ui.dim(` measured ${fmtDate(a.usage.at)}`));
|
|
400
396
|
}
|
|
401
|
-
}
|
|
397
|
+
});
|
|
402
398
|
const next = store.pickAccount();
|
|
403
|
-
|
|
399
|
+
lines.push(ui.dim(`\nrotation would pick: ${next ? next.name : '(none usable)'} · threshold 5h ${meta.threshold5h}% / weekly ${meta.thresholdWeekly}%`));
|
|
400
|
+
return lines;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function cmdUsage(args) {
|
|
404
|
+
const meta = store.loadMeta();
|
|
405
|
+
let accounts = store.listAccounts();
|
|
406
|
+
if (args[0]) {
|
|
407
|
+
accounts = accounts.filter((a) => a.name === args[0]);
|
|
408
|
+
if (accounts.length === 0) throw new Error(`no such account: ${args[0]}`);
|
|
409
|
+
}
|
|
410
|
+
if (accounts.length === 0) {
|
|
411
|
+
out('no accounts yet — add one with "codexswitch login" or "codexswitch import"');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
for (const l of usageLines(accounts, meta)) out(l);
|
|
404
415
|
}
|
|
405
416
|
|
|
406
417
|
// Warm up quota measurements: send a minimal request per account so the
|
|
@@ -417,32 +428,38 @@ async function cmdProbe(args) {
|
|
|
417
428
|
out(ui.info(ui.dim(`probing ${accounts.length} account(s) with a minimal request...`)));
|
|
418
429
|
let failures = 0;
|
|
419
430
|
for (const a of accounts) {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
['exec', ...buildExecArgs(['Reply with exactly: ok'], meta)],
|
|
424
|
-
{ capture: true, silent: true }
|
|
425
|
-
);
|
|
426
|
-
const usage = runner.recordUsage(a.name, res.profile, startTs);
|
|
427
|
-
if (res.code !== 0) {
|
|
428
|
-
failures++;
|
|
429
|
-
const why = runner.looksAuthFailed(res.output)
|
|
430
|
-
? `login revoked — fix with "codexswitch login ${a.name}"`
|
|
431
|
-
: runner.looksRateLimited(res.output, meta.limitPatterns)
|
|
432
|
-
? 'rate limited'
|
|
433
|
-
: `exit ${res.code}`;
|
|
434
|
-
out(ui.fail(`${a.name}: ${why}`));
|
|
435
|
-
store.logEvent('probe', `${a.name} failed: ${why}`);
|
|
436
|
-
} else if (usage && usage.p5h) {
|
|
437
|
-
out(ui.ok(`${a.name}: 5h ${Math.round(usage.p5h.pct)}%${usage.weekly ? ` / weekly ${Math.round(usage.weekly.pct)}%` : ''}`));
|
|
438
|
-
store.logEvent('probe', `${a.name} measured`);
|
|
439
|
-
} else {
|
|
440
|
-
out(ui.warn(`${a.name}: reachable, but codex reported no usage data`));
|
|
441
|
-
}
|
|
431
|
+
const line = await probeOne(a, meta);
|
|
432
|
+
out(line.text);
|
|
433
|
+
if (!line.ok) failures++;
|
|
442
434
|
}
|
|
443
435
|
return failures > 0 ? 1 : 0;
|
|
444
436
|
}
|
|
445
437
|
|
|
438
|
+
// Probe a single account; returns a printable result line.
|
|
439
|
+
async function probeOne(a, meta) {
|
|
440
|
+
const startTs = Date.now();
|
|
441
|
+
const res = await runner.runCodex(
|
|
442
|
+
a.name,
|
|
443
|
+
['exec', ...buildExecArgs(['Reply with exactly: ok'], meta)],
|
|
444
|
+
{ capture: true, silent: true }
|
|
445
|
+
);
|
|
446
|
+
const usage = runner.recordUsage(a.name, res.profile, startTs);
|
|
447
|
+
if (res.code !== 0) {
|
|
448
|
+
const why = runner.looksAuthFailed(res.output)
|
|
449
|
+
? `login revoked — fix with "codexswitch login ${a.name}"`
|
|
450
|
+
: runner.looksRateLimited(res.output, meta.limitPatterns)
|
|
451
|
+
? 'rate limited'
|
|
452
|
+
: `exit ${res.code}`;
|
|
453
|
+
store.logEvent('probe', `${a.name} failed: ${why}`);
|
|
454
|
+
return { ok: false, text: ui.fail(`${a.name}: ${why}`) };
|
|
455
|
+
}
|
|
456
|
+
store.logEvent('probe', `${a.name} measured`);
|
|
457
|
+
if (usage && usage.p5h) {
|
|
458
|
+
return { ok: true, text: ui.ok(`${a.name}: 5h ${Math.round(usage.p5h.pct)}%${usage.weekly ? ` / weekly ${Math.round(usage.weekly.pct)}%` : ''}`) };
|
|
459
|
+
}
|
|
460
|
+
return { ok: true, text: ui.warn(`${a.name}: reachable, but codex reported no usage data`) };
|
|
461
|
+
}
|
|
462
|
+
|
|
446
463
|
function cmdLog(args) {
|
|
447
464
|
const count = args[0] ? parseInt(args[0], 10) : 20;
|
|
448
465
|
if (Number.isNaN(count) || count < 1) throw new Error('usage: codexswitch log [count]');
|
|
@@ -458,35 +475,81 @@ function cmdLog(args) {
|
|
|
458
475
|
}
|
|
459
476
|
}
|
|
460
477
|
|
|
461
|
-
// Live dashboard:
|
|
478
|
+
// Live interactive dashboard: usage gauges + recent activity, refreshed
|
|
479
|
+
// every 5s. Keyboard: up/down select, s switch, e enable/disable, p probe,
|
|
480
|
+
// r refresh, q quit.
|
|
462
481
|
async function cmdWatch() {
|
|
463
482
|
if (!process.stdout.isTTY) {
|
|
464
483
|
cmdUsage([]);
|
|
465
484
|
return 0;
|
|
466
485
|
}
|
|
486
|
+
let cursor = 0;
|
|
487
|
+
let message = '';
|
|
488
|
+
let busy = false;
|
|
467
489
|
const render = () => {
|
|
490
|
+
const meta = store.loadMeta();
|
|
491
|
+
const accounts = store.listAccounts();
|
|
492
|
+
if (cursor >= accounts.length) cursor = Math.max(0, accounts.length - 1);
|
|
468
493
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
469
|
-
out(`${ui.bold('codexswitch watch')}${ui.dim(` ${new Date().toLocaleTimeString()}
|
|
470
|
-
|
|
471
|
-
|
|
494
|
+
out(`${ui.bold('codexswitch watch')}${ui.dim(` ${new Date().toLocaleTimeString()} \u00b7 refreshes every 5s`)}\n`);
|
|
495
|
+
if (accounts.length === 0) {
|
|
496
|
+
out('no accounts yet \u2014 add one with "codexswitch login"');
|
|
497
|
+
} else {
|
|
498
|
+
for (const l of usageLines(accounts, meta, { cursor })) out(l);
|
|
499
|
+
}
|
|
500
|
+
const events = store.readEvents(4);
|
|
472
501
|
if (events.length > 0) {
|
|
473
502
|
out(ui.dim('\nrecent activity:'));
|
|
474
503
|
for (const e of events) out(ui.dim(` ${e.at.replace('T', ' ').slice(11, 19)} ${e.type}: ${e.message}`));
|
|
475
504
|
}
|
|
505
|
+
out(`\n${ui.dim('keys:')} ${ui.cyan('\u2191/\u2193')} select ${ui.cyan('s')} switch ${ui.cyan('e')} enable/disable ${ui.cyan('p')} probe ${ui.cyan('r')} refresh ${ui.cyan('q')} quit`);
|
|
506
|
+
if (message) out(message);
|
|
476
507
|
};
|
|
477
508
|
render();
|
|
478
|
-
const timer = setInterval(
|
|
509
|
+
const timer = setInterval(() => {
|
|
510
|
+
if (!busy) render();
|
|
511
|
+
}, 5000);
|
|
479
512
|
await new Promise((resolve) => {
|
|
480
513
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
481
514
|
process.stdin.resume();
|
|
482
|
-
process.stdin.on('data', (b) => {
|
|
515
|
+
process.stdin.on('data', async (b) => {
|
|
516
|
+
if (busy) return;
|
|
483
517
|
const s = b.toString();
|
|
518
|
+
const accounts = store.listAccounts();
|
|
519
|
+
const selected = accounts[cursor];
|
|
484
520
|
if (s === 'q' || s === '\u0003') {
|
|
485
521
|
clearInterval(timer);
|
|
486
522
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
487
523
|
process.stdin.pause();
|
|
488
524
|
resolve();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (s === '\x1b[A' || s === 'k') cursor = Math.max(0, cursor - 1);
|
|
528
|
+
else if (s === '\x1b[B' || s === 'j') cursor = Math.min(accounts.length - 1, cursor + 1);
|
|
529
|
+
else if (s === 'r') message = '';
|
|
530
|
+
else if (s === 's' && selected) {
|
|
531
|
+
try {
|
|
532
|
+
cmdUse([selected.name]);
|
|
533
|
+
message = ui.ok(`switched to "${selected.name}"`);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
message = ui.fail(e.message);
|
|
536
|
+
}
|
|
537
|
+
} else if (s === 'e' && selected) {
|
|
538
|
+
setFlag(selected.name, { disabled: !selected.disabled });
|
|
539
|
+
message = selected.disabled ? ui.ok(`enabled "${selected.name}"`) : ui.warn(`disabled "${selected.name}"`);
|
|
540
|
+
} else if (s === 'p' && selected) {
|
|
541
|
+
busy = true;
|
|
542
|
+
message = ui.dim(`probing "${selected.name}"...`);
|
|
543
|
+
render();
|
|
544
|
+
try {
|
|
545
|
+
const result = await probeOne(selected, store.loadMeta());
|
|
546
|
+
message = result.text;
|
|
547
|
+
} catch (e) {
|
|
548
|
+
message = ui.fail(e.message);
|
|
549
|
+
}
|
|
550
|
+
busy = false;
|
|
489
551
|
}
|
|
552
|
+
render();
|
|
490
553
|
});
|
|
491
554
|
});
|
|
492
555
|
return 0;
|
|
@@ -575,7 +638,7 @@ function cmdNames() {
|
|
|
575
638
|
}
|
|
576
639
|
|
|
577
640
|
const COMMANDS =
|
|
578
|
-
'login import add-key list usage watch probe log use current next run exec order model remove rename ' +
|
|
641
|
+
'login import add-key list usage watch probe log chat use current next run exec order model remove rename ' +
|
|
579
642
|
'enable disable priority clear-limit cooldown threshold patterns export restore sync completion help';
|
|
580
643
|
|
|
581
644
|
function cmdCompletion(args) {
|
|
@@ -600,6 +663,18 @@ complete -F _codexswitch codexswitch cxs`;
|
|
|
600
663
|
// non-git folders, and the configured default model — unless the user
|
|
601
664
|
// already passed their own flags.
|
|
602
665
|
function buildExecArgs(rest, meta) {
|
|
666
|
+
// "exec resume [--last|<id>] ..." — the subcommand and its target must
|
|
667
|
+
// stay in front; flags are injected after them.
|
|
668
|
+
if (rest[0] === 'resume') {
|
|
669
|
+
const head = ['resume'];
|
|
670
|
+
let i = 1;
|
|
671
|
+
if (rest[i] === '--last' || (rest[i] && /^[0-9a-f][0-9a-f-]{7,}$/i.test(rest[i]))) head.push(rest[i++]);
|
|
672
|
+
return [...head, ...injectExecFlags(rest.slice(i), meta)];
|
|
673
|
+
}
|
|
674
|
+
return injectExecFlags(rest, meta);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function injectExecFlags(rest, meta) {
|
|
603
678
|
const args = [...rest];
|
|
604
679
|
if (!args.includes('--skip-git-repo-check')) args.unshift('--skip-git-repo-check');
|
|
605
680
|
const hasModel = args.some((a) => a === '-m' || a === '--model' || a.startsWith('--model='));
|
|
@@ -716,6 +791,79 @@ async function cmdExec(args) {
|
|
|
716
791
|
return 2;
|
|
717
792
|
}
|
|
718
793
|
|
|
794
|
+
// Interactive chat: a Claude Code-style prompt loop on top of exec.
|
|
795
|
+
// Every turn goes through the rotation engine, and turns 2+ resume the same
|
|
796
|
+
// codex session — so the conversation continues even when the account under
|
|
797
|
+
// it changes between (or during) turns.
|
|
798
|
+
async function cmdChat() {
|
|
799
|
+
const readline = require('readline');
|
|
800
|
+
runner.assertCodexAvailable();
|
|
801
|
+
if (store.listAccounts().length === 0) {
|
|
802
|
+
throw new Error('no accounts — add one with "codexswitch login"');
|
|
803
|
+
}
|
|
804
|
+
const meta = store.loadMeta();
|
|
805
|
+
out(`${ui.bold('codexswitch chat')} ${ui.dim(`· model ${meta.model || '(codex default)'} · rotation on limits`)}`);
|
|
806
|
+
out(ui.dim('type a prompt, or /help for commands (/quit to exit)\n'));
|
|
807
|
+
|
|
808
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
809
|
+
rl.setPrompt(ui.enabled ? `${ui.cyan('cxs')} ${ui.dim('›')} ` : 'cxs › ');
|
|
810
|
+
// Buffer lines ourselves: input can arrive while a turn is still running
|
|
811
|
+
// (type-ahead, piped input) and must not be dropped or hit a closed rl.
|
|
812
|
+
const pending = [];
|
|
813
|
+
const waiters = [];
|
|
814
|
+
let closed = false;
|
|
815
|
+
rl.on('line', (l) => {
|
|
816
|
+
const w = waiters.shift();
|
|
817
|
+
if (w) w(l);
|
|
818
|
+
else pending.push(l);
|
|
819
|
+
});
|
|
820
|
+
rl.on('close', () => {
|
|
821
|
+
closed = true;
|
|
822
|
+
for (const w of waiters.splice(0)) w(null);
|
|
823
|
+
});
|
|
824
|
+
const ask = () => {
|
|
825
|
+
if (pending.length > 0) return Promise.resolve(pending.shift());
|
|
826
|
+
if (closed) return Promise.resolve(null);
|
|
827
|
+
rl.prompt();
|
|
828
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
let inSession = false;
|
|
832
|
+
for (;;) {
|
|
833
|
+
const raw = await ask();
|
|
834
|
+
if (raw == null) break; // EOF (Ctrl-D or piped input ended)
|
|
835
|
+
const line = raw.trim();
|
|
836
|
+
if (!line) continue;
|
|
837
|
+
|
|
838
|
+
if (line[0] === '/') {
|
|
839
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
840
|
+
try {
|
|
841
|
+
if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') break;
|
|
842
|
+
else if (cmd === 'help')
|
|
843
|
+
out(ui.dim('/usage /list /use <name> /next /model [m] /new (fresh session) /quit'));
|
|
844
|
+
else if (cmd === 'usage') cmdUsage([]);
|
|
845
|
+
else if (cmd === 'list') cmdList();
|
|
846
|
+
else if (cmd === 'use') cmdUse(rest);
|
|
847
|
+
else if (cmd === 'next') cmdNext();
|
|
848
|
+
else if (cmd === 'model') cmdModel(rest);
|
|
849
|
+
else if (cmd === 'new') {
|
|
850
|
+
inSession = false;
|
|
851
|
+
out(ui.ok('starting a fresh session on the next prompt'));
|
|
852
|
+
} else out(ui.warn(`unknown command /${cmd} — try /help`));
|
|
853
|
+
} catch (e) {
|
|
854
|
+
out(ui.fail(e.message));
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const code = await cmdExec(inSession ? ['resume', '--last', line] : [line]);
|
|
860
|
+
if (code === 0) inSession = true;
|
|
861
|
+
else if (code === 2) out(ui.fail('all accounts exhausted — try again later or /use a specific account'));
|
|
862
|
+
}
|
|
863
|
+
rl.close();
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
719
867
|
function warnIfOverThreshold(name) {
|
|
720
868
|
const meta = store.loadMeta();
|
|
721
869
|
const account = store.listAccounts().find((a) => a.name === name);
|
|
@@ -817,6 +965,9 @@ async function main(argv) {
|
|
|
817
965
|
return cmdRun(args);
|
|
818
966
|
case 'exec':
|
|
819
967
|
return cmdExec(args);
|
|
968
|
+
case 'chat':
|
|
969
|
+
case 'repl':
|
|
970
|
+
return cmdChat();
|
|
820
971
|
default:
|
|
821
972
|
// Forward everything else to codex under the managed account, so
|
|
822
973
|
// codexswitch is a drop-in replacement: "cxs goal", "cxs resume", ...
|