@zergai/ztc 0.2.6-dev.36101352252 → 0.2.7-dev.36236183737
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 +43 -17
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +19 -5
- package/dist/agent/agent.d.ts +5 -1
- package/dist/agent/agent.js +51 -8
- package/dist/agent/backends/account_pool.d.ts +5 -6
- package/dist/agent/backends/account_pool.js +39 -14
- package/dist/agent/backends/account_rotation.d.ts +3 -0
- package/dist/agent/backends/account_rotation.js +14 -3
- package/dist/agent/backends/codex_account_status.d.ts +42 -0
- package/dist/agent/backends/codex_account_status.js +196 -0
- package/dist/agent/backends/codex_app_server.d.ts +12 -2
- package/dist/agent/backends/codex_app_server.js +66 -6
- package/dist/agent/backends/index.d.ts +2 -0
- package/dist/agent/backends/index.js +13 -5
- package/dist/agent/backends/openai.d.ts +2 -2
- package/dist/agent/backends/openai.js +4 -3
- package/dist/agent/backends/openai_compatible.js +10 -10
- package/dist/agent/backends/openai_responses.d.ts +17 -5
- package/dist/agent/backends/openai_responses.js +2 -4
- package/dist/agent/backends/token_usage.js +1 -1
- package/dist/agent/backends/types.d.ts +3 -0
- package/dist/agent/commands/accounts.d.ts +7 -1
- package/dist/agent/commands/accounts.js +299 -101
- package/dist/agent/commands/config.js +11 -3
- package/dist/agent/commands/config_profile_switch.d.ts +2 -0
- package/dist/agent/commands/config_profile_switch.js +22 -0
- package/dist/agent/commands/context.js +4 -3
- package/dist/agent/commands/index.js +1 -2
- package/dist/agent/commands/input_mode.js +1 -1
- package/dist/agent/commands/model.js +7 -2
- package/dist/agent/commands/plan.d.ts +12 -0
- package/dist/agent/commands/plan.js +39 -28
- package/dist/agent/commands/profile.d.ts +1 -1
- package/dist/agent/commands/profile.js +34 -257
- package/dist/agent/commands/provider_choices.d.ts +12 -1
- package/dist/agent/commands/provider_choices.js +29 -2
- package/dist/agent/commands/provider_selection.d.ts +3 -0
- package/dist/agent/commands/provider_selection.js +63 -0
- package/dist/agent/commands/settings.d.ts +2 -0
- package/dist/agent/commands/settings.js +655 -50
- package/dist/agent/commands/types.d.ts +19 -2
- package/dist/agent/tools/ask_user.js +2 -2
- package/dist/app/agent_runner.d.ts +1 -1
- package/dist/app/agent_runner.js +2 -2
- package/dist/app/ask_user_response.d.ts +3 -0
- package/dist/app/ask_user_response.js +16 -0
- package/dist/app/command_context.js +29 -13
- package/dist/app/controller.d.ts +17 -2
- package/dist/app/controller.js +24 -1
- package/dist/app/selection_completion.d.ts +9 -3
- package/dist/app/selection_completion.js +54 -22
- package/dist/cli_args.js +10 -1
- package/dist/cli_main.js +85 -2
- package/dist/cli_ratatui.d.ts +22 -4
- package/dist/cli_ratatui.js +421 -146
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +20 -0
- package/dist/config/profiles.d.ts +9 -0
- package/dist/config/profiles.js +87 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +283 -40
- package/dist/headless.js +14 -0
- package/dist/panes/model_catalog.d.ts +1 -1
- package/dist/panes/model_catalog.js +24 -2
- package/dist/panes/ztc_launch.js +6 -0
- package/dist/router_auth.js +3 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +3 -0
- package/dist/types.d.ts +23 -2
- package/dist/ui/views/app.d.ts +3 -1
- package/dist/ui/views/app.js +3 -2
- package/dist/ui/views/completion_menu.js +6 -6
- package/dist/ui/views/input_area.d.ts +2 -1
- package/dist/ui/views/input_area.js +9 -8
- package/dist/ui/views/status_bar.d.ts +2 -0
- package/dist/ui/views/status_bar.js +15 -4
- package/dist/ui/widgets/color_panel.d.ts +9 -0
- package/dist/ui/widgets/color_panel.js +41 -26
- package/dist/ui/widgets/navigator_panel.d.ts +9 -0
- package/dist/ui/widgets/navigator_panel.js +17 -16
- package/dist/ui/widgets/panel_frame.d.ts +83 -3
- package/dist/ui/widgets/panel_frame.js +107 -9
- package/dist/ui/widgets/questionnaire.d.ts +9 -0
- package/dist/ui/widgets/questionnaire.js +214 -16
- package/dist/ui/widgets/resume_panel.d.ts +9 -0
- package/dist/ui/widgets/resume_panel.js +18 -17
- package/dist/ui/widgets/rewind_panel.d.ts +9 -0
- package/dist/ui/widgets/rewind_panel.js +15 -16
- package/dist/ui/widgets/select.d.ts +6 -0
- package/dist/ui/widgets/select.js +47 -12
- package/dist/ui/widgets/settings_panel.d.ts +75 -0
- package/dist/ui/widgets/settings_panel.js +197 -39
- package/dist/ui/widgets/status_line_panel.d.ts +9 -0
- package/dist/ui/widgets/status_line_panel.js +18 -19
- package/dist/ui/widgets/transcript_preview.d.ts +8 -0
- package/dist/ui/widgets/transcript_preview.js +10 -0
- package/dist/utils/context.js +2 -0
- package/dist/utils/fireworks.d.ts +20 -0
- package/dist/utils/fireworks.js +23 -0
- package/dist/utils/models.d.ts +2 -1
- package/dist/utils/models.js +19 -0
- package/dist/web/accounts_mirror.d.ts +7 -0
- package/dist/web/accounts_mirror.js +59 -7
- package/dist/web/mirror_stream.js +19 -5
- package/dist/zlm-embed.mjs +5734 -5362
- package/package.json +2 -2
- package/dist/agent/commands/provider.d.ts +0 -2
- package/dist/agent/commands/provider.js +0 -75
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* accounts. ztc stores only ids/labels/paths; tokens live in each account's own
|
|
4
4
|
* credential dir, written by the official CLI's own login. Subcommands:
|
|
5
5
|
*
|
|
6
|
-
* /accounts list
|
|
6
|
+
* /accounts open the account manager (or list in plain output)
|
|
7
7
|
* /accounts add <provider> <id> [label…] [--path <dir>]
|
|
8
8
|
* /accounts use <id> make an account the live one for its provider
|
|
9
9
|
* /accounts login <id> run the official CLI login at the account's dir
|
|
10
10
|
* /accounts remove <id> drop an account from the pool
|
|
11
11
|
*/
|
|
12
12
|
import { Command } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* The account list is written into the transcript, which is saved and mirrored
|
|
15
|
+
* to every viewer, so the ChatGPT email appears masked: enough to tell two
|
|
16
|
+
* logins apart, not enough to publish the address.
|
|
17
|
+
*/
|
|
18
|
+
export declare function maskAccountEmail(email: string | undefined): string | undefined;
|
|
13
19
|
export declare const accountsCommand: Command;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* accounts. ztc stores only ids/labels/paths; tokens live in each account's own
|
|
4
4
|
* credential dir, written by the official CLI's own login. Subcommands:
|
|
5
5
|
*
|
|
6
|
-
* /accounts list
|
|
6
|
+
* /accounts open the account manager (or list in plain output)
|
|
7
7
|
* /accounts add <provider> <id> [label…] [--path <dir>]
|
|
8
8
|
* /accounts use <id> make an account the live one for its provider
|
|
9
9
|
* /accounts login <id> run the official CLI login at the account's dir
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
13
|
import { configStore } from '../../config.js';
|
|
14
|
-
import { buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, isAccountProvider, probeAccountUsable, sourceDir, } from '../backends/account_pool.js';
|
|
14
|
+
import { buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, findCodexHomeCollision, isAccountProvider, probeAccountUsable, sourceDir, } from '../backends/account_pool.js';
|
|
15
15
|
import { loginAutoRunEnabled, loginFailureReasonText, runLoginCommand } from '../backends/account_login.js';
|
|
16
16
|
import { notifyAccountsChanged } from '../backends/account_events.js';
|
|
17
|
+
import { codexStatusCache } from '../backends/codex_account_status.js';
|
|
17
18
|
import { runKimiDeviceLogin } from '../backends/kimi_code_auth.js';
|
|
18
19
|
const PROVIDER_LABELS = {
|
|
19
20
|
claude_code: 'Claude Code (claude CLI)',
|
|
@@ -28,7 +29,8 @@ function say(ctx, content) {
|
|
|
28
29
|
function usage() {
|
|
29
30
|
return [
|
|
30
31
|
'Usage:',
|
|
31
|
-
' /accounts
|
|
32
|
+
' /accounts open account manager',
|
|
33
|
+
' /accounts list print account status',
|
|
32
34
|
' /accounts add <provider> <id> [label…] [--path <dir>]',
|
|
33
35
|
' /accounts use <id> switch the live account for its provider',
|
|
34
36
|
' /accounts login <id> sign in an account (official CLI or device flow)',
|
|
@@ -38,7 +40,38 @@ function usage() {
|
|
|
38
40
|
`Providers: ${Object.keys(PROVIDER_LABELS).join(', ')}`,
|
|
39
41
|
].join('\n');
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The account list is written into the transcript, which is saved and mirrored
|
|
45
|
+
* to every viewer, so the ChatGPT email appears masked: enough to tell two
|
|
46
|
+
* logins apart, not enough to publish the address.
|
|
47
|
+
*/
|
|
48
|
+
export function maskAccountEmail(email) {
|
|
49
|
+
if (!email)
|
|
50
|
+
return undefined;
|
|
51
|
+
const at = email.lastIndexOf('@');
|
|
52
|
+
if (at <= 0)
|
|
53
|
+
return '***';
|
|
54
|
+
return `${email[0]}***${email.slice(at)}`;
|
|
55
|
+
}
|
|
56
|
+
async function accountStatusLabel(account) {
|
|
57
|
+
if (!probeAccountUsable(account))
|
|
58
|
+
return 'needs login';
|
|
59
|
+
if (account.source.kind !== 'codex_home')
|
|
60
|
+
return 'ready';
|
|
61
|
+
const status = await codexStatusCache.read(account.source.path);
|
|
62
|
+
if (status.state === 'reauth_required')
|
|
63
|
+
return 'needs login';
|
|
64
|
+
const windows = status.windows.map((window) => `${window.name} ${Math.round(window.remainingPercent)}% left`).join(', ');
|
|
65
|
+
const tokens = status.usage?.lifetimeTokens === undefined
|
|
66
|
+
? '' : `${status.usage.lifetimeTokens.toLocaleString()} lifetime tokens${status.usageStale ? ' (old)' : ''}`;
|
|
67
|
+
if (status.state === 'unavailable')
|
|
68
|
+
return windows ? `account unverified; last observed ${windows}` : 'account unverified';
|
|
69
|
+
const email = maskAccountEmail(status.email);
|
|
70
|
+
if (status.state === 'quota_unavailable')
|
|
71
|
+
return [email, 'quota stale', windows, tokens].filter(Boolean).join('; ');
|
|
72
|
+
return [email, windows, tokens].filter(Boolean).join('; ') || 'ready';
|
|
73
|
+
}
|
|
74
|
+
async function renderList(ctx) {
|
|
42
75
|
const accounts = configStore.listAccounts();
|
|
43
76
|
if (accounts.length === 0) {
|
|
44
77
|
say(ctx, `No accounts configured.\n\n${usage()}`);
|
|
@@ -57,13 +90,13 @@ function renderList(ctx) {
|
|
|
57
90
|
lines.push('', PROVIDER_LABELS[key] ?? key);
|
|
58
91
|
for (const account of group) {
|
|
59
92
|
const marker = active?.id === account.id ? '*' : ' ';
|
|
60
|
-
const usable =
|
|
93
|
+
const usable = await accountStatusLabel(account);
|
|
61
94
|
lines.push(` ${marker} ${account.id} — ${account.label} [${usable}]`);
|
|
62
95
|
lines.push(` ${account.source.kind}: ${account.source.path}`);
|
|
63
96
|
}
|
|
64
97
|
}
|
|
65
98
|
const rotation = configStore.getRotation();
|
|
66
|
-
lines.push('', `* = live account for that provider. Auto-rotate: ${rotation.enabled ? `ON (${rotation.mode})` : 'off'}
|
|
99
|
+
lines.push('', `* = live account for that provider. Auto-rotate (Claude/Kimi only): ${rotation.enabled ? `ON (${rotation.mode})` : 'off'}. Codex switches only when you choose an account.`);
|
|
67
100
|
say(ctx, lines.join('\n'));
|
|
68
101
|
}
|
|
69
102
|
function parseAdd(args) {
|
|
@@ -88,29 +121,41 @@ function parseAdd(args) {
|
|
|
88
121
|
}
|
|
89
122
|
return { provider, id, label: labelParts.join(' ') || id, path };
|
|
90
123
|
}
|
|
91
|
-
async function
|
|
92
|
-
const parsed = parseAdd(args);
|
|
93
|
-
if (typeof parsed === 'string') {
|
|
94
|
-
say(ctx, parsed);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
124
|
+
async function addAccount(parsed) {
|
|
97
125
|
if (configStore.getAccount(parsed.id)) {
|
|
98
|
-
|
|
99
|
-
return;
|
|
126
|
+
return { error: 'duplicate_id', message: `An account with id "${parsed.id}" already exists. Remove it first or pick another id.` };
|
|
100
127
|
}
|
|
101
128
|
const dir = parsed.path ?? defaultAccountDir(parsed.provider, parsed.id);
|
|
102
129
|
const source = defaultSourceFor(parsed.provider, dir);
|
|
130
|
+
const collision = findCodexHomeCollision(configStore.listAccounts(), source);
|
|
131
|
+
if (collision) {
|
|
132
|
+
return { error: 'codex_home_collision', message: `That Codex credential home is already used by "${collision.label}". Choose another path.` };
|
|
133
|
+
}
|
|
103
134
|
ensureAccountDir(source);
|
|
104
135
|
const account = { id: parsed.id, label: parsed.label, provider: parsed.provider, source };
|
|
105
136
|
configStore.addAccount(account);
|
|
106
137
|
await configStore.save();
|
|
138
|
+
return { account };
|
|
139
|
+
}
|
|
140
|
+
async function handleAdd(args, ctx) {
|
|
141
|
+
const parsed = parseAdd(args);
|
|
142
|
+
if (typeof parsed === 'string') {
|
|
143
|
+
say(ctx, parsed);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const result = await addAccount(parsed);
|
|
147
|
+
if ('error' in result) {
|
|
148
|
+
say(ctx, result.message);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const { account } = result;
|
|
107
152
|
const ready = probeAccountUsable(account);
|
|
108
153
|
const next = ready
|
|
109
154
|
? `Already has credentials. Use it with: /accounts use ${account.id}`
|
|
110
155
|
: `Sign in with: /accounts login ${account.id}`;
|
|
111
156
|
say(ctx, [
|
|
112
157
|
`Added "${account.label}" (${account.id}) for ${PROVIDER_LABELS[account.provider]}.`,
|
|
113
|
-
` ${source.kind}: ${source.path}`,
|
|
158
|
+
` ${account.source.kind}: ${account.source.path}`,
|
|
114
159
|
next,
|
|
115
160
|
].join('\n'));
|
|
116
161
|
}
|
|
@@ -125,6 +170,15 @@ async function handleUse(args, ctx) {
|
|
|
125
170
|
say(ctx, `No account "${id}". See /accounts.`);
|
|
126
171
|
return;
|
|
127
172
|
}
|
|
173
|
+
if (account.source.kind === 'codex_home') {
|
|
174
|
+
const status = await codexStatusCache.read(account.source.path, true);
|
|
175
|
+
if (status.state === 'reauth_required' || status.state === 'unavailable') {
|
|
176
|
+
say(ctx, status.state === 'reauth_required'
|
|
177
|
+
? `"${account.label}" needs a fresh ChatGPT sign-in. Run /accounts login ${account.id}, then switch again.`
|
|
178
|
+
: `"${account.label}" could not be verified. Refresh its status or sign in, then switch again.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
128
182
|
configStore.setActiveAccount(account.provider, account.id);
|
|
129
183
|
await configStore.save();
|
|
130
184
|
const accountKey = canonicalAccountProvider(account.provider);
|
|
@@ -138,7 +192,7 @@ async function handleUse(args, ctx) {
|
|
|
138
192
|
say(ctx, `Switched live ${PROVIDER_LABELS[account.provider]} account to "${account.label}".`);
|
|
139
193
|
}
|
|
140
194
|
else {
|
|
141
|
-
say(ctx, `Set "${account.label}" active for ${PROVIDER_LABELS[account.provider]}. It goes live when you switch to that provider
|
|
195
|
+
say(ctx, `Set "${account.label}" active for ${PROVIDER_LABELS[account.provider]}. It goes live when you switch to that provider with /model.`);
|
|
142
196
|
}
|
|
143
197
|
}
|
|
144
198
|
async function handleLogin(args, ctx) {
|
|
@@ -216,6 +270,15 @@ async function handleLogin(args, ctx) {
|
|
|
216
270
|
say(ctx, `Signing in "${account.label}" on the ztc host — complete it in the browser…`);
|
|
217
271
|
const attempt = await runLoginCommand(login, () => probeAccountUsable(account));
|
|
218
272
|
if (attempt.outcome === 'succeeded') {
|
|
273
|
+
if (account.source.kind === 'codex_home') {
|
|
274
|
+
codexStatusCache.invalidate(account.source.path);
|
|
275
|
+
const status = await codexStatusCache.read(account.source.path, true);
|
|
276
|
+
if (status.state === 'reauth_required' || status.state === 'unavailable') {
|
|
277
|
+
notifyAccountsChanged();
|
|
278
|
+
say(ctx, `"${account.label}" saved credentials, but the ChatGPT subscription could not be verified. Run /accounts login ${account.id} again.`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
219
282
|
notifyAccountsChanged();
|
|
220
283
|
say(ctx, `"${account.label}" is signed in. Make it live with: /accounts use ${account.id}`);
|
|
221
284
|
}
|
|
@@ -235,7 +298,12 @@ async function handleLogin(args, ctx) {
|
|
|
235
298
|
].join('\n'));
|
|
236
299
|
return;
|
|
237
300
|
}
|
|
238
|
-
|
|
301
|
+
let ready = probeAccountUsable(account);
|
|
302
|
+
if (ready && account.source.kind === 'codex_home') {
|
|
303
|
+
codexStatusCache.invalidate(account.source.path);
|
|
304
|
+
const status = await codexStatusCache.read(account.source.path, true);
|
|
305
|
+
ready = status.state === 'ready' || status.state === 'quota_unavailable';
|
|
306
|
+
}
|
|
239
307
|
say(ctx, ready
|
|
240
308
|
? `"${account.label}" is signed in. Make it live with: /accounts use ${account.id}`
|
|
241
309
|
: `Login exited without usable credentials in ${sourceDir(account.source)}. Try again: /accounts login ${account.id}`);
|
|
@@ -244,7 +312,8 @@ function describeRotation() {
|
|
|
244
312
|
const r = configStore.getRotation();
|
|
245
313
|
const hours = (ms) => `${Math.round((ms / 3_600_000) * 10) / 10}h`;
|
|
246
314
|
return [
|
|
247
|
-
`Auto-rotate: ${r.enabled ? 'ON' : 'off'}`,
|
|
315
|
+
`Auto-rotate (Claude/Kimi): ${r.enabled ? 'ON' : 'off'}`,
|
|
316
|
+
' Codex: manual selection only',
|
|
248
317
|
` mode: ${r.mode}`,
|
|
249
318
|
` subagents: ${r.subagentInheritance}`,
|
|
250
319
|
` cooldowns: default ${hours(r.defaultCooldownMs)}, weekly ${hours(r.weeklyCooldownMs)}`,
|
|
@@ -303,86 +372,212 @@ async function handleRemove(args, ctx) {
|
|
|
303
372
|
await configStore.save();
|
|
304
373
|
say(ctx, `Removed "${account.label}" (${id}) from the pool. Its credential dir was left in place:\n ${sourceDir(account.source)}`);
|
|
305
374
|
}
|
|
306
|
-
// --- interactive
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
375
|
+
// --- interactive terminal settings page (text list in non-interactive output) ---
|
|
376
|
+
function accountFamily(provider) {
|
|
377
|
+
if (provider.startsWith('claude_code'))
|
|
378
|
+
return 'Claude';
|
|
379
|
+
if (provider.startsWith('codex_'))
|
|
380
|
+
return 'Codex';
|
|
381
|
+
return 'Kimi';
|
|
382
|
+
}
|
|
383
|
+
function interactiveAddPage(ctx, provider) {
|
|
384
|
+
const selected = configStore.getProvider();
|
|
385
|
+
const draft = {
|
|
386
|
+
provider: provider ?? (isAccountProvider(selected) ? selected : 'claude_code'),
|
|
387
|
+
id: '', label: '', path: '',
|
|
388
|
+
};
|
|
389
|
+
const originalFamily = provider ? accountFamily(provider) : undefined;
|
|
390
|
+
const showAccounts = (family) => {
|
|
391
|
+
void runInteractive(ctx, family).catch((error) => say(ctx, `Could not open Accounts: ${String(error)}`));
|
|
392
|
+
};
|
|
393
|
+
const back = () => showAccounts(originalFamily);
|
|
394
|
+
let error = '';
|
|
395
|
+
let errorField;
|
|
396
|
+
let pending = false;
|
|
397
|
+
const showPage = (initialSetting) => {
|
|
398
|
+
const title = originalFamily ? `Add ${accountFamily(draft.provider)} account` : 'Add account';
|
|
399
|
+
ctx.settingsPanel?.showSettingsPanel({
|
|
400
|
+
title,
|
|
401
|
+
layout: 'page',
|
|
402
|
+
notice: error,
|
|
403
|
+
initialSetting,
|
|
404
|
+
categories: [
|
|
405
|
+
{
|
|
406
|
+
name: 'Account',
|
|
407
|
+
settings: [
|
|
408
|
+
{
|
|
409
|
+
name: 'provider', label: 'Provider', value: draft.provider, type: 'select',
|
|
410
|
+
options: Object.keys(PROVIDER_LABELS)
|
|
411
|
+
.map(id => ({ value: id, label: PROVIDER_LABELS[id] })),
|
|
412
|
+
description: 'Choose a provider for sign-in.',
|
|
413
|
+
},
|
|
414
|
+
{ name: 'id', label: 'Account ID', value: draft.id, type: 'text', placeholder: 'work',
|
|
415
|
+
formatValue: value => value || '(required)',
|
|
416
|
+
description: 'Unique name used to switch.' },
|
|
417
|
+
{ name: 'label', label: 'Label', value: draft.label, type: 'text', placeholder: 'Optional',
|
|
418
|
+
formatValue: value => value || '(uses ID)',
|
|
419
|
+
description: 'Optional name shown in Accounts.' },
|
|
420
|
+
{ name: 'path', label: 'Credential directory', value: draft.path, type: 'text', placeholder: 'Automatic',
|
|
421
|
+
formatValue: value => value || 'Automatic',
|
|
422
|
+
description: 'Blank creates an isolated profile.' },
|
|
423
|
+
{
|
|
424
|
+
name: 'create', label: 'Create account', value: '', type: 'text',
|
|
425
|
+
formatValue: () => '↵',
|
|
426
|
+
description: 'Save now; sign in from Accounts.',
|
|
427
|
+
onActivate: () => {
|
|
428
|
+
if (pending)
|
|
429
|
+
return;
|
|
430
|
+
const id = draft.id.trim();
|
|
431
|
+
if (!id) {
|
|
432
|
+
error = 'Account ID required';
|
|
433
|
+
errorField = 'id';
|
|
434
|
+
showPage('id');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
pending = true;
|
|
438
|
+
void addAccount({
|
|
439
|
+
provider: draft.provider,
|
|
440
|
+
id,
|
|
441
|
+
label: draft.label.trim() || id,
|
|
442
|
+
path: draft.path.trim() || undefined,
|
|
443
|
+
}).then(result => {
|
|
444
|
+
if ('error' in result) {
|
|
445
|
+
error = result.error === 'duplicate_id' ? 'Account ID already exists' : 'Credential directory in use';
|
|
446
|
+
errorField = result.error === 'duplicate_id' ? 'id' : 'path';
|
|
447
|
+
showPage(errorField);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
showAccounts(accountFamily(result.account.provider));
|
|
451
|
+
}
|
|
452
|
+
}).catch((cause) => {
|
|
453
|
+
error = `Could not add account: ${String(cause)}`;
|
|
454
|
+
errorField = 'create';
|
|
455
|
+
showPage('create');
|
|
456
|
+
}).finally(() => { pending = false; });
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{ name: 'back', label: 'Back to accounts', value: '', type: 'text',
|
|
460
|
+
formatValue: () => '↵', description: 'Return without saving.', onActivate: back },
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
onSettingChange: (name, value) => {
|
|
465
|
+
const changedProvider = name === 'provider' && isAccountProvider(value);
|
|
466
|
+
if (changedProvider) {
|
|
467
|
+
draft.provider = value;
|
|
468
|
+
}
|
|
469
|
+
else if (name === 'id')
|
|
470
|
+
draft.id = value;
|
|
471
|
+
else if (name === 'label')
|
|
472
|
+
draft.label = value;
|
|
473
|
+
else if (name === 'path')
|
|
474
|
+
draft.path = value;
|
|
475
|
+
const resolvedError = Boolean(error) && (name === errorField || (changedProvider && errorField === 'path'));
|
|
476
|
+
if (resolvedError) {
|
|
477
|
+
error = '';
|
|
478
|
+
errorField = undefined;
|
|
479
|
+
}
|
|
480
|
+
if (changedProvider)
|
|
481
|
+
showPage('id');
|
|
482
|
+
else if (resolvedError)
|
|
483
|
+
showPage(name);
|
|
318
484
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}, async (values) => {
|
|
324
|
-
if (!values.id) {
|
|
325
|
-
// Empty values map = cancelled (browser × or terminal ESC), or a submit
|
|
326
|
-
// with no id. Either way nothing is added; don't frame it as an error.
|
|
327
|
-
say(ctx, 'Add cancelled (no account id given).');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const args = [values.provider, values.id];
|
|
331
|
-
if (values.label)
|
|
332
|
-
args.push(...values.label.split(/\s+/));
|
|
333
|
-
if (values.path)
|
|
334
|
-
args.push('--path', values.path);
|
|
335
|
-
await handleAdd(args, ctx);
|
|
336
|
-
});
|
|
485
|
+
onClose: back,
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
showPage();
|
|
337
489
|
}
|
|
338
|
-
function runInteractive(ctx) {
|
|
490
|
+
async function runInteractive(ctx, focus) {
|
|
339
491
|
const accounts = configStore.listAccounts();
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
492
|
+
const families = [
|
|
493
|
+
{ name: 'Claude', provider: 'claude_code', matches: account => account.provider.startsWith('claude_code') },
|
|
494
|
+
{ name: 'Codex', provider: 'codex_app_server', matches: account => account.provider.startsWith('codex_') },
|
|
495
|
+
{ name: 'Kimi', provider: 'kimi_code', matches: account => account.provider === 'kimi_code' },
|
|
496
|
+
];
|
|
497
|
+
const categories = await Promise.all(families.map(async (family) => {
|
|
498
|
+
const group = accounts.filter(family.matches);
|
|
499
|
+
const statuses = await Promise.all(group.map(accountStatusLabel));
|
|
500
|
+
const refresh = () => { void runInteractive(ctx, family.name); };
|
|
501
|
+
const runAction = (action) => {
|
|
502
|
+
void action().catch((error) => say(ctx, `Account action failed: ${String(error)}`)).finally(refresh);
|
|
347
503
|
};
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
504
|
+
return {
|
|
505
|
+
name: family.name,
|
|
506
|
+
settings: [
|
|
507
|
+
{
|
|
508
|
+
name: `add:${family.name}`,
|
|
509
|
+
label: `+ Add ${family.name} account`,
|
|
510
|
+
value: '',
|
|
511
|
+
type: 'text',
|
|
512
|
+
description: 'Keep another account signed in for a quick manual switch.',
|
|
513
|
+
onActivate: () => interactiveAddPage(ctx, family.provider),
|
|
514
|
+
},
|
|
515
|
+
...(family.name === 'Codex' ? [{
|
|
516
|
+
name: 'refresh:Codex',
|
|
517
|
+
label: 'Refresh Codex status',
|
|
518
|
+
value: '',
|
|
519
|
+
type: 'text',
|
|
520
|
+
description: 'Check sign-in, quota, and token activity again for every Codex account.',
|
|
521
|
+
onActivate: () => runAction(async () => {
|
|
522
|
+
await Promise.all(group.filter(account => account.source.kind === 'codex_home')
|
|
523
|
+
.map(account => codexStatusCache.read(account.source.path, true)));
|
|
524
|
+
}),
|
|
525
|
+
}] : []),
|
|
526
|
+
...group.flatMap((account, index) => {
|
|
527
|
+
const active = configStore.getActiveAccount(account.provider)?.id === account.id;
|
|
528
|
+
const status = statuses[index];
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
name: `use:${account.id}`,
|
|
532
|
+
label: `${active ? '● ' : ''}${account.label}`,
|
|
533
|
+
value: `${active ? 'In use · ' : ''}${status}`,
|
|
534
|
+
type: 'text',
|
|
535
|
+
description: `${PROVIDER_LABELS[account.provider]} · ${account.id} · ${sourceDir(account.source)}. Enter to use this account.`,
|
|
536
|
+
onActivate: () => runAction(() => handleUse([account.id], ctx)),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: `login:${account.id}`,
|
|
540
|
+
label: `Sign in · ${account.label}`,
|
|
541
|
+
value: '',
|
|
542
|
+
type: 'text',
|
|
543
|
+
description: 'Refresh this account’s sign-in without signing out the others.',
|
|
544
|
+
onActivate: () => runAction(() => handleLogin([account.id], ctx)),
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: `remove:${account.id}`,
|
|
548
|
+
label: `Remove · ${account.label}`,
|
|
549
|
+
value: '',
|
|
550
|
+
type: 'text',
|
|
551
|
+
description: 'Forget this account. Its credential directory stays on disk.',
|
|
552
|
+
onActivate: () => ctx.settingsPanel?.showSettingsPanel({
|
|
553
|
+
title: `Remove ${account.label}?`,
|
|
554
|
+
categories: [{ name: 'Confirm removal', settings: [
|
|
555
|
+
{
|
|
556
|
+
name: 'keepAccount', label: 'Keep account', value: '', type: 'text',
|
|
557
|
+
onActivate: refresh,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'confirmRemoveAccount', label: `Remove ${account.label}`, value: '', type: 'text',
|
|
561
|
+
description: 'The credential directory stays on disk.',
|
|
562
|
+
onActivate: () => runAction(() => handleRemove([account.id], ctx)),
|
|
563
|
+
},
|
|
564
|
+
] }],
|
|
565
|
+
onSettingChange: () => { },
|
|
566
|
+
onClose: refresh,
|
|
567
|
+
}),
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
}),
|
|
367
571
|
],
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
handleLogin([id], ctx);
|
|
378
|
-
return;
|
|
379
|
-
case 'remove':
|
|
380
|
-
await handleRemove([id], ctx);
|
|
381
|
-
return;
|
|
382
|
-
default:
|
|
383
|
-
say(ctx, `Unknown action "${act}".`);
|
|
384
|
-
}
|
|
385
|
-
});
|
|
572
|
+
};
|
|
573
|
+
}));
|
|
574
|
+
const provider = configStore.getProvider();
|
|
575
|
+
const defaultFocus = provider.startsWith('codex_') ? 'Codex'
|
|
576
|
+
: provider.startsWith('claude_code') ? 'Claude'
|
|
577
|
+
: provider === 'kimi_code' ? 'Kimi' : undefined;
|
|
578
|
+
ctx.settingsPanel?.showSettingsPanel({
|
|
579
|
+
title: 'Accounts', categories, initialCategory: focus ?? defaultFocus,
|
|
580
|
+
onSettingChange: () => { },
|
|
386
581
|
});
|
|
387
582
|
}
|
|
388
583
|
export const accountsCommand = {
|
|
@@ -397,23 +592,26 @@ export const accountsCommand = {
|
|
|
397
592
|
// auto-starts in every interactive session, so `isRunning()` is almost
|
|
398
593
|
// always true — gating on it sent `/accounts` to the browser even when no
|
|
399
594
|
// browser was open, so the terminal showed nothing. `hasViewers()` is true
|
|
400
|
-
// only when a browser is really watching: then open
|
|
401
|
-
// otherwise
|
|
595
|
+
// only when a browser is really watching: then open Settings there;
|
|
596
|
+
// otherwise use the settings surface (terminal or parent mirror) or a text list.
|
|
402
597
|
if (ctx.mirror?.hasViewers?.()) {
|
|
403
|
-
ctx.mirror.publish?.({ type: '
|
|
598
|
+
ctx.mirror.publish?.({ type: 'settings_page', data: { title: 'Settings', filter: 'accounts' } });
|
|
404
599
|
}
|
|
405
|
-
else if (
|
|
406
|
-
runInteractive(ctx);
|
|
600
|
+
else if (ctx.settingsPanel) {
|
|
601
|
+
await runInteractive(ctx);
|
|
407
602
|
}
|
|
408
603
|
else {
|
|
409
|
-
renderList(ctx);
|
|
604
|
+
await renderList(ctx);
|
|
410
605
|
}
|
|
411
606
|
return;
|
|
412
607
|
case 'list':
|
|
413
|
-
renderList(ctx);
|
|
608
|
+
await renderList(ctx);
|
|
414
609
|
return;
|
|
415
610
|
case 'add':
|
|
416
|
-
|
|
611
|
+
if (rest.length === 0 && ctx.settingsPanel)
|
|
612
|
+
interactiveAddPage(ctx);
|
|
613
|
+
else
|
|
614
|
+
await handleAdd(rest, ctx);
|
|
417
615
|
return;
|
|
418
616
|
case 'use':
|
|
419
617
|
case 'switch':
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_SPINNER_VERBS, formatSpinnerVerbs, parseSpinnerVerbs } from '../../utils/spinner_verbs.js';
|
|
2
2
|
import { CODEX_REASONING_EFFORTS, } from '../../config/types.js';
|
|
3
3
|
import { runOpenAICompatibleWizard } from './openai_compat_wizard.js';
|
|
4
|
-
import { settingsCommand } from './settings.js';
|
|
4
|
+
import { openFactoryResetConfirmation, settingsCommand } from './settings.js';
|
|
5
5
|
import { contextProgressBarConfig, normalizeBarColor, publishContextProgressBar, withBarColor, } from './context_progress_bar.js';
|
|
6
6
|
import { compatEndpoint, DEFAULT_CONTEXT_PROGRESS_BAR } from '../../config.js';
|
|
7
7
|
import { parseGoalTurnCap } from './goal.js';
|
|
@@ -116,7 +116,7 @@ const STATUS_LINE_USAGE = [
|
|
|
116
116
|
export const configCommand = {
|
|
117
117
|
name: 'config',
|
|
118
118
|
description: 'Manage configuration',
|
|
119
|
-
usage: '[show|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
|
|
119
|
+
usage: '[show|reset|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
|
|
120
120
|
agentRunnable: true,
|
|
121
121
|
category: 'config',
|
|
122
122
|
handler: async (args, ctx) => {
|
|
@@ -139,6 +139,14 @@ export const configCommand = {
|
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
switch (subCmd) {
|
|
142
|
+
case 'reset': {
|
|
143
|
+
if (rest.length > 0) {
|
|
144
|
+
ctx.addMessage({ role: 'system', content: 'Usage: /config reset' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
openFactoryResetConfirmation(ctx);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
142
150
|
case 'scope': {
|
|
143
151
|
const current = ctx.config.getConfigScope?.() || 'global';
|
|
144
152
|
const requested = value.trim().toLowerCase();
|
|
@@ -498,7 +506,7 @@ Usage: /config zrouter-url <url|clear>` });
|
|
|
498
506
|
}
|
|
499
507
|
ctx.addMessage({
|
|
500
508
|
role: 'system',
|
|
501
|
-
content: `Models (${provider})\nCurrent model: ${currentModel}\n\nUsage: /model <model-name
|
|
509
|
+
content: `Models (${provider})\nCurrent model: ${currentModel}\n\nUsage: /model <model-name> or /model <provider> [model-name]${modelListSuffix}`
|
|
502
510
|
});
|
|
503
511
|
return;
|
|
504
512
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { activeProfile, builtinProfileEmulation, listConfigProfiles, profileConfigDir, validateProfileName } from '../../config/profiles.js';
|
|
3
|
+
export async function switchConfigProfile(ctx, requested, allowNew = false) {
|
|
4
|
+
const name = validateProfileName(requested.trim());
|
|
5
|
+
if (!ctx.switchConfigProfile) {
|
|
6
|
+
throw new Error('This renderer cannot switch config profiles in place.');
|
|
7
|
+
}
|
|
8
|
+
if (!allowNew && !(await listConfigProfiles()).includes(name)) {
|
|
9
|
+
throw new Error(`Config profile "${name}" does not exist. Use /profile create ${name} to start a new one.`);
|
|
10
|
+
}
|
|
11
|
+
if (allowNew && (name === 'default' || builtinProfileEmulation(name))) {
|
|
12
|
+
throw new Error(`Config profile "${name}" is built in. Use /profile ${name} to select it.`);
|
|
13
|
+
}
|
|
14
|
+
if (allowNew && name !== 'default')
|
|
15
|
+
await mkdir(profileConfigDir(name), { recursive: true, mode: 0o700 });
|
|
16
|
+
if (name === activeProfile()) {
|
|
17
|
+
ctx.addMessage({ role: 'system', content: `Already using config profile ${name}.` });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await ctx.switchConfigProfile(name);
|
|
21
|
+
ctx.addMessage({ role: 'system', content: `Switched to config profile ${name} in this session.` });
|
|
22
|
+
}
|
|
@@ -113,11 +113,12 @@ export const contextCommand = {
|
|
|
113
113
|
// 0% would read as a cold prefix when the truth is "no telemetry".
|
|
114
114
|
if (!estimated) {
|
|
115
115
|
const cached = ctx.getCachedInputTokens?.();
|
|
116
|
-
const
|
|
116
|
+
const cacheInput = ctx.getCacheInputTokens?.() ?? usedTokens;
|
|
117
|
+
const hitRate = cacheHitRate(cached, cacheInput);
|
|
117
118
|
if (hitRate !== undefined && cached !== undefined) {
|
|
118
119
|
lines.push('');
|
|
119
|
-
lines.push('Prompt cache (
|
|
120
|
-
lines.push(` ${hitRate.toFixed(0)}% served from cache (${formatTokens(cached)} of ${formatTokens(
|
|
120
|
+
lines.push('Prompt cache (last model call):');
|
|
121
|
+
lines.push(` ${hitRate.toFixed(0)}% served from cache (${formatTokens(cached)} of ${formatTokens(cacheInput)} input tokens)`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
// Approximate composition — estimate only, for orientation. The authoritative
|