@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.
Files changed (112) hide show
  1. package/README.md +43 -17
  2. package/bin/zmux-darwin-arm64 +0 -0
  3. package/bin/ztc-tui-darwin-arm64 +0 -0
  4. package/bin/zterm-darwin-arm64 +0 -0
  5. package/dist/App.js +19 -5
  6. package/dist/agent/agent.d.ts +5 -1
  7. package/dist/agent/agent.js +51 -8
  8. package/dist/agent/backends/account_pool.d.ts +5 -6
  9. package/dist/agent/backends/account_pool.js +39 -14
  10. package/dist/agent/backends/account_rotation.d.ts +3 -0
  11. package/dist/agent/backends/account_rotation.js +14 -3
  12. package/dist/agent/backends/codex_account_status.d.ts +42 -0
  13. package/dist/agent/backends/codex_account_status.js +196 -0
  14. package/dist/agent/backends/codex_app_server.d.ts +12 -2
  15. package/dist/agent/backends/codex_app_server.js +66 -6
  16. package/dist/agent/backends/index.d.ts +2 -0
  17. package/dist/agent/backends/index.js +13 -5
  18. package/dist/agent/backends/openai.d.ts +2 -2
  19. package/dist/agent/backends/openai.js +4 -3
  20. package/dist/agent/backends/openai_compatible.js +10 -10
  21. package/dist/agent/backends/openai_responses.d.ts +17 -5
  22. package/dist/agent/backends/openai_responses.js +2 -4
  23. package/dist/agent/backends/token_usage.js +1 -1
  24. package/dist/agent/backends/types.d.ts +3 -0
  25. package/dist/agent/commands/accounts.d.ts +7 -1
  26. package/dist/agent/commands/accounts.js +299 -101
  27. package/dist/agent/commands/config.js +11 -3
  28. package/dist/agent/commands/config_profile_switch.d.ts +2 -0
  29. package/dist/agent/commands/config_profile_switch.js +22 -0
  30. package/dist/agent/commands/context.js +4 -3
  31. package/dist/agent/commands/index.js +1 -2
  32. package/dist/agent/commands/input_mode.js +1 -1
  33. package/dist/agent/commands/model.js +7 -2
  34. package/dist/agent/commands/plan.d.ts +12 -0
  35. package/dist/agent/commands/plan.js +39 -28
  36. package/dist/agent/commands/profile.d.ts +1 -1
  37. package/dist/agent/commands/profile.js +34 -257
  38. package/dist/agent/commands/provider_choices.d.ts +12 -1
  39. package/dist/agent/commands/provider_choices.js +29 -2
  40. package/dist/agent/commands/provider_selection.d.ts +3 -0
  41. package/dist/agent/commands/provider_selection.js +63 -0
  42. package/dist/agent/commands/settings.d.ts +2 -0
  43. package/dist/agent/commands/settings.js +655 -50
  44. package/dist/agent/commands/types.d.ts +19 -2
  45. package/dist/agent/tools/ask_user.js +2 -2
  46. package/dist/app/agent_runner.d.ts +1 -1
  47. package/dist/app/agent_runner.js +2 -2
  48. package/dist/app/ask_user_response.d.ts +3 -0
  49. package/dist/app/ask_user_response.js +16 -0
  50. package/dist/app/command_context.js +29 -13
  51. package/dist/app/controller.d.ts +17 -2
  52. package/dist/app/controller.js +24 -1
  53. package/dist/app/selection_completion.d.ts +9 -3
  54. package/dist/app/selection_completion.js +54 -22
  55. package/dist/cli_args.js +10 -1
  56. package/dist/cli_main.js +85 -2
  57. package/dist/cli_ratatui.d.ts +22 -4
  58. package/dist/cli_ratatui.js +421 -146
  59. package/dist/config/defaults.d.ts +6 -0
  60. package/dist/config/defaults.js +20 -0
  61. package/dist/config/profiles.d.ts +9 -0
  62. package/dist/config/profiles.js +87 -0
  63. package/dist/config/types.d.ts +2 -0
  64. package/dist/config.d.ts +7 -0
  65. package/dist/config.js +283 -40
  66. package/dist/headless.js +14 -0
  67. package/dist/panes/model_catalog.d.ts +1 -1
  68. package/dist/panes/model_catalog.js +24 -2
  69. package/dist/panes/ztc_launch.js +6 -0
  70. package/dist/router_auth.js +3 -2
  71. package/dist/session.d.ts +2 -0
  72. package/dist/session.js +3 -0
  73. package/dist/types.d.ts +23 -2
  74. package/dist/ui/views/app.d.ts +3 -1
  75. package/dist/ui/views/app.js +3 -2
  76. package/dist/ui/views/completion_menu.js +6 -6
  77. package/dist/ui/views/input_area.d.ts +2 -1
  78. package/dist/ui/views/input_area.js +9 -8
  79. package/dist/ui/views/status_bar.d.ts +2 -0
  80. package/dist/ui/views/status_bar.js +15 -4
  81. package/dist/ui/widgets/color_panel.d.ts +9 -0
  82. package/dist/ui/widgets/color_panel.js +41 -26
  83. package/dist/ui/widgets/navigator_panel.d.ts +9 -0
  84. package/dist/ui/widgets/navigator_panel.js +17 -16
  85. package/dist/ui/widgets/panel_frame.d.ts +83 -3
  86. package/dist/ui/widgets/panel_frame.js +107 -9
  87. package/dist/ui/widgets/questionnaire.d.ts +9 -0
  88. package/dist/ui/widgets/questionnaire.js +214 -16
  89. package/dist/ui/widgets/resume_panel.d.ts +9 -0
  90. package/dist/ui/widgets/resume_panel.js +18 -17
  91. package/dist/ui/widgets/rewind_panel.d.ts +9 -0
  92. package/dist/ui/widgets/rewind_panel.js +15 -16
  93. package/dist/ui/widgets/select.d.ts +6 -0
  94. package/dist/ui/widgets/select.js +47 -12
  95. package/dist/ui/widgets/settings_panel.d.ts +75 -0
  96. package/dist/ui/widgets/settings_panel.js +197 -39
  97. package/dist/ui/widgets/status_line_panel.d.ts +9 -0
  98. package/dist/ui/widgets/status_line_panel.js +18 -19
  99. package/dist/ui/widgets/transcript_preview.d.ts +8 -0
  100. package/dist/ui/widgets/transcript_preview.js +10 -0
  101. package/dist/utils/context.js +2 -0
  102. package/dist/utils/fireworks.d.ts +20 -0
  103. package/dist/utils/fireworks.js +23 -0
  104. package/dist/utils/models.d.ts +2 -1
  105. package/dist/utils/models.js +19 -0
  106. package/dist/web/accounts_mirror.d.ts +7 -0
  107. package/dist/web/accounts_mirror.js +59 -7
  108. package/dist/web/mirror_stream.js +19 -5
  109. package/dist/zlm-embed.mjs +5734 -5362
  110. package/package.json +2 -2
  111. package/dist/agent/commands/provider.d.ts +0 -2
  112. package/dist/agent/commands/provider.js +0 -75
