@carjms/codexswitch 0.6.3 → 0.7.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 +10 -3
- package/README.md +10 -3
- package/package.json +1 -1
- package/src/cli.js +478 -38
- package/src/proxy.js +52 -7
- package/src/store.js +78 -0
- package/src/util.js +30 -3
- package/src/welcome.js +184 -0
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
6
7
|
const { readJSONSafe, writeJSONAtomic, authInfo, ensureDir, fmtDate, fmtRemaining, table } = require('./util.js');
|
|
7
8
|
const store = require('./store.js');
|
|
8
9
|
const runner = require('./runner.js');
|
|
@@ -10,6 +11,8 @@ const ui = require('./ui.js');
|
|
|
10
11
|
|
|
11
12
|
const HELP = `codexswitch — multi-account manager for the OpenAI Codex CLI
|
|
12
13
|
|
|
14
|
+
Run without a command to open the persistent interactive prompt.
|
|
15
|
+
|
|
13
16
|
Accounts
|
|
14
17
|
login [name] log in to a new account (isolated "codex login") and store it
|
|
15
18
|
import [name] import the account currently in ~/.codex/auth.json
|
|
@@ -21,6 +24,8 @@ Accounts
|
|
|
21
24
|
probe [name] warm up quota gauges with a minimal request per
|
|
22
25
|
account (costs a few tokens; alias: warmup)
|
|
23
26
|
log [count] recent activity: switches, limits, rotations
|
|
27
|
+
history [count] work history: request, result, account, duration, files
|
|
28
|
+
history show <id> full details (use "latest" for the newest run)
|
|
24
29
|
use <name> make <name> the active account in ~/.codex
|
|
25
30
|
current show the active account
|
|
26
31
|
next switch to the next account in rotation order (wraps around)
|
|
@@ -34,10 +39,11 @@ Accounts
|
|
|
34
39
|
clear-limit <name> forget a recorded rate-limit for an account
|
|
35
40
|
|
|
36
41
|
Running codex
|
|
37
|
-
chat
|
|
42
|
+
chat [--simple] interactive prompt loop with a two-column welcome
|
|
43
|
+
dashboard (--simple skips the full-screen landing): each turn
|
|
38
44
|
runs through rotation and resumes the same session,
|
|
39
45
|
so the conversation survives account switches
|
|
40
|
-
(/usage /use /next /model /new /quit
|
|
46
|
+
(/status /usage /memory /use /next /model /new /quit)
|
|
41
47
|
run [name] [args...] run codex as <name> (or active account) in an isolated
|
|
42
48
|
per-account CODEX_HOME; config/sessions are shared
|
|
43
49
|
exec [args...] run "codex exec ..." and auto-rotate through accounts in
|
|
@@ -47,18 +53,25 @@ Running codex
|
|
|
47
53
|
--no-resume restart the prompt instead of resuming on rotation
|
|
48
54
|
server [--port N] EXPERIMENTAL local proxy: per-request account
|
|
49
55
|
rotation with live usage from response headers
|
|
50
|
-
|
|
56
|
+
server status show whether the local proxy is reachable
|
|
57
|
+
run --proxy [args...] use proxy when available, otherwise run directly
|
|
58
|
+
--require-proxy fail instead of falling back when proxy is unavailable
|
|
51
59
|
|
|
52
60
|
Settings
|
|
53
61
|
model [name|default] show/set the default model injected into run/exec
|
|
54
62
|
reasoning [mode] how much model reasoning to print during runs:
|
|
55
63
|
show (codex default) | concise | hide
|
|
64
|
+
output [mode] exec/chat display: auto (compact on TTY, raw when
|
|
65
|
+
piped) | compact (result + summary) | raw
|
|
56
66
|
sandbox [mode] file access for exec/chat: read-only (codex default)
|
|
57
|
-
| write (edit files in cwd) |
|
|
67
|
+
| write (edit files in cwd) | write+net (also allow
|
|
68
|
+
ssh/curl/network) | full (no sandbox)
|
|
58
69
|
threshold [5h%] [wk%] rotate early when recorded usage reaches these percents
|
|
59
70
|
of the 5-hour / weekly window (default 95, one value = both)
|
|
60
71
|
cooldown [minutes] show/set rate-limit cooldown (default 60)
|
|
61
72
|
patterns [add|remove] extra regex patterns treated as rate-limit errors
|
|
73
|
+
memory [command] cross-account memory for exec/chat: status, shared,
|
|
74
|
+
isolated, off, add <text>, show [account], path [account]
|
|
62
75
|
|
|
63
76
|
Maintenance
|
|
64
77
|
sync save tokens refreshed by codex back into the store
|
|
@@ -154,7 +167,7 @@ function cmdLogin(args) {
|
|
|
154
167
|
function cmdList() {
|
|
155
168
|
const accounts = store.listAccounts();
|
|
156
169
|
if (accounts.length === 0) {
|
|
157
|
-
|
|
170
|
+
printEmptyAccounts();
|
|
158
171
|
return;
|
|
159
172
|
}
|
|
160
173
|
const now = Date.now();
|
|
@@ -169,10 +182,10 @@ function cmdList() {
|
|
|
169
182
|
const status = a.disabled
|
|
170
183
|
? ui.red('disabled')
|
|
171
184
|
: a.limitedUntil && a.limitedUntil > now
|
|
172
|
-
? ui.yellow(`
|
|
185
|
+
? ui.yellow(`paused ${fmtRemaining(a.limitedUntil)}`)
|
|
173
186
|
: over
|
|
174
187
|
? ui.yellow(`over-${over}`)
|
|
175
|
-
: ui.green('
|
|
188
|
+
: ui.green('ready');
|
|
176
189
|
return [
|
|
177
190
|
a.active ? ui.green('●') : '',
|
|
178
191
|
a.active ? ui.bold(a.name) : a.name,
|
|
@@ -186,7 +199,19 @@ function cmdList() {
|
|
|
186
199
|
];
|
|
187
200
|
});
|
|
188
201
|
out(table(rows, ['', 'name', 'email', 'plan', 'prio', 'status', '5h', 'week', 'token refreshed']));
|
|
189
|
-
|
|
202
|
+
const ready = accounts.filter((a) => store.isUsable(a, meta, now));
|
|
203
|
+
const next = store.pickAccount();
|
|
204
|
+
out(ui.dim(`\n${ready.length}/${accounts.length} ready · next: ${next ? next.name : 'none'} · threshold: 5h ${meta.threshold5h}% / week ${meta.thresholdWeekly}%`));
|
|
205
|
+
if (!next) out(ui.warn('No account can run now. Check details with "codexswitch usage".'));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printEmptyAccounts() {
|
|
209
|
+
out(ui.bold('No accounts yet'));
|
|
210
|
+
out(' 1. Import the account already signed in to Codex:');
|
|
211
|
+
out(` ${ui.cyan('codexswitch import')}`);
|
|
212
|
+
out(' 2. Add another account in an isolated login:');
|
|
213
|
+
out(` ${ui.cyan('codexswitch login <name>')}`);
|
|
214
|
+
out(ui.dim('\nYour current Codex login is never replaced during step 2.'));
|
|
190
215
|
}
|
|
191
216
|
|
|
192
217
|
function cmdUse(args) {
|
|
@@ -271,6 +296,12 @@ function cmdRename(args) {
|
|
|
271
296
|
if (store.accountExists(to)) throw new Error(`account "${to}" already exists`);
|
|
272
297
|
store.writeAccountAuth(to, auth);
|
|
273
298
|
fs.rmSync(store.accountPath(from), { force: true });
|
|
299
|
+
const oldMemory = store.memoryPath('isolated', from);
|
|
300
|
+
const newMemory = store.memoryPath('isolated', to);
|
|
301
|
+
if (fs.existsSync(oldMemory)) {
|
|
302
|
+
ensureDir(path.dirname(newMemory));
|
|
303
|
+
fs.renameSync(oldMemory, newMemory);
|
|
304
|
+
}
|
|
274
305
|
const meta = store.loadMeta();
|
|
275
306
|
meta.accounts[to] = meta.accounts[from] || { priority: 0 };
|
|
276
307
|
delete meta.accounts[from];
|
|
@@ -376,6 +407,13 @@ function cmdModel(args) {
|
|
|
376
407
|
function usageLines(accounts, meta, { cursor = -1 } = {}) {
|
|
377
408
|
const now = Date.now();
|
|
378
409
|
const lines = [];
|
|
410
|
+
const usable = accounts.filter((a) => store.isUsable(a, meta, now));
|
|
411
|
+
const active = accounts.find((a) => a.active);
|
|
412
|
+
lines.push(
|
|
413
|
+
`${ui.bold('Usage overview')} ${ui.green(`${usable.length} ready`)} / ${accounts.length}` +
|
|
414
|
+
`${active ? ui.dim(` · active ${active.name}`) : ''}`
|
|
415
|
+
);
|
|
416
|
+
lines.push('');
|
|
379
417
|
const bar = (pct, threshold) => {
|
|
380
418
|
const width = 20;
|
|
381
419
|
const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)));
|
|
@@ -395,7 +433,7 @@ function usageLines(accounts, meta, { cursor = -1 } = {}) {
|
|
|
395
433
|
const state = a.disabled ? ui.red(' [disabled]') : a.limitedUntil && a.limitedUntil > now ? ui.yellow(` [limited ${fmtRemaining(a.limitedUntil)}]`) : '';
|
|
396
434
|
lines.push(`${cursor >= 0 ? sel : ''}${mark} ${ui.bold(a.name)}${ui.dim(` (${a.plan || a.email || '-'})`)}${state}`);
|
|
397
435
|
if (!a.usage) {
|
|
398
|
-
lines.push(ui.dim(
|
|
436
|
+
lines.push(ui.dim(` no usage data yet — measure it with "codexswitch probe ${a.name}"`));
|
|
399
437
|
} else {
|
|
400
438
|
lines.push(gauge('5h ', a.usage.p5h, meta.threshold5h));
|
|
401
439
|
lines.push(gauge('weekly', a.usage.weekly, meta.thresholdWeekly));
|
|
@@ -403,7 +441,8 @@ function usageLines(accounts, meta, { cursor = -1 } = {}) {
|
|
|
403
441
|
}
|
|
404
442
|
});
|
|
405
443
|
const next = store.pickAccount();
|
|
406
|
-
lines.push(ui.dim(
|
|
444
|
+
lines.push(`\n${ui.dim('Next account')} ${next ? ui.bold(next.name) : ui.red('none usable')}`);
|
|
445
|
+
lines.push(ui.dim(`Thresholds 5h ${meta.threshold5h}% · week ${meta.thresholdWeekly}%`));
|
|
407
446
|
return lines;
|
|
408
447
|
}
|
|
409
448
|
|
|
@@ -415,7 +454,7 @@ function cmdUsage(args) {
|
|
|
415
454
|
if (accounts.length === 0) throw new Error(`no such account: ${args[0]}`);
|
|
416
455
|
}
|
|
417
456
|
if (accounts.length === 0) {
|
|
418
|
-
|
|
457
|
+
printEmptyAccounts();
|
|
419
458
|
return;
|
|
420
459
|
}
|
|
421
460
|
for (const l of usageLines(accounts, meta)) out(l);
|
|
@@ -482,6 +521,93 @@ function cmdLog(args) {
|
|
|
482
521
|
}
|
|
483
522
|
}
|
|
484
523
|
|
|
524
|
+
function shortText(value, max = 56) {
|
|
525
|
+
const oneLine = String(value || '').replace(/\s+/g, ' ').trim();
|
|
526
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function formatDuration(ms) {
|
|
530
|
+
if (!Number.isFinite(ms)) return '-';
|
|
531
|
+
if (ms < 1000) return `${ms}ms`;
|
|
532
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)}s`;
|
|
533
|
+
return `${Math.floor(ms / 60000)}m${Math.round((ms % 60000) / 1000)}s`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function cmdHistory(args) {
|
|
537
|
+
if (args[0] === 'show') {
|
|
538
|
+
const id = requireArg(args, 1, 'history id (or "latest")');
|
|
539
|
+
const run = store.findRun(id);
|
|
540
|
+
if (!run) throw new Error(`no history entry matching "${id}"`);
|
|
541
|
+
out(`${ui.bold(run.id)} ${run.status === 'done' ? ui.green('done') : ui.red(run.status)}`);
|
|
542
|
+
out(`${ui.dim('Started')} ${fmtDate(run.startedAt)}`);
|
|
543
|
+
out(`${ui.dim('Duration')} ${formatDuration(run.durationMs)}`);
|
|
544
|
+
out(`${ui.dim('Directory')} ${run.cwd || '-'}`);
|
|
545
|
+
out(`${ui.dim('Session')} ${run.session} · memory ${run.memory} · reasoning ${run.reasoning}`);
|
|
546
|
+
out(`\n${ui.bold('Request')}\n${run.prompt || ui.dim('(not captured)')}`);
|
|
547
|
+
out(`\n${ui.bold('Attempts')}`);
|
|
548
|
+
for (const attempt of run.attempts || []) {
|
|
549
|
+
const paint = attempt.status === 'done' ? ui.green : attempt.status === 'rate-limited' ? ui.yellow : ui.red;
|
|
550
|
+
out(` ${paint(attempt.status.padEnd(12))} ${attempt.account} · ${formatDuration(attempt.durationMs)}`);
|
|
551
|
+
}
|
|
552
|
+
out(`\n${ui.bold('Files changed during this run')}`);
|
|
553
|
+
if (run.touchedFiles && run.touchedFiles.length) run.touchedFiles.forEach((file) => out(` ${file}`));
|
|
554
|
+
else out(ui.dim(' no file changes detected'));
|
|
555
|
+
out(`\n${ui.bold('Git workspace after run')}`);
|
|
556
|
+
if (run.files && run.files.length) run.files.forEach((file) => out(` ${file}`));
|
|
557
|
+
else out(ui.dim(' clean'));
|
|
558
|
+
if (run.promptTruncated) out(ui.warn('\nRequest was truncated to the last 16 KiB in history.'));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const count = args[0] == null ? 20 : parseInt(args[0], 10);
|
|
562
|
+
if (Number.isNaN(count) || count < 1 || count > 200) {
|
|
563
|
+
throw new Error('usage: codexswitch history [1-200] | history show <id|latest>');
|
|
564
|
+
}
|
|
565
|
+
const runs = store.readRuns(count);
|
|
566
|
+
if (runs.length === 0) {
|
|
567
|
+
out('no work history yet — exec, run <account> exec, and chat runs appear here');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const rows = runs.map((run) => [
|
|
571
|
+
run.id,
|
|
572
|
+
fmtDate(run.startedAt),
|
|
573
|
+
run.status === 'done' ? ui.green('done') : ui.red(run.status),
|
|
574
|
+
(run.attempts || []).map((a) => a.account).join(' → ') || '-',
|
|
575
|
+
formatDuration(run.durationMs),
|
|
576
|
+
shortText(run.prompt),
|
|
577
|
+
]);
|
|
578
|
+
out(table(rows, ['id', 'started', 'result', 'account flow', 'time', 'request']));
|
|
579
|
+
out(ui.dim('\nFull details: codexswitch history show <id>'));
|
|
580
|
+
out(ui.dim(`Stored locally in ${path.join(store.paths().home, 'history.jsonl')} (contains prompts)`));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function workspaceSnapshot() {
|
|
584
|
+
const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], { cwd: process.cwd(), encoding: 'utf8' });
|
|
585
|
+
if (result.status !== 0 || !result.stdout) return {};
|
|
586
|
+
const snapshot = {};
|
|
587
|
+
for (const line of result.stdout.trim().split(/\r?\n/).filter(Boolean).slice(0, 100)) {
|
|
588
|
+
let file = line.slice(3);
|
|
589
|
+
if (file.includes(' -> ')) file = file.split(' -> ').pop();
|
|
590
|
+
if (file.startsWith('"')) {
|
|
591
|
+
try { file = JSON.parse(file); } catch { /* keep Git's displayed path */ }
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
const stat = fs.statSync(path.resolve(file), { bigint: true });
|
|
595
|
+
snapshot[line] = `${stat.size}:${stat.mtimeNs}`;
|
|
596
|
+
} catch {
|
|
597
|
+
snapshot[line] = 'missing';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return snapshot;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function promptFromExecArgs(args) {
|
|
604
|
+
if (!args.length) return '';
|
|
605
|
+
const separator = args.lastIndexOf('--');
|
|
606
|
+
const value = args[args.length - 1];
|
|
607
|
+
if (separator < 0 && String(value).startsWith('-')) return '';
|
|
608
|
+
return String(value);
|
|
609
|
+
}
|
|
610
|
+
|
|
485
611
|
// Live interactive dashboard: usage gauges + recent activity, refreshed
|
|
486
612
|
// every 5s. Keyboard: up/down select, s switch, e enable/disable, p probe,
|
|
487
613
|
// r refresh, q quit.
|
|
@@ -645,8 +771,8 @@ function cmdNames() {
|
|
|
645
771
|
}
|
|
646
772
|
|
|
647
773
|
const COMMANDS =
|
|
648
|
-
'login import add-key list usage watch probe log chat server use current next run exec order model remove rename ' +
|
|
649
|
-
'enable disable priority clear-limit cooldown threshold reasoning sandbox patterns export restore sync completion help';
|
|
774
|
+
'login import add-key list usage watch probe log history chat server use current next run exec order model remove rename ' +
|
|
775
|
+
'enable disable priority clear-limit cooldown threshold reasoning output sandbox memory patterns export restore sync completion help';
|
|
650
776
|
|
|
651
777
|
function cmdCompletion(args) {
|
|
652
778
|
const shell = args[0];
|
|
@@ -693,12 +819,27 @@ function injectExecFlags(rest, meta) {
|
|
|
693
819
|
a === '--full-auto' ||
|
|
694
820
|
a === '--dangerously-bypass-approvals-and-sandbox'
|
|
695
821
|
);
|
|
696
|
-
if (meta.sandbox && !hasSandbox)
|
|
822
|
+
if (meta.sandbox && !hasSandbox) {
|
|
823
|
+
if (meta.sandbox === 'workspace-write+net') {
|
|
824
|
+
// workspace-write plus outbound network (ssh, curl, npm install...)
|
|
825
|
+
args.unshift('--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true');
|
|
826
|
+
} else {
|
|
827
|
+
args.unshift('--sandbox', meta.sandbox);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
697
830
|
args.unshift(...reasoningFlags(meta, args));
|
|
698
831
|
return args;
|
|
699
832
|
}
|
|
700
833
|
|
|
701
|
-
const SANDBOX_MODES = {
|
|
834
|
+
const SANDBOX_MODES = {
|
|
835
|
+
'read-only': 'read-only',
|
|
836
|
+
write: 'workspace-write',
|
|
837
|
+
'workspace-write': 'workspace-write',
|
|
838
|
+
'write+net': 'workspace-write+net',
|
|
839
|
+
net: 'workspace-write+net',
|
|
840
|
+
full: 'danger-full-access',
|
|
841
|
+
'danger-full-access': 'danger-full-access',
|
|
842
|
+
};
|
|
702
843
|
|
|
703
844
|
// codex exec defaults to a read-only sandbox; this setting lets it write.
|
|
704
845
|
function cmdSandbox(args) {
|
|
@@ -714,14 +855,15 @@ function cmdSandbox(args) {
|
|
|
714
855
|
return;
|
|
715
856
|
}
|
|
716
857
|
const mode = SANDBOX_MODES[args[0]];
|
|
717
|
-
if (!mode) throw new Error('usage: codexswitch sandbox <read-only|write|full>');
|
|
858
|
+
if (!mode) throw new Error('usage: codexswitch sandbox <read-only|write|write+net|full>');
|
|
718
859
|
meta.sandbox = mode;
|
|
719
860
|
store.saveMeta(meta);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
861
|
+
const messages = {
|
|
862
|
+
'workspace-write': ui.ok('sandbox set to workspace-write — codex can edit files in the working directory'),
|
|
863
|
+
'workspace-write+net': ui.ok('sandbox set to workspace-write + network — codex can also use ssh/curl/npm'),
|
|
864
|
+
'danger-full-access': ui.warn('sandbox set to danger-full-access — codex can touch anything, use with care'),
|
|
865
|
+
};
|
|
866
|
+
out(messages[mode]);
|
|
725
867
|
}
|
|
726
868
|
|
|
727
869
|
// -c overrides implementing "cxs reasoning hide|concise|show".
|
|
@@ -756,9 +898,157 @@ function cmdReasoning(args) {
|
|
|
756
898
|
);
|
|
757
899
|
}
|
|
758
900
|
|
|
901
|
+
function cmdOutput(args) {
|
|
902
|
+
const meta = store.loadMeta();
|
|
903
|
+
if (args[0] == null) {
|
|
904
|
+
out(`output: ${meta.outputMode || 'auto'} ${ui.dim('(auto | compact | raw)')}`);
|
|
905
|
+
out(ui.dim('auto uses compact output in a terminal and raw output when piped'));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const mode = args[0];
|
|
909
|
+
if (!['auto', 'compact', 'raw'].includes(mode)) throw new Error('usage: codexswitch output <auto|compact|raw>');
|
|
910
|
+
if (mode === 'auto') delete meta.outputMode;
|
|
911
|
+
else meta.outputMode = mode;
|
|
912
|
+
store.saveMeta(meta);
|
|
913
|
+
out(ui.ok(`output set to ${mode}`));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function compactOutputEnabled(meta) {
|
|
917
|
+
const mode = meta.outputMode || 'auto';
|
|
918
|
+
return mode === 'compact' || (mode === 'auto' && process.stdout.isTTY);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function outputFileArg(args, file) {
|
|
922
|
+
if (args.some((a) => a === '-o' || a === '--output-last-message' || a.startsWith('--output-last-message='))) return args;
|
|
923
|
+
const result = [...args];
|
|
924
|
+
const resumeOffset = result[0] === 'resume' ? (result[1] === '--last' || /^[0-9a-f][0-9a-f-]{7,}$/i.test(result[1] || '') ? 2 : 1) : 0;
|
|
925
|
+
result.splice(resumeOffset, 0, '--output-last-message', file);
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function existingOutputFile(args) {
|
|
930
|
+
for (let i = 0; i < args.length; i++) {
|
|
931
|
+
if ((args[i] === '-o' || args[i] === '--output-last-message') && args[i + 1]) return path.resolve(args[i + 1]);
|
|
932
|
+
if (args[i].startsWith('--output-last-message=')) return path.resolve(args[i].slice(args[i].indexOf('=') + 1));
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function startCompactProgress(account, attempt, session) {
|
|
938
|
+
const started = Date.now();
|
|
939
|
+
const label = () => `Working · ${account} · ${session} session · ${Math.round((Date.now() - started) / 1000)}s`;
|
|
940
|
+
if (!process.stdout.isTTY) {
|
|
941
|
+
out(ui.info(label()));
|
|
942
|
+
return () => {};
|
|
943
|
+
}
|
|
944
|
+
const draw = () => process.stderr.write(`\r\x1b[2K${ui.cyan('●')} ${label()}`);
|
|
945
|
+
draw();
|
|
946
|
+
const timer = setInterval(draw, 1000);
|
|
947
|
+
return () => {
|
|
948
|
+
clearInterval(timer);
|
|
949
|
+
process.stderr.write('\r\x1b[2K');
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function printCompactResult(message, run) {
|
|
954
|
+
out(`\n${ui.bold(ui.green('Result'))}`);
|
|
955
|
+
out(ui.dim('─'.repeat(48)));
|
|
956
|
+
out((message || '').trim() || ui.dim('(Codex returned no final message)'));
|
|
957
|
+
out(`\n${ui.bold('Work summary')}`);
|
|
958
|
+
out(ui.dim('─'.repeat(48)));
|
|
959
|
+
out(`${ui.green('✓')} ${run.status} · ${formatDuration(run.durationMs)} · ${(run.attempts || []).map((a) => a.account).join(' → ')}`);
|
|
960
|
+
if (run.touchedFiles && run.touchedFiles.length) {
|
|
961
|
+
out(`${ui.dim('Changed')} ${run.touchedFiles.length} file(s)`);
|
|
962
|
+
run.touchedFiles.slice(0, 12).forEach((file) => out(` ${file}`));
|
|
963
|
+
if (run.touchedFiles.length > 12) out(ui.dim(` … and ${run.touchedFiles.length - 12} more`));
|
|
964
|
+
} else {
|
|
965
|
+
out(ui.dim('Changed 0 files'));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function memoryTarget(meta, requestedAccount = null) {
|
|
970
|
+
if (meta.memoryMode !== 'isolated') return null;
|
|
971
|
+
const picked = store.pickAccount();
|
|
972
|
+
const name = requestedAccount || meta.active || (picked && picked.name);
|
|
973
|
+
if (!name || !store.accountExists(name)) throw new Error('isolated memory needs a valid account (activate one with "codexswitch use")');
|
|
974
|
+
return name;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function cmdMemory(args) {
|
|
978
|
+
const meta = store.loadMeta();
|
|
979
|
+
const sub = args[0];
|
|
980
|
+
if (!sub || sub === 'status') {
|
|
981
|
+
const mode = meta.memoryMode || 'off';
|
|
982
|
+
const account = mode === 'isolated' ? memoryTarget(meta) : null;
|
|
983
|
+
const file = mode === 'off' ? null : store.memoryPath(mode, account);
|
|
984
|
+
const size = file ? Buffer.byteLength(store.readMemory(mode, account), 'utf8') : 0;
|
|
985
|
+
out(`memory: ${ui.bold(mode)}${account ? ` · account ${ui.bold(account)}` : ''}`);
|
|
986
|
+
out(ui.dim(file ? ` ${size} bytes · ${file}` : ' disabled · existing Codex session files are still shared'));
|
|
987
|
+
if (mode === 'off') out(` Enable: ${ui.cyan('codexswitch memory shared')} ${ui.dim('(shared across accounts)')}`);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (['shared', 'isolated', 'off'].includes(sub)) {
|
|
991
|
+
meta.memoryMode = sub;
|
|
992
|
+
store.saveMeta(meta);
|
|
993
|
+
if (sub === 'shared') out(ui.ok('shared memory enabled for exec/chat — all accounts read the same memory file'));
|
|
994
|
+
else if (sub === 'isolated') out(ui.ok('isolated memory enabled — each account reads only its own memory file'));
|
|
995
|
+
else out(ui.ok('memory injection disabled — stored memory files were kept'));
|
|
996
|
+
if (sub !== 'off') out(ui.dim(`add a note: codexswitch memory add "your note"`));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const mode = meta.memoryMode || 'off';
|
|
1000
|
+
if (mode === 'off') throw new Error('memory is off — choose "codexswitch memory shared" or "memory isolated" first');
|
|
1001
|
+
if (sub === 'add') {
|
|
1002
|
+
const account = memoryTarget(meta);
|
|
1003
|
+
const text = args.slice(1).join(' ');
|
|
1004
|
+
const file = store.appendMemory(mode, account, text);
|
|
1005
|
+
out(ui.ok(`memory added${account ? ` for "${account}"` : ''}`));
|
|
1006
|
+
out(ui.dim(file));
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (sub === 'show') {
|
|
1010
|
+
const target = memoryTarget(meta, args[1]);
|
|
1011
|
+
const text = store.readMemory(mode, target);
|
|
1012
|
+
out(text || ui.dim('(memory is empty)'));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (sub === 'path') {
|
|
1016
|
+
out(store.memoryPath(mode, memoryTarget(meta, args[1])));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
throw new Error('usage: codexswitch memory [status|shared|isolated|off|add <text>|show [account]|path [account]]');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function withMemoryPrompt(args, accountName, meta) {
|
|
1023
|
+
const mode = meta.memoryMode || 'off';
|
|
1024
|
+
if (mode === 'off') return args;
|
|
1025
|
+
const memory = store.readMemory(mode, mode === 'isolated' ? accountName : null).trim();
|
|
1026
|
+
if (!memory) return args;
|
|
1027
|
+
const result = [...args];
|
|
1028
|
+
const separator = result.lastIndexOf('--');
|
|
1029
|
+
const promptIndex = separator >= 0 && separator < result.length - 1
|
|
1030
|
+
? result.length - 1
|
|
1031
|
+
: result.length > 0 && !String(result[result.length - 1]).startsWith('-')
|
|
1032
|
+
? result.length - 1
|
|
1033
|
+
: -1;
|
|
1034
|
+
if (promptIndex < 0) return result;
|
|
1035
|
+
result[promptIndex] =
|
|
1036
|
+
`<codexswitch-memory mode="${mode}">\n${memory}\n</codexswitch-memory>\n\n` +
|
|
1037
|
+
`<user-request>\n${result[promptIndex]}\n</user-request>`;
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
759
1041
|
// EXPERIMENTAL: foreground proxy server for per-request rotation.
|
|
760
1042
|
async function cmdServer(args) {
|
|
761
1043
|
const proxy = require('./proxy.js');
|
|
1044
|
+
if (args[0] === 'status') {
|
|
1045
|
+
const state = readJSONSafe(path.join(store.paths().home, 'proxy.json'));
|
|
1046
|
+
const port = (state && state.port) || proxy.DEFAULT_PORT;
|
|
1047
|
+
const running = await proxy.ping(port);
|
|
1048
|
+
out(running ? ui.ok(`proxy running on http://127.0.0.1:${port}`) : ui.warn(`proxy not reachable on port ${port}`));
|
|
1049
|
+
out(ui.dim(running ? `${store.listAccounts().length} account(s) available · per-request rotation active` : '"codexswitch run --proxy" will fall back to direct mode'));
|
|
1050
|
+
return 0;
|
|
1051
|
+
}
|
|
762
1052
|
let port = proxy.DEFAULT_PORT;
|
|
763
1053
|
for (let i = 0; i < args.length; i++) {
|
|
764
1054
|
if (args[i] === '--port' || args[i] === '-p') port = parseInt(args[++i], 10);
|
|
@@ -790,13 +1080,18 @@ async function cmdServer(args) {
|
|
|
790
1080
|
async function cmdRun(args) {
|
|
791
1081
|
store.syncBack(); // pick up tokens refreshed by plain codex before overlaying
|
|
792
1082
|
let proxyPort = null;
|
|
1083
|
+
const requireProxy = args.includes('--require-proxy');
|
|
1084
|
+
args = args.filter((a) => a !== '--require-proxy');
|
|
793
1085
|
if (args.includes('--proxy')) {
|
|
794
1086
|
const proxy = require('./proxy.js');
|
|
795
1087
|
args = args.filter((a) => a !== '--proxy');
|
|
796
1088
|
const state = readJSONSafe(path.join(store.paths().home, 'proxy.json'));
|
|
797
1089
|
proxyPort = (state && state.port) || proxy.DEFAULT_PORT;
|
|
798
1090
|
if (!(await proxy.ping(proxyPort))) {
|
|
799
|
-
throw new Error(`no proxy on port ${proxyPort} — start it first with "codexswitch server"`);
|
|
1091
|
+
if (requireProxy) throw new Error(`no proxy on port ${proxyPort} — start it first with "codexswitch server"`);
|
|
1092
|
+
out(ui.warn(`proxy unavailable on port ${proxyPort} — running directly with the selected account`));
|
|
1093
|
+
out(ui.dim('use --require-proxy to fail instead of falling back'));
|
|
1094
|
+
proxyPort = null;
|
|
800
1095
|
}
|
|
801
1096
|
}
|
|
802
1097
|
let name = null;
|
|
@@ -812,9 +1107,10 @@ async function cmdRun(args) {
|
|
|
812
1107
|
name = meta.active && store.accountExists(meta.active) ? meta.active : picked.name;
|
|
813
1108
|
}
|
|
814
1109
|
if (rest[0] === '--') rest = rest.slice(1);
|
|
1110
|
+
const rawExecArgs = rest[0] === 'exec' ? rest.slice(1) : null;
|
|
815
1111
|
const meta = store.loadMeta();
|
|
816
1112
|
if (rest[0] === 'exec') {
|
|
817
|
-
rest = ['exec', ...buildExecArgs(rest.slice(1), meta)];
|
|
1113
|
+
rest = ['exec', ...buildExecArgs(withMemoryPrompt(rest.slice(1), name, meta), meta)];
|
|
818
1114
|
} else if (rest.length === 0) {
|
|
819
1115
|
// interactive TUI launch — apply the same defaults
|
|
820
1116
|
if (meta.model) rest = ['-m', meta.model];
|
|
@@ -822,8 +1118,30 @@ async function cmdRun(args) {
|
|
|
822
1118
|
}
|
|
823
1119
|
out(ui.info(`running codex as ${ui.bold(`"${name}"`)}${proxyPort ? ui.dim(` via proxy :${proxyPort}`) : ''}`));
|
|
824
1120
|
const startTs = Date.now();
|
|
1121
|
+
const before = rawExecArgs ? workspaceSnapshot() : null;
|
|
825
1122
|
const res = await runner.runCodex(name, rest, { proxyPort });
|
|
826
1123
|
runner.recordUsage(name, res.profile, startTs);
|
|
1124
|
+
if (rawExecArgs) {
|
|
1125
|
+
const rawPrompt = promptFromExecArgs(rawExecArgs);
|
|
1126
|
+
const after = workspaceSnapshot();
|
|
1127
|
+
const id = `${startTs.toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
|
|
1128
|
+
store.logRun({
|
|
1129
|
+
id,
|
|
1130
|
+
startedAt: startTs,
|
|
1131
|
+
cwd: process.cwd(),
|
|
1132
|
+
prompt: rawPrompt.length > 16384 ? rawPrompt.slice(-16384) : rawPrompt,
|
|
1133
|
+
promptTruncated: rawPrompt.length > 16384,
|
|
1134
|
+
session: rawExecArgs[0] === 'resume' ? 'continue' : 'new',
|
|
1135
|
+
memory: meta.memoryMode || 'off',
|
|
1136
|
+
reasoning: meta.reasoning || 'show',
|
|
1137
|
+
status: res.code === 0 ? 'done' : `failed-${res.code}`,
|
|
1138
|
+
durationMs: Date.now() - startTs,
|
|
1139
|
+
attempts: [{ account: name, status: res.code === 0 ? 'done' : `exit-${res.code}`, durationMs: Date.now() - startTs }],
|
|
1140
|
+
files: Object.keys(after),
|
|
1141
|
+
touchedFiles: Object.keys({ ...before, ...after }).filter((file) => before[file] !== after[file]),
|
|
1142
|
+
});
|
|
1143
|
+
console.error(ui.info(ui.dim(`history ${id} · view: codexswitch history show ${id}`)));
|
|
1144
|
+
}
|
|
827
1145
|
return res.code;
|
|
828
1146
|
}
|
|
829
1147
|
|
|
@@ -855,9 +1173,45 @@ async function cmdExec(args) {
|
|
|
855
1173
|
|
|
856
1174
|
store.syncBack(); // pick up tokens refreshed by plain codex before overlaying
|
|
857
1175
|
const meta = store.loadMeta();
|
|
1176
|
+
const sessionMode = rest[0] === 'resume' ? 'continue' : 'new';
|
|
1177
|
+
const reasoningMode = meta.reasoning || 'show';
|
|
1178
|
+
const compact = compactOutputEnabled(meta);
|
|
1179
|
+
const userOutputFile = existingOutputFile(rest);
|
|
1180
|
+
const lastMessageFile = userOutputFile || path.join(os.tmpdir(), `codexswitch-result-${process.pid}-${Date.now()}.txt`);
|
|
1181
|
+
const cleanupResult = () => {
|
|
1182
|
+
if (!userOutputFile) fs.rmSync(lastMessageFile, { force: true });
|
|
1183
|
+
};
|
|
858
1184
|
const total = store.listAccounts().length;
|
|
859
1185
|
if (total === 0) throw new Error('no accounts — add one with "codexswitch login"');
|
|
860
1186
|
|
|
1187
|
+
const historyStarted = Date.now();
|
|
1188
|
+
const rawPrompt = promptFromExecArgs(rest);
|
|
1189
|
+
const promptTruncated = rawPrompt.length > 16384;
|
|
1190
|
+
const runRecord = {
|
|
1191
|
+
id: `${historyStarted.toString(36)}-${Math.random().toString(36).slice(2, 5)}`,
|
|
1192
|
+
startedAt: historyStarted,
|
|
1193
|
+
cwd: process.cwd(),
|
|
1194
|
+
prompt: promptTruncated ? rawPrompt.slice(-16384) : rawPrompt,
|
|
1195
|
+
promptTruncated,
|
|
1196
|
+
session: sessionMode,
|
|
1197
|
+
memory: meta.memoryMode || 'off',
|
|
1198
|
+
reasoning: reasoningMode,
|
|
1199
|
+
status: 'running',
|
|
1200
|
+
attempts: [],
|
|
1201
|
+
workspaceBefore: workspaceSnapshot(),
|
|
1202
|
+
};
|
|
1203
|
+
const finishHistory = (status) => {
|
|
1204
|
+
runRecord.status = status;
|
|
1205
|
+
runRecord.durationMs = Date.now() - historyStarted;
|
|
1206
|
+
const after = workspaceSnapshot();
|
|
1207
|
+
runRecord.files = Object.keys(after);
|
|
1208
|
+
runRecord.touchedFiles = Object.keys({ ...runRecord.workspaceBefore, ...after }).filter(
|
|
1209
|
+
(file) => runRecord.workspaceBefore[file] !== after[file]
|
|
1210
|
+
);
|
|
1211
|
+
delete runRecord.workspaceBefore;
|
|
1212
|
+
store.logRun(runRecord);
|
|
1213
|
+
};
|
|
1214
|
+
|
|
861
1215
|
const tried = [];
|
|
862
1216
|
let useResume = false;
|
|
863
1217
|
for (let attempt = 0; attempt < total; attempt++) {
|
|
@@ -870,25 +1224,47 @@ async function cmdExec(args) {
|
|
|
870
1224
|
name = picked.name;
|
|
871
1225
|
}
|
|
872
1226
|
tried.push(name);
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1227
|
+
if (!compact) {
|
|
1228
|
+
console.error(
|
|
1229
|
+
ui.info(
|
|
1230
|
+
`account ${ui.bold(`"${name}"`)}${ui.dim(` · session ${useResume || sessionMode === 'continue' ? 'continue' : 'new'} · reasoning ${reasoningMode}`)}` +
|
|
1231
|
+
`${ui.dim(attempt > 0 ? ` · attempt ${attempt + 1}` : '')}`
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
876
1235
|
// On rotation, continue the same session with the next account instead
|
|
877
1236
|
// of restarting the whole prompt — the session files are shared.
|
|
878
|
-
const
|
|
879
|
-
? ['
|
|
880
|
-
:
|
|
1237
|
+
const memoryArgs = useResume
|
|
1238
|
+
? withMemoryPrompt(['resume', '--last', '--', RESUME_PROMPT], name, meta)
|
|
1239
|
+
: withMemoryPrompt(rest, name, meta);
|
|
1240
|
+
const displayArgs = compact ? outputFileArg(memoryArgs, lastMessageFile) : memoryArgs;
|
|
1241
|
+
const codexArgs = ['exec', ...buildExecArgs(displayArgs, meta)];
|
|
881
1242
|
const startTs = Date.now();
|
|
882
|
-
const
|
|
1243
|
+
const stopProgress = compact
|
|
1244
|
+
? startCompactProgress(name, attempt + 1, useResume || sessionMode === 'continue' ? 'continued' : 'new')
|
|
1245
|
+
: () => {};
|
|
1246
|
+
const res = await runner.runCodex(name, codexArgs, { capture: true, silent: compact });
|
|
1247
|
+
stopProgress();
|
|
883
1248
|
runner.recordUsage(name, res.profile, startTs);
|
|
884
1249
|
if (res.code === 0) {
|
|
1250
|
+
runRecord.attempts.push({ account: name, status: 'done', durationMs: Date.now() - startTs });
|
|
885
1251
|
store.clearLimited(name);
|
|
886
|
-
console.error(ui.ok(`done as ${ui.bold(`"${name}"`)}`));
|
|
1252
|
+
if (!compact) console.error(ui.ok(`done as ${ui.bold(`"${name}"`)}`));
|
|
887
1253
|
store.logEvent('exec', `done as "${name}"${useResume ? ' (resumed session)' : ''}`);
|
|
888
1254
|
warnIfOverThreshold(name);
|
|
1255
|
+
finishHistory('done');
|
|
1256
|
+
if (compact) {
|
|
1257
|
+
const finalMessage = (() => {
|
|
1258
|
+
try { return fs.readFileSync(lastMessageFile, 'utf8'); } catch { return ''; }
|
|
1259
|
+
})();
|
|
1260
|
+
printCompactResult(finalMessage, runRecord);
|
|
1261
|
+
}
|
|
1262
|
+
cleanupResult();
|
|
1263
|
+
console.error(ui.info(ui.dim(`history ${runRecord.id} · view: codexswitch history show ${runRecord.id}`)));
|
|
889
1264
|
return 0;
|
|
890
1265
|
}
|
|
891
1266
|
if (runner.looksRateLimited(res.output, meta.limitPatterns)) {
|
|
1267
|
+
runRecord.attempts.push({ account: name, status: 'rate-limited', durationMs: Date.now() - startTs });
|
|
892
1268
|
const until = Date.now() + runner.limitCooldownMs(res.output, meta.cooldownMinutes);
|
|
893
1269
|
store.markLimited(name, until);
|
|
894
1270
|
if (allowResume && !useResume && runner.sessionTouchedSince(res.profile, startTs)) {
|
|
@@ -901,6 +1277,7 @@ async function cmdExec(args) {
|
|
|
901
1277
|
continue;
|
|
902
1278
|
}
|
|
903
1279
|
if (runner.looksAuthFailed(res.output)) {
|
|
1280
|
+
runRecord.attempts.push({ account: name, status: 'auth-failed', durationMs: Date.now() - startTs });
|
|
904
1281
|
// A revoked token won't heal by itself — take the account out of
|
|
905
1282
|
// rotation and keep the task going on the next one.
|
|
906
1283
|
setFlag(name, { disabled: true });
|
|
@@ -910,9 +1287,19 @@ async function cmdExec(args) {
|
|
|
910
1287
|
store.logEvent('auth', `"${name}" disabled (revoked login)`);
|
|
911
1288
|
continue;
|
|
912
1289
|
}
|
|
1290
|
+
runRecord.attempts.push({ account: name, status: `exit-${res.code}`, durationMs: Date.now() - startTs });
|
|
1291
|
+
finishHistory(`failed-${res.code}`);
|
|
1292
|
+
if (compact) {
|
|
1293
|
+
console.error(ui.fail('Codex failed'));
|
|
1294
|
+
console.error(res.output.trim().split(/\r?\n/).slice(-12).join('\n'));
|
|
1295
|
+
}
|
|
1296
|
+
cleanupResult();
|
|
913
1297
|
return res.code; // real failure, don't burn other accounts on it
|
|
914
1298
|
}
|
|
915
1299
|
console.error(ui.fail('all accounts are rate-limited, over threshold, or disabled'));
|
|
1300
|
+
finishHistory('exhausted');
|
|
1301
|
+
cleanupResult();
|
|
1302
|
+
console.error(ui.info(ui.dim(`history ${runRecord.id} · view: codexswitch history show ${runRecord.id}`)));
|
|
916
1303
|
return 2;
|
|
917
1304
|
}
|
|
918
1305
|
|
|
@@ -920,21 +1307,32 @@ async function cmdExec(args) {
|
|
|
920
1307
|
// Every turn goes through the rotation engine, and turns 2+ resume the same
|
|
921
1308
|
// codex session — so the conversation continues even when the account under
|
|
922
1309
|
// it changes between (or during) turns.
|
|
923
|
-
async function cmdChat() {
|
|
1310
|
+
async function cmdChat({ welcome = true } = {}) {
|
|
924
1311
|
const readline = require('readline');
|
|
925
1312
|
runner.assertCodexAvailable();
|
|
926
1313
|
if (store.listAccounts().length === 0) {
|
|
927
1314
|
throw new Error('no accounts — add one with "codexswitch login"');
|
|
928
1315
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1316
|
+
let welcomePrompt = null;
|
|
1317
|
+
if (welcome) {
|
|
1318
|
+
const landing = await require('./welcome.js').readWelcomePrompt();
|
|
1319
|
+
if (landing.shown && landing.prompt == null) return 0;
|
|
1320
|
+
welcomePrompt = landing.prompt;
|
|
1321
|
+
}
|
|
1322
|
+
const chatStatus = () => {
|
|
1323
|
+
const currentMeta = store.loadMeta();
|
|
1324
|
+
const active = store.listAccounts().find((a) => a.active);
|
|
1325
|
+
const next = store.pickAccount();
|
|
1326
|
+
return `model ${currentMeta.model || 'default'} · reasoning ${currentMeta.reasoning || 'show'} · output ${currentMeta.outputMode || 'auto'} · memory ${currentMeta.memoryMode || 'off'} · account ${(active && active.name) || (next && next.name) || 'none'}`;
|
|
1327
|
+
};
|
|
1328
|
+
out(`${ui.bold('codexswitch chat')} ${ui.dim(`· ${chatStatus()}`)}`);
|
|
1329
|
+
out(ui.dim('Enter sends · /paste starts multiline input · /help lists commands\n'));
|
|
932
1330
|
|
|
933
1331
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
934
1332
|
rl.setPrompt(ui.enabled ? `${ui.cyan('cxs')} ${ui.dim('›')} ` : 'cxs › ');
|
|
935
1333
|
// Buffer lines ourselves: input can arrive while a turn is still running
|
|
936
1334
|
// (type-ahead, piped input) and must not be dropped or hit a closed rl.
|
|
937
|
-
const pending = [];
|
|
1335
|
+
const pending = welcomePrompt ? [welcomePrompt] : [];
|
|
938
1336
|
const waiters = [];
|
|
939
1337
|
let closed = false;
|
|
940
1338
|
rl.on('line', (l) => {
|
|
@@ -954,6 +1352,7 @@ async function cmdChat() {
|
|
|
954
1352
|
};
|
|
955
1353
|
|
|
956
1354
|
let inSession = false;
|
|
1355
|
+
let turn = 0;
|
|
957
1356
|
for (;;) {
|
|
958
1357
|
const raw = await ask();
|
|
959
1358
|
if (raw == null) break; // EOF (Ctrl-D or piped input ended)
|
|
@@ -965,7 +1364,9 @@ async function cmdChat() {
|
|
|
965
1364
|
try {
|
|
966
1365
|
if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') break;
|
|
967
1366
|
else if (cmd === 'help')
|
|
968
|
-
out(ui.dim('/usage /list /use <name> /next /model [m] /sandbox [mode] /reasoning [mode] /
|
|
1367
|
+
out(ui.dim('/status /history [id] /usage /list /use <name> /next /model [m] /sandbox [mode] /reasoning [mode] /output [mode] /memory [command] /paste /new /quit'));
|
|
1368
|
+
else if (cmd === 'status') out(ui.info(chatStatus()));
|
|
1369
|
+
else if (cmd === 'history') cmdHistory(rest.length ? ['show', rest[0]] : []);
|
|
969
1370
|
else if (cmd === 'usage') cmdUsage([]);
|
|
970
1371
|
else if (cmd === 'list') cmdList();
|
|
971
1372
|
else if (cmd === 'use') cmdUse(rest);
|
|
@@ -973,9 +1374,33 @@ async function cmdChat() {
|
|
|
973
1374
|
else if (cmd === 'model') cmdModel(rest);
|
|
974
1375
|
else if (cmd === 'sandbox') cmdSandbox(rest);
|
|
975
1376
|
else if (cmd === 'reasoning') cmdReasoning(rest);
|
|
1377
|
+
else if (cmd === 'output') cmdOutput(rest);
|
|
1378
|
+
else if (cmd === 'memory') cmdMemory(rest);
|
|
976
1379
|
else if (cmd === 'new') {
|
|
977
1380
|
inSession = false;
|
|
1381
|
+
turn = 0;
|
|
978
1382
|
out(ui.ok('starting a fresh session on the next prompt'));
|
|
1383
|
+
} else if (cmd === 'paste' || cmd === 'multiline') {
|
|
1384
|
+
out(ui.info('multiline input · finish with /end on its own line · cancel with /cancel'));
|
|
1385
|
+
const parts = [];
|
|
1386
|
+
for (;;) {
|
|
1387
|
+
const pasted = await ask();
|
|
1388
|
+
if (pasted == null || pasted.trim() === '/cancel') {
|
|
1389
|
+
parts.length = 0;
|
|
1390
|
+
out(ui.warn('multiline input cancelled'));
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
if (pasted.trim() === '/end') break;
|
|
1394
|
+
parts.push(pasted);
|
|
1395
|
+
}
|
|
1396
|
+
if (parts.length > 0) {
|
|
1397
|
+
const prompt = parts.join('\n');
|
|
1398
|
+
turn++;
|
|
1399
|
+
out(ui.info(`turn ${turn} · ${inSession ? 'continuing session' : 'new session'} · ${prompt.split('\n').length} lines`));
|
|
1400
|
+
const code = await cmdExec(inSession ? ['resume', '--last', '--', prompt] : ['--', prompt]);
|
|
1401
|
+
if (code === 0) inSession = true;
|
|
1402
|
+
else if (code === 2) out(ui.fail('all accounts exhausted — try again later or /use a specific account'));
|
|
1403
|
+
}
|
|
979
1404
|
} else out(ui.warn(`unknown command /${cmd} — try /help`));
|
|
980
1405
|
} catch (e) {
|
|
981
1406
|
out(ui.fail(e.message));
|
|
@@ -985,11 +1410,14 @@ async function cmdChat() {
|
|
|
985
1410
|
|
|
986
1411
|
// "--" marks the prompt as positional so lines starting with "-" are
|
|
987
1412
|
// never parsed as codex options.
|
|
1413
|
+
turn++;
|
|
1414
|
+
out(ui.info(`turn ${turn} · ${inSession ? 'continuing session' : 'new session'}`));
|
|
988
1415
|
const code = await cmdExec(inSession ? ['resume', '--last', '--', line] : ['--', line]);
|
|
989
1416
|
if (code === 0) inSession = true;
|
|
990
1417
|
else if (code === 2) out(ui.fail('all accounts exhausted — try again later or /use a specific account'));
|
|
991
1418
|
}
|
|
992
1419
|
rl.close();
|
|
1420
|
+
out(ui.dim(`\nsession ended · ${turn} turn${turn === 1 ? '' : 's'}${turn > 0 ? ' · review with "codexswitch history"' : ''}`));
|
|
993
1421
|
return 0;
|
|
994
1422
|
}
|
|
995
1423
|
|
|
@@ -1010,6 +1438,11 @@ async function main(argv) {
|
|
|
1010
1438
|
const [cmd, ...args] = argv;
|
|
1011
1439
|
switch (cmd) {
|
|
1012
1440
|
case undefined:
|
|
1441
|
+
if (store.listAccounts().length === 0) {
|
|
1442
|
+
printEmptyAccounts();
|
|
1443
|
+
return 0;
|
|
1444
|
+
}
|
|
1445
|
+
return cmdChat();
|
|
1013
1446
|
case 'help':
|
|
1014
1447
|
case '--help':
|
|
1015
1448
|
case '-h':
|
|
@@ -1039,6 +1472,9 @@ async function main(argv) {
|
|
|
1039
1472
|
case 'log':
|
|
1040
1473
|
case 'activity':
|
|
1041
1474
|
return cmdLog(args), 0;
|
|
1475
|
+
case 'history':
|
|
1476
|
+
case 'work':
|
|
1477
|
+
return cmdHistory(args), 0;
|
|
1042
1478
|
case 'use':
|
|
1043
1479
|
return cmdUse(args), 0;
|
|
1044
1480
|
case 'current':
|
|
@@ -1067,8 +1503,12 @@ async function main(argv) {
|
|
|
1067
1503
|
return cmdModel(args), 0;
|
|
1068
1504
|
case 'reasoning':
|
|
1069
1505
|
return cmdReasoning(args), 0;
|
|
1506
|
+
case 'output':
|
|
1507
|
+
return cmdOutput(args), 0;
|
|
1070
1508
|
case 'sandbox':
|
|
1071
1509
|
return cmdSandbox(args), 0;
|
|
1510
|
+
case 'memory':
|
|
1511
|
+
return cmdMemory(args), 0;
|
|
1072
1512
|
case 'add-key':
|
|
1073
1513
|
case 'apikey':
|
|
1074
1514
|
return cmdAddKey(args), 0;
|
|
@@ -1100,7 +1540,7 @@ async function main(argv) {
|
|
|
1100
1540
|
return cmdExec(args);
|
|
1101
1541
|
case 'chat':
|
|
1102
1542
|
case 'repl':
|
|
1103
|
-
return cmdChat();
|
|
1543
|
+
return cmdChat({ welcome: !args.includes('--simple') });
|
|
1104
1544
|
case 'server':
|
|
1105
1545
|
case 'proxy':
|
|
1106
1546
|
return cmdServer(args);
|