@@ -15,6 +15,12 @@ export declare const DEFAULT_CLARIFAI_MODEL = "https://clarifai.com/openai/chat-
15
15
  * (factory) and to display the active effort in the model picker.
16
16
  */
17
17
  export declare function resolveCodexEffort(config: ZTCConfig, provider?: string, model?: string): CodexReasoningEffort | undefined;
18
+ /**
19
+ * The highest-priority saved effort when the active model rejects it, so the
20
+ * UI can say the choice is not applied instead of silently falling back.
21
+ * Undefined when the top choice is supported or nothing is saved.
22
+ */
23
+ export declare function unsupportedCodexEffort(config: ZTCConfig, provider?: string, model?: string): CodexReasoningEffort | undefined;
18
24
  export declare function inferDefaultProvider(): string;
19
25
  export declare function getDefaultModelForProvider(provider: string): string;
20
26
  export declare function getDefaultApiKeyForProvider(provider: string): string | undefined;
@@ -4,6 +4,7 @@ import { KIMI_CODE_AUTH_SENTINEL, hasUsableKimiCodeAuth } from '../agent/backend
4
4
  import { CODEX_APP_SERVER_SENTINEL, CODEX_LEGACY_PROVIDER, isCodexAppServerProvider, isCodexLegacyProvider } from '../agent/backends/provider_ids.js';
5
5
  import { isCodexReasoningEffortSupported } from '../utils/models.js';
6
6
  import { DEEPSEEK_FLASH_MODEL, MODAL_DEEPSEEK_FLASH_MODEL } from '../utils/deepseek.js';
7
+ import { FIREWORKS_FUNDED_MODEL } from '../utils/fireworks.js';
7
8
  export const DEFAULT_OPENAI_MODEL = 'gpt-5.4-nano';
8
9
  export const DEFAULT_CODEX_MODEL = 'gpt-5.6-sol';
9
10
  export const DEFAULT_ANTHROPIC_MODEL = 'claude-opus-5';
@@ -31,6 +32,21 @@ export function resolveCodexEffort(config, provider, model) {
31
32
  }
32
33
  return undefined;
33
34
  }
35
+ /**
36
+ * The highest-priority saved effort when the active model rejects it, so the
37
+ * UI can say the choice is not applied instead of silently falling back.
38
+ * Undefined when the top choice is supported or nothing is saved.
39
+ */
40
+ export function unsupportedCodexEffort(config, provider, model) {
41
+ const chosen = [
42
+ model ? config.codexEffortByModel?.[model] : undefined,
43
+ provider ? config.codexEffortByProvider?.[provider] : undefined,
44
+ config.codexEffort,
45
+ ].find((candidate) => Boolean(candidate));
46
+ if (!chosen || isCodexReasoningEffortSupported(provider, model, chosen))
47
+ return undefined;
48
+ return chosen;
49
+ }
34
50
  function has_env_key(name) {
35
51
  const value = process.env[name];
36
52
  return typeof value === 'string' && value.length > 0;
@@ -44,6 +60,8 @@ export function inferDefaultProvider() {
44
60
  return 'gemini';
45
61
  if (has_env_key('KIMI_API_KEY') || has_env_key('MOONSHOT_API_KEY'))
46
62
  return 'kimi';
63
+ if (has_env_key('FIREWORKS_API_KEY'))
64
+ return 'fireworks';
47
65
  if (has_env_key('OPENAI_COMPAT_API_KEY'))
48
66
  return 'openai_compatible';
49
67
  if (has_env_key('INCEPTION_API_KEY'))
@@ -80,6 +98,8 @@ export function getDefaultModelForProvider(provider) {
80
98
  return DEFAULT_DEEPSEEK_MODEL;
81
99
  case 'modal':
82
100
  return MODAL_DEEPSEEK_FLASH_MODEL;
101
+ case 'fireworks':
102
+ return FIREWORKS_FUNDED_MODEL;
83
103
  case 'clarifai':
84
104
  return DEFAULT_CLARIFAI_MODEL;
85
105
  case 'openai':
@@ -0,0 +1,9 @@
1
+ export declare const BUILTIN_CONFIG_PROFILES: readonly ["ztc", "ztc-coder", "ztc-datascience", "scientist"];
2
+ export declare function builtinProfileEmulation(name: string): string | undefined;
3
+ export declare function validateProfileName(name: string): string;
4
+ export declare function activeProfile(): string;
5
+ export declare function profileConfigDir(profile?: string): string;
6
+ /** Profiles live beside the default config. Listing never creates or changes one. */
7
+ export declare function listConfigProfiles(): Promise<string[]>;
8
+ /** Replace a launch's original --profile with the profile selected in this process. */
9
+ export declare function withActiveProfileArg(argv: readonly string[]): string[];
@@ -0,0 +1,87 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { readdir } from 'node:fs/promises';
5
+ export const BUILTIN_CONFIG_PROFILES = ['ztc', 'ztc-coder', 'ztc-datascience', 'scientist'];
6
+ export function builtinProfileEmulation(name) {
7
+ const builtin = BUILTIN_CONFIG_PROFILES.find(profile => profile === name);
8
+ if (!builtin)
9
+ return undefined;
10
+ const root = process.env.ZTC_CONFIG_DIR || join(homedir(), '.ztc');
11
+ return existsSync(join(root, 'profiles', name)) ? undefined : builtin;
12
+ }
13
+ export function validateProfileName(name) {
14
+ if (name !== 'default' && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
15
+ throw new Error('Profile name must be 1–64 characters: letters, numbers, dots, underscores, or hyphens; it must start with a letter or number.');
16
+ }
17
+ if (name === '.' || name === '..') {
18
+ throw new Error('Profile name cannot be . or ..');
19
+ }
20
+ return name;
21
+ }
22
+ export function activeProfile() {
23
+ return validateProfileName(process.env.ZTC_PROFILE || 'default');
24
+ }
25
+ export function profileConfigDir(profile = activeProfile()) {
26
+ const root = process.env.ZTC_CONFIG_DIR || join(homedir(), '.ztc');
27
+ const name = validateProfileName(profile);
28
+ if (name === 'default')
29
+ return root;
30
+ const namedDir = join(root, 'profiles', name);
31
+ // Built-in presets share the default config and sign-ins. Preserve an older
32
+ // explicitly created directory with the same name instead of hiding its data.
33
+ return builtinProfileEmulation(name) ? root : namedDir;
34
+ }
35
+ /** Profiles live beside the default config. Listing never creates or changes one. */
36
+ export async function listConfigProfiles() {
37
+ const root = process.env.ZTC_CONFIG_DIR || join(homedir(), '.ztc');
38
+ let entries;
39
+ try {
40
+ entries = await readdir(join(root, 'profiles'), { withFileTypes: true });
41
+ }
42
+ catch (error) {
43
+ if (error.code === 'ENOENT') {
44
+ return [...BUILTIN_CONFIG_PROFILES, 'default', activeProfile()]
45
+ .filter((name, index, names) => names.indexOf(name) === index)
46
+ .sort(profileOrder);
47
+ }
48
+ throw error;
49
+ }
50
+ const named = entries.filter(entry => entry.isDirectory() && entry.name !== 'default')
51
+ .map(entry => entry.name)
52
+ .filter(name => {
53
+ try {
54
+ validateProfileName(name);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ });
61
+ const profiles = new Set([activeProfile(), ...BUILTIN_CONFIG_PROFILES, ...named]);
62
+ profiles.delete('default');
63
+ return ['default', ...profiles].sort(profileOrder);
64
+ }
65
+ function profileOrder(left, right) {
66
+ const rank = (name) => {
67
+ if (name === 'default')
68
+ return 0;
69
+ const index = BUILTIN_CONFIG_PROFILES.findIndex(profile => profile === name);
70
+ return index < 0 ? Number.MAX_SAFE_INTEGER : index + 1;
71
+ };
72
+ return rank(left) - rank(right) || left.localeCompare(right);
73
+ }
74
+ /** Replace a launch's original --profile with the profile selected in this process. */
75
+ export function withActiveProfileArg(argv) {
76
+ const delimiter = argv.indexOf('--');
77
+ const flags = delimiter < 0 ? argv : argv.slice(0, delimiter);
78
+ const retained = [];
79
+ for (let i = 0; i < flags.length; i++) {
80
+ if (flags[i] === '--profile') {
81
+ i += 1;
82
+ continue;
83
+ }
84
+ retained.push(flags[i]);
85
+ }
86
+ return [...retained, '--profile', activeProfile(), ...(delimiter < 0 ? [] : argv.slice(delimiter))];
87
+ }
@@ -297,6 +297,8 @@ export interface CompactionSettings {
297
297
  jev?: JevCompactionSettings;
298
298
  }
299
299
  export interface ZTCConfig {
300
+ /** Marks the one-time replacement of pre-preset settings in this profile. */
301
+ settingsDefaultsVersion?: number;
300
302
  /** Configurable terminal status line fields, ordering, separator, and colors. */
301
303
  statusLine?: import('../ui/status_line_config.js').StatusLineSettings;
302
304
  apiKey?: string;
package/dist/config.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare function build_default_config(): ZTCConfig;
12
12
  export declare function shouldRefreshSuperuserStatus(env?: NodeJS.ProcessEnv): boolean;
13
13
  export declare class ConfigStore {
14
14
  private config;
15
+ private profileEmulation;
15
16
  private legacyApiKeyFromEnvironment;
16
17
  private routerCredentials;
17
18
  private routerCredentialsError;
@@ -34,7 +35,11 @@ export declare class ConfigStore {
34
35
  private applyEnvOverrides;
35
36
  load(force?: boolean): Promise<ZTCConfig>;
36
37
  refresh(): Promise<void>;
38
+ /** Load a profile in this process; restore the old view if runtime activation fails. */
39
+ switchProfile(name: string, activate: () => void | Promise<void>): Promise<void>;
37
40
  save(): Promise<void>;
41
+ /** Clear the active profile's saved settings and reset this process's settings view. */
42
+ factoryReset(): Promise<void>;
38
43
  get(): ZTCConfig;
39
44
  /**
40
45
  * The configuration without this process's `--effort` override: the saved
@@ -105,6 +110,8 @@ export declare class ConfigStore {
105
110
  * per-model > per-provider > global. Defaults to the current provider/model.
106
111
  */
107
112
  getCodexEffort(provider?: string, model?: string): CodexReasoningEffort | undefined;
113
+ /** Saved effort the active (or given) model rejects; undefined when it applies. */
114
+ getUnsupportedCodexEffort(provider?: string, model?: string): CodexReasoningEffort | undefined;
108
115
  /** Effort chosen by a startup `--effort` flag for this process (= none when unset). */
109
116
  getCodexEffortOverride(): CodexReasoningEffort | undefined;
110
117
  /**
package/dist/config.js CHANGED
@@ -1,9 +1,10 @@
1
- import { homedir } from 'os';
2
1
  import { join } from 'path';
3
- import { readFile, writeFile, mkdir, chmod } from 'fs/promises';
2
+ import { readFile, writeFile, mkdir, chmod, unlink, rename, realpath } from 'fs/promises';
3
+ import { randomUUID } from 'crypto';
4
+ import { activeProfile, builtinProfileEmulation, profileConfigDir, validateProfileName } from './config/profiles.js';
4
5
  import { accountSourceToBackendOptions, canonicalAccountProvider, probeAccountIsolated, resolveActiveAccount, resolveRotation, } from './agent/backends/account_pool.js';
5
6
  import { notifyAccountsChanged } from './agent/backends/account_events.js';
6
- import { inferDefaultProvider, getDefaultApiKeyForProvider, getDefaultModelForProvider, resolveCodexEffort } from './config/defaults.js';
7
+ import { inferDefaultProvider, getDefaultApiKeyForProvider, getDefaultModelForProvider, resolveCodexEffort, unsupportedCodexEffort } from './config/defaults.js';
7
8
  import { resolveRepoModelOverride } from './config/repo_config.js';
8
9
  import { CODEX_AUTH_SENTINEL, hasUsableCodexAuth } from './agent/backends/codex_auth.js';
9
10
  import { CLAUDE_CODE_AUTH_SENTINEL, hasUsableClaudeCodeCliAuth as hasUsableClaudeCodeAuth } from './agent/backends/claude_cli.js';
@@ -182,21 +183,44 @@ const DEFAULT_WORKFLOWS = {
182
183
  maxAgents: 50,
183
184
  workflowTimeoutMs: 43_200_000,
184
185
  };
186
+ const SETTINGS_DEFAULTS_VERSION = 1;
187
+ const PERSONAL_DEFAULT_PROVIDER = 'modal';
188
+ const PERSONAL_DEFAULT_MODEL = 'deepseek-v4.1-flash';
189
+ const PERSONAL_DEFAULT_PROXY_URL = 'https://zergrouter.com';
190
+ /** Environment variables that already name a working provider route. */
191
+ const ROUTE_CREDENTIAL_ENV = [
192
+ 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_API_KEY', 'KIMI_API_KEY',
193
+ 'MOONSHOT_API_KEY', 'FIREWORKS_API_KEY', 'OPENAI_COMPAT_API_KEY', 'INCEPTION_API_KEY',
194
+ 'CLARIFAI_API_KEY', 'DEEPSEEK_API_KEY', 'ZTC_AUTH_TOKEN',
195
+ ];
196
+ /**
197
+ * Modal DeepSeek through Zerg Router is the default for a person starting
198
+ * fresh. The Modal route needs a Router device login, so a process launched
199
+ * with another credential in its environment (a provider key, or a platform
200
+ * token as headless hosts pass) keeps the route that credential serves, as
201
+ * ZTC did before the personal default. Only the environment is consulted, so
202
+ * the default does not depend on files in the user's home.
203
+ */
185
204
  function default_provider() {
186
- return inferDefaultProvider();
205
+ const hasRouteCredential = ROUTE_CREDENTIAL_ENV.some(name => Boolean(process.env[name]?.trim()));
206
+ return hasRouteCredential ? inferDefaultProvider() : PERSONAL_DEFAULT_PROVIDER;
187
207
  }
188
208
  export function build_default_config() {
189
209
  const provider = default_provider();
190
210
  return {
211
+ settingsDefaultsVersion: SETTINGS_DEFAULTS_VERSION,
191
212
  apiKey: getDefaultApiKeyForProvider(provider),
192
213
  apiKeys: undefined,
193
214
  provider,
194
215
  openaiCompatibleBaseUrl: undefined,
195
- model: getDefaultModelForProvider(provider),
216
+ model: provider === PERSONAL_DEFAULT_PROVIDER ? PERSONAL_DEFAULT_MODEL : getDefaultModelForProvider(provider),
196
217
  // Like Codex, leave output limits to the provider unless explicitly set.
197
218
  maxTokens: undefined,
198
219
  zergEndpoint: undefined,
220
+ // `auto` routes through the Router once signed in and still honors an
221
+ // explicit provider with a direct key; `zrouter` would reject that key.
199
222
  authMode: 'auto',
223
+ zergProxyUrl: PERSONAL_DEFAULT_PROXY_URL,
200
224
  emulationId: undefined,
201
225
  spinnerVerbs: DEFAULT_SPINNER_VERBS,
202
226
  toolPermissions: { ...DEFAULT_TOOL_PERMISSIONS },
@@ -238,11 +262,132 @@ export function build_default_config() {
238
262
  mcpServers: {},
239
263
  };
240
264
  }
265
+ /**
266
+ * The settings this release gives new defaults: the compaction and display
267
+ * behavior every existing profile adopts once. Everything else a profile saved
268
+ * (security settings, effort choices, limits, keybindings, the status line) is
269
+ * the user's and carries over untouched.
270
+ */
271
+ const ADOPTED_DEFAULT_KEYS = [
272
+ 'autoCompactThreshold',
273
+ 'compactionTargetPercent',
274
+ 'compactionTriggerMode',
275
+ 'compaction',
276
+ 'contextTokenBudget',
277
+ 'autoContinue',
278
+ 'messageDisplay',
279
+ ];
280
+ /** Keys that pick the provider route. Replaced together so a route stays coherent. */
281
+ const ROUTING_SETTING_KEYS = [
282
+ 'provider',
283
+ 'model',
284
+ 'authMode',
285
+ 'zergProxyUrl',
286
+ 'zergEndpoint',
287
+ 'apiEnvironment',
288
+ 'zrouterRoutePlan',
289
+ ];
290
+ function normalizedOrigin(value) {
291
+ return value?.trim().replace(/\/+$/, '') || undefined;
292
+ }
293
+ /**
294
+ * True when the saved provider route can already reach a model: a direct or
295
+ * platform credential, a subscription or account login, a configured
296
+ * OpenAI-compatible endpoint, an explicit direct mode, or a router other than
297
+ * the new default. Only a route that cannot work is replaced by the new
298
+ * personal default.
299
+ */
300
+ function savedRouteIsUsable(saved) {
301
+ if (!saved.provider)
302
+ return false;
303
+ const provider = normalizeProviderId(saved.provider);
304
+ if (saved.authMode === 'direct')
305
+ return true;
306
+ const origin = normalizedOrigin(saved.zergProxyUrl);
307
+ if (origin && origin !== PERSONAL_DEFAULT_PROXY_URL)
308
+ return true;
309
+ if (saved.auth?.token)
310
+ return true;
311
+ if (saved.apiKey || saved.apiKeys?.[provider] || saved.apiKeys?.[saved.provider])
312
+ return true;
313
+ if (getDefaultApiKeyForProvider(provider))
314
+ return true;
315
+ if (provider === 'openai_compatible'
316
+ && (saved.openaiCompatibleBaseUrl || saved.openaiCompatible?.baseUrl))
317
+ return true;
318
+ return Boolean(saved.accounts?.accounts?.some(account => normalizeProviderId(account.provider) === provider));
319
+ }
320
+ /**
321
+ * Adopt this release's new defaults once: the compaction and display settings,
322
+ * and the Modal route for a profile whose saved route cannot reach a model.
323
+ * Everything else the profile saved carries over, so the migration can never
324
+ * widen a security setting or drop a credential.
325
+ */
326
+ function migrateSettingsDefaults(saved) {
327
+ const defaults = build_default_config();
328
+ const migrated = { ...saved, settingsDefaultsVersion: SETTINGS_DEFAULTS_VERSION };
329
+ const record = migrated;
330
+ const fresh = defaults;
331
+ const adopted = savedRouteIsUsable(saved)
332
+ ? ADOPTED_DEFAULT_KEYS
333
+ : [...ADOPTED_DEFAULT_KEYS, ...ROUTING_SETTING_KEYS];
334
+ for (const key of adopted) {
335
+ if (fresh[key] === undefined)
336
+ delete record[key];
337
+ else
338
+ record[key] = fresh[key];
339
+ }
340
+ // The legacy single slot belongs to the saved provider; give it a
341
+ // provider-scoped home so a later provider change cannot misattribute it.
342
+ if (saved.apiKey !== undefined) {
343
+ const oldProvider = saved.provider || inferDefaultProvider();
344
+ migrated.apiKeys = { [oldProvider]: saved.apiKey, ...saved.apiKeys };
345
+ delete migrated.apiKey;
346
+ }
347
+ return migrated;
348
+ }
349
+ async function writeMigratedSettings(path, settings, original) {
350
+ // A symlinked config (dotfiles setups) is migrated at its target, so the link
351
+ // keeps pointing at the file ZTC reads instead of being replaced by a copy.
352
+ let target = path;
353
+ try {
354
+ target = await realpath(path);
355
+ }
356
+ catch (error) {
357
+ if (error.code !== 'ENOENT')
358
+ throw error;
359
+ }
360
+ // Keep the pre-migration file once, so a user can recover anything the new
361
+ // defaults replaced. Never overwrite an earlier backup.
362
+ try {
363
+ await writeFile(`${target}.pre-defaults-v${SETTINGS_DEFAULTS_VERSION}.bak`, original, {
364
+ encoding: 'utf-8', mode: 0o600, flag: 'wx',
365
+ });
366
+ }
367
+ catch (error) {
368
+ if (error.code !== 'EEXIST')
369
+ throw error;
370
+ }
371
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
372
+ try {
373
+ await writeFile(temporary, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
374
+ await rename(temporary, target);
375
+ }
376
+ finally {
377
+ try {
378
+ await unlink(temporary);
379
+ }
380
+ catch (error) {
381
+ if (error.code !== 'ENOENT')
382
+ throw error;
383
+ }
384
+ }
385
+ }
241
386
  /** Config dir, overridable via ZTC_CONFIG_DIR (test isolation + custom locations).
242
387
  * Resolved at call time so a test can point it at a temp dir without import-order
243
388
  * games, and so it never writes the real ~/.ztc. */
244
389
  function configDir() {
245
- return process.env.ZTC_CONFIG_DIR || join(homedir(), '.ztc');
390
+ return profileConfigDir();
246
391
  }
247
392
  function configFile() {
248
393
  return join(configDir(), 'config.json');
@@ -253,6 +398,7 @@ export function shouldRefreshSuperuserStatus(env = process.env) {
253
398
  }
254
399
  export class ConfigStore {
255
400
  config = build_default_config();
401
+ profileEmulation = builtinProfileEmulation(activeProfile());
256
402
  // The legacy slot also carries runtime defaults. Only a configured key may
257
403
  // be migrated into apiKeys or written to disk.
258
404
  legacyApiKeyFromEnvironment = true;
@@ -375,6 +521,7 @@ export class ConfigStore {
375
521
  async load(force = false) {
376
522
  if (this.loaded && !force)
377
523
  return this.config;
524
+ this.profileEmulation = builtinProfileEmulation(activeProfile());
378
525
  this.routerCredentialsError = null;
379
526
  try {
380
527
  this.routerCredentials = await loadRouterCredentials();
@@ -386,46 +533,62 @@ export class ConfigStore {
386
533
  this.routerCredentialsError = error instanceof Error ? error
387
534
  : new Error('Invalid ZTC router credentials. Run ztc auth login.');
388
535
  }
536
+ let saved;
537
+ let savedText = '';
389
538
  try {
390
539
  const data = await readFile(configFile(), 'utf-8');
391
- const saved = JSON.parse(data);
392
- this.config = { ...build_default_config(), ...saved };
393
- this.legacyApiKeyFromEnvironment = saved.apiKey === undefined;
394
- // Defaults were built for the inferred provider. A saved provider without
395
- // a saved key must not inherit that other provider's credential.
396
- if (saved.provider && saved.apiKey === undefined) {
397
- this.config.apiKey = getDefaultApiKeyForProvider(saved.provider);
398
- }
399
- if (saved.provider && saved.model === undefined) {
400
- this.config.model = getDefaultModelForProvider(saved.provider);
401
- }
402
- if (this.config.model === 'claude-opus-4-5-20250514' || this.config.model === 'claude-opus-4-20250514') {
403
- this.config.model = 'claude-opus-5';
540
+ savedText = data;
541
+ const parsed = JSON.parse(data);
542
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
543
+ throw new Error('config.json must contain an object');
404
544
  }
405
- if (!this.config.spinnerVerbs) {
406
- this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
407
- }
408
- if (!this.config.toolPermissions) {
409
- this.config.toolPermissions = { ...DEFAULT_TOOL_PERMISSIONS };
410
- }
411
- this.config.permissionProfiles = {
412
- ...DEFAULT_PERMISSION_PROFILES,
413
- ...(this.config.permissionProfiles || {}),
414
- };
415
- this.config.permissionProfile = this.config.permissionProfile || 'trusted';
416
- this.config.sandbox = { ...DEFAULT_SANDBOX, ...(this.config.sandbox || {}) };
417
- this.config.intents = { ...DEFAULT_INTENTS, ...(this.config.intents || {}) };
418
- this.config.contextProgressBar = { ...DEFAULT_CONTEXT_PROGRESS_BAR, ...(this.config.contextProgressBar || {}) };
419
- this.config.messageDisplay = { ...DEFAULT_MESSAGE_DISPLAY, ...(this.config.messageDisplay || {}) };
420
- this.config.compaction = resolveCompactionSettings(this.config);
421
- this.applyEnvOverrides();
545
+ saved = parsed;
422
546
  }
423
547
  catch {
424
- // File doesn't exist or is invalid, use defaults
548
+ // File doesn't exist or is invalid, use defaults.
425
549
  this.config = build_default_config();
426
550
  this.legacyApiKeyFromEnvironment = true;
427
551
  this.applyEnvOverrides();
552
+ this.loaded = true;
553
+ return this.config;
554
+ }
555
+ if (typeof saved.settingsDefaultsVersion !== 'number'
556
+ || saved.settingsDefaultsVersion < SETTINGS_DEFAULTS_VERSION) {
557
+ const migrated = migrateSettingsDefaults(saved);
558
+ // A failed write must not make an unmigrated profile appear migrated.
559
+ await writeMigratedSettings(configFile(), migrated, savedText);
560
+ saved = migrated;
561
+ }
562
+ this.config = { ...build_default_config(), ...saved };
563
+ this.legacyApiKeyFromEnvironment = saved.apiKey === undefined;
564
+ // Defaults were built for the preset provider. A saved provider without
565
+ // a saved key must not inherit that other provider's credential.
566
+ if (saved.provider && saved.apiKey === undefined) {
567
+ this.config.apiKey = getDefaultApiKeyForProvider(saved.provider);
428
568
  }
569
+ if (saved.provider && saved.model === undefined) {
570
+ this.config.model = getDefaultModelForProvider(saved.provider);
571
+ }
572
+ if (this.config.model === 'claude-opus-4-5-20250514' || this.config.model === 'claude-opus-4-20250514') {
573
+ this.config.model = 'claude-opus-5';
574
+ }
575
+ if (!this.config.spinnerVerbs) {
576
+ this.config.spinnerVerbs = DEFAULT_SPINNER_VERBS;
577
+ }
578
+ if (!this.config.toolPermissions) {
579
+ this.config.toolPermissions = { ...DEFAULT_TOOL_PERMISSIONS };
580
+ }
581
+ this.config.permissionProfiles = {
582
+ ...DEFAULT_PERMISSION_PROFILES,
583
+ ...(this.config.permissionProfiles || {}),
584
+ };
585
+ this.config.permissionProfile = this.config.permissionProfile || 'trusted';
586
+ this.config.sandbox = { ...DEFAULT_SANDBOX, ...(this.config.sandbox || {}) };
587
+ this.config.intents = { ...DEFAULT_INTENTS, ...(this.config.intents || {}) };
588
+ this.config.contextProgressBar = { ...DEFAULT_CONTEXT_PROGRESS_BAR, ...(this.config.contextProgressBar || {}) };
589
+ this.config.messageDisplay = { ...DEFAULT_MESSAGE_DISPLAY, ...(this.config.messageDisplay || {}) };
590
+ this.config.compaction = resolveCompactionSettings(this.config);
591
+ this.applyEnvOverrides();
429
592
  this.loaded = true;
430
593
  return this.config;
431
594
  }
@@ -436,6 +599,64 @@ export class ConfigStore {
436
599
  this.sessionOverrides = sessionOverrides;
437
600
  this.configScope = scope;
438
601
  }
602
+ /** Load a profile in this process; restore the old view if runtime activation fails. */
603
+ async switchProfile(name, activate) {
604
+ const next = validateProfileName(name);
605
+ if (next === activeProfile())
606
+ return;
607
+ const sameConfigDirectory = profileConfigDir(next) === profileConfigDir();
608
+ const target = join(profileConfigDir(next), 'config.json');
609
+ try {
610
+ const saved = JSON.parse(await readFile(target, 'utf-8'));
611
+ if (!saved || typeof saved !== 'object' || Array.isArray(saved)) {
612
+ throw new Error('config.json must contain an object');
613
+ }
614
+ }
615
+ catch (error) {
616
+ if (error.code !== 'ENOENT') {
617
+ throw new Error(`Cannot load config profile ${next}: ${error.message}`);
618
+ }
619
+ }
620
+ const previousEnv = process.env.ZTC_PROFILE;
621
+ const previous = {
622
+ config: this.config,
623
+ profileEmulation: this.profileEmulation,
624
+ loaded: this.loaded,
625
+ legacyApiKeyFromEnvironment: this.legacyApiKeyFromEnvironment,
626
+ routerCredentials: this.routerCredentials,
627
+ routerCredentialsError: this.routerCredentialsError,
628
+ routerFallbackWarning: this.routerFallbackWarning,
629
+ sessionOverrides: this.sessionOverrides,
630
+ cliOverrides: this.cliOverrides,
631
+ runtimeActivePerProvider: this.runtimeActivePerProvider,
632
+ configScope: this.configScope,
633
+ };
634
+ process.env.ZTC_PROFILE = next;
635
+ if (sameConfigDirectory) {
636
+ // Preset switches keep the current settings layer, except an explicit
637
+ // emulation override that would conceal the selected preset.
638
+ const { emulationId: _emulationId, ...remainingOverrides } = this.sessionOverrides;
639
+ this.sessionOverrides = remainingOverrides;
640
+ }
641
+ else {
642
+ this.sessionOverrides = {};
643
+ this.cliOverrides = {};
644
+ this.runtimeActivePerProvider = {};
645
+ this.configScope = 'global';
646
+ }
647
+ try {
648
+ await this.load(true);
649
+ await activate();
650
+ }
651
+ catch (error) {
652
+ if (previousEnv === undefined)
653
+ delete process.env.ZTC_PROFILE;
654
+ else
655
+ process.env.ZTC_PROFILE = previousEnv;
656
+ Object.assign(this, previous);
657
+ throw error;
658
+ }
659
+ }
439
660
  async save() {
440
661
  if (this.configScope === 'session') {
441
662
  return;
@@ -464,6 +685,23 @@ export class ConfigStore {
464
685
  runtimeDiag('error', `Failed to save config: ${err?.message ?? err}`);
465
686
  }
466
687
  }
688
+ /** Clear the active profile's saved settings and reset this process's settings view. */
689
+ async factoryReset() {
690
+ for (const path of [configFile(), join(configDir(), 'router-auth.json')]) {
691
+ try {
692
+ await unlink(path);
693
+ }
694
+ catch (error) {
695
+ if (error.code !== 'ENOENT')
696
+ throw error;
697
+ }
698
+ }
699
+ this.sessionOverrides = {};
700
+ this.cliOverrides = {};
701
+ this.runtimeActivePerProvider = {};
702
+ this.configScope = 'global';
703
+ await this.load(true);
704
+ }
467
705
  get() {
468
706
  return this.composeConfig(true);
469
707
  }
@@ -478,6 +716,7 @@ export class ConfigStore {
478
716
  composeConfig(includeEffortOverride) {
479
717
  return {
480
718
  ...this.config,
719
+ ...(this.profileEmulation ? { emulationId: this.profileEmulation } : {}),
481
720
  ...this.sessionOverrides,
482
721
  ...(includeEffortOverride ? this.cliOverrides : this.cliOverridesWithoutEffort()),
483
722
  // Explicit process effort outranks saved scoped defaults without changing them.
@@ -515,7 +754,7 @@ export class ConfigStore {
515
754
  }
516
755
  }
517
756
  set(key, value) {
518
- if (this.configScope === 'session') {
757
+ if (this.configScope === 'session' || (key === 'emulationId' && this.profileEmulation)) {
519
758
  this.sessionOverrides[key] = value;
520
759
  return;
521
760
  }
@@ -606,10 +845,10 @@ export class ConfigStore {
606
845
  }
607
846
  }
608
847
  getEmulationId() {
609
- return this.config.emulationId;
848
+ return this.get().emulationId;
610
849
  }
611
850
  setEmulationId(id) {
612
- this.config.emulationId = id;
851
+ this.set('emulationId', id);
613
852
  }
614
853
  getMaskedApiKey() {
615
854
  const key = this.getApiKey();
@@ -780,6 +1019,10 @@ export class ConfigStore {
780
1019
  getCodexEffort(provider, model) {
781
1020
  return resolveCodexEffort(this.get(), provider ?? this.getProvider(), model ?? this.getModel());
782
1021
  }
1022
+ /** Saved effort the active (or given) model rejects; undefined when it applies. */
1023
+ getUnsupportedCodexEffort(provider, model) {
1024
+ return unsupportedCodexEffort(this.get(), provider ?? this.getProvider(), model ?? this.getModel());
1025
+ }
783
1026
  /** Effort chosen by a startup `--effort` flag for this process (= none when unset). */
784
1027
  getCodexEffortOverride() {
785
1028
  return this.cliOverrides.codexEffort;
package/dist/headless.js CHANGED
@@ -668,11 +668,18 @@ export class HeadlessApp {
668
668
  inputTokens: event.usage.inputTokens,
669
669
  outputTokens: event.usage.outputTokens,
670
670
  totalTokens: event.usage.totalTokens,
671
+ ...(event.usage.cachedInputTokens === undefined ? {} : { cachedInputTokens: event.usage.cachedInputTokens }),
671
672
  estimated: event.usage.estimated === true,
672
673
  },
673
674
  ...(typeof event.contextTokens === 'number'
674
675
  ? { contextTokens: event.contextTokens }
675
676
  : {}),
677
+ ...(typeof event.cacheInputTokens === 'number'
678
+ ? { cacheInputTokens: event.cacheInputTokens }
679
+ : {}),
680
+ ...(typeof event.contextCachedInputTokens === 'number'
681
+ ? { contextCachedInputTokens: event.contextCachedInputTokens }
682
+ : {}),
676
683
  ...(typeof event.contextWindow === 'number'
677
684
  ? { contextWindow: event.contextWindow }
678
685
  : {}),
@@ -989,6 +996,13 @@ export class HeadlessApp {
989
996
  * Initialize the app
990
997
  */
991
998
  async init() {
999
+ // Load the persisted profile before reading any credential signal.
1000
+ // `runHeadless` is a public entry point, so it cannot assume its caller ran
1001
+ // the CLI loader (headless_cli.ts does; direct library callers may not).
1002
+ // Without this the gate below reads unloaded defaults and rejects a profile
1003
+ // whose real credential (for example a Codex subscription auth.json) is on
1004
+ // disk. `load()` is idempotent, so the CLI path is unaffected.
1005
+ await configStore.load();
992
1006
  // Require either direct provider key or platform auth token.
993
1007
  if (!configStore.hasApiKey() && !configStore.isAuthenticated()) {
994
1008
  this.output({
@@ -1,4 +1,4 @@
1
- export declare const PANE_PROVIDERS: readonly ["anthropic", "claude_code", "openai", "codex_app_server", "codex_legacy", "gemini", "deepseek", "modal", "kimi", "kimi_code"];
1
+ export declare const PANE_PROVIDERS: readonly ["anthropic", "claude_code", "openai", "codex_app_server", "codex_legacy", "gemini", "deepseek", "fireworks", "modal", "kimi", "kimi_code"];
2
2
  export type PaneProvider = typeof PANE_PROVIDERS[number];
3
3
  export interface CatalogModel {
4
4
  id: string;