zames_pro 2.88.2 → 2.90.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +32 -585
  3. package/dist/agent-loop.js +47 -4
  4. package/dist/ask-retry.js +49 -0
  5. package/dist/ask-state.js +80 -0
  6. package/dist/browser-base.js +69 -0
  7. package/dist/browser.js +170 -41
  8. package/dist/command-registry.js +58 -0
  9. package/dist/commands.js +25 -2
  10. package/dist/completion-check.js +77 -0
  11. package/dist/config-commands.js +12 -4
  12. package/dist/config-menu.js +179 -81
  13. package/dist/config-schema.js +562 -0
  14. package/dist/config.js +19 -405
  15. package/dist/dev-commands.js +27 -0
  16. package/dist/dev-watch.js +96 -0
  17. package/dist/first-run.js +80 -0
  18. package/dist/glm-browser.js +133 -32
  19. package/dist/i18n/common.js +6 -3
  20. package/dist/i18n/config.js +78 -3
  21. package/dist/i18n/deepseek.js +1 -0
  22. package/dist/i18n/wizard.js +25 -0
  23. package/dist/i18n.js +6 -1
  24. package/dist/index.js +1299 -924
  25. package/dist/info-commands.js +61 -0
  26. package/dist/input/layout.js +38 -1
  27. package/dist/input.js +2 -2
  28. package/dist/password-store.js +48 -0
  29. package/dist/prompt-budget.js +23 -0
  30. package/dist/provider-errors.js +53 -0
  31. package/dist/provider-fallback.js +46 -0
  32. package/dist/qwen-browser.js +155 -39
  33. package/dist/run-task.js +2 -1
  34. package/dist/scheduler-commands.js +41 -0
  35. package/dist/send-slot.js +36 -0
  36. package/dist/session-commands.js +42 -0
  37. package/dist/spinner.js +3 -3
  38. package/dist/swallow.js +45 -0
  39. package/dist/system-prompt.js +2 -2
  40. package/dist/tree-commands.js +36 -0
  41. package/dist/tui/ConfigMenu.js +149 -64
  42. package/dist/tui/FullScreenApp.js +13 -4
  43. package/dist/tui/InputLine.js +20 -3
  44. package/dist/tui/LogView.js +11 -5
  45. package/dist/tui/StatusBar.js +4 -1
  46. package/dist/tui/TuiApp.js +11 -2
  47. package/dist/tui/layout.js +14 -1
  48. package/dist/tui/store.js +2 -1
  49. package/dist/tui/ui-colors.js +2 -0
  50. package/dist/width.js +10 -5
  51. package/llms-full.txt +16 -292
  52. package/package.json +2 -2
package/dist/config.js CHANGED
@@ -3,14 +3,21 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { DEFAULT_DIAGNOSTICS } from './diagnostics.js';
5
5
  import { DEFAULT_LOCALE } from './i18n.js';
6
+ // The /config schema is DATA ONLY and lives in its own module; re-exported
7
+ // here so every existing importer keeps working.
8
+ export { CONFIG_SCHEMA, CONFIG_TOP_GROUPS, getConfigField, fieldsOfTopGroup, subgroupsOf, topGroupModified, } from './config-schema.js';
9
+ import { getConfigField } from './config-schema.js';
6
10
  import { writeJsonAtomic } from './fsutil.js';
7
- import { PROVIDER_IDS } from './providers.js';
8
- import { DEFAULT_THEME, themeNames } from './theme.js';
11
+ import { DEFAULT_THEME } from './theme.js';
9
12
  const ZAMES_HOME = path.join(os.homedir(), '.zames');
10
13
  const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
11
14
  // The PROJECT config path is resolved LAZILY: `/cd` changes process.cwd() at
12
15
  // runtime, so freezing it at import time meant a `.zamesrc.json` in the new
13
16
  // working directory was never read (and writes went to the OLD directory).
17
+ /** The HOME config file (`~/.zames/config.json`). */
18
+ export function homeConfigPath() {
19
+ return HOME_CONFIG;
20
+ }
14
21
  export function projectConfigPath() {
15
22
  return path.join(process.cwd(), '.zamesrc.json');
16
23
  }
@@ -118,6 +125,13 @@ export const DEFAULTS = {
118
125
  // out of the main context, and the main chat is restored afterwards.
119
126
  subagents: true,
120
127
  maxSubagents: 3,
128
+ // N8: providers a task may CONTINUE on after the active one exhausted its
129
+ // retries. Empty by default (off): a fallback is only used for an
130
+ // AUTHORIZED provider (auth marker + a confirmed login), never a guest.
131
+ fallbackProviders: [],
132
+ // `ask` (the default) asks the operator before switching; `auto` switches
133
+ // without a prompt.
134
+ fallbackMode: 'ask',
121
135
  deepThinking: false,
122
136
  webSearch: true,
123
137
  // OFF by default: the telemetry file only serves diagnosis and manual
@@ -130,6 +144,9 @@ export const DEFAULTS = {
130
144
  password: '',
131
145
  saveSession: true,
132
146
  },
147
+ // N21: where the password lives. `config` keeps the historical field,
148
+ // `none` never stores it, `keychain` uses the OS store (with a fallback).
149
+ passwordStore: 'config',
133
150
  },
134
151
  ui: {
135
152
  locale: DEFAULT_LOCALE,
@@ -276,409 +293,6 @@ function deepMerge(target, source) {
276
293
  }
277
294
  }
278
295
  }
279
- export const CONFIG_SCHEMA = [
280
- {
281
- path: 'ui.locale',
282
- type: 'enum',
283
- values: ['ru', 'en'],
284
- labelKey: 'cfg.f.ui_locale',
285
- groupKey: 'cfg.group.ui',
286
- },
287
- {
288
- path: 'maxIterations',
289
- type: 'number',
290
- min: 0,
291
- max: 100000,
292
- labelKey: 'cfg.f.maxIterations',
293
- groupKey: 'cfg.group.agent',
294
- },
295
- {
296
- path: 'ui.inputBorder',
297
- type: 'enum',
298
- values: ['round', 'single', 'double', 'bold', 'none'],
299
- labelKey: 'cfg.f.ui_inputBorder',
300
- groupKey: 'cfg.group.ui',
301
- },
302
- {
303
- path: 'ui.theme',
304
- type: 'enum',
305
- values: themeNames(),
306
- labelKey: 'cfg.f.ui_theme',
307
- groupKey: 'cfg.group.ui',
308
- },
309
- {
310
- path: 'ui.answerWidth',
311
- type: 'number',
312
- min: 0,
313
- max: 1000,
314
- labelKey: 'cfg.f.ui_answerWidth',
315
- groupKey: 'cfg.group.ui',
316
- },
317
- {
318
- path: 'ui.tui',
319
- type: 'boolean',
320
- labelKey: 'cfg.f.ui_tui',
321
- groupKey: 'cfg.group.ui',
322
- },
323
- {
324
- path: 'ui.alternateScreen',
325
- type: 'boolean',
326
- labelKey: 'cfg.f.ui_alternateScreen',
327
- groupKey: 'cfg.group.ui',
328
- },
329
- {
330
- path: 'ui.mouseScroll',
331
- type: 'boolean',
332
- labelKey: 'cfg.f.ui_mouseScroll',
333
- groupKey: 'cfg.group.ui',
334
- },
335
- {
336
- path: 'headless',
337
- type: 'boolean',
338
- labelKey: 'cfg.f.headless',
339
- groupKey: 'cfg.group.agent',
340
- },
341
- {
342
- path: 'browser.auth.username',
343
- type: 'string',
344
- labelKey: 'cfg.f.browser_authUsername',
345
- groupKey: 'cfg.group.browser',
346
- },
347
- {
348
- path: 'browser.auth.password',
349
- type: 'string',
350
- labelKey: 'cfg.f.browser_authPassword',
351
- groupKey: 'cfg.group.browser',
352
- },
353
- {
354
- path: 'browser.auth.saveSession',
355
- type: 'boolean',
356
- labelKey: 'cfg.f.browser_authSaveSession',
357
- groupKey: 'cfg.group.browser',
358
- },
359
- // Each non-DeepSeek provider owns its credentials, so a DeepSeek login is
360
- // never typed into a Qwen/GLM form (and vice versa). They MUST be in the
361
- // schema: writeConfigValue rejects an unknown key, which silently dropped
362
- // every GLM/Qwen credential saved from the login window.
363
- {
364
- path: 'qwen.auth.username',
365
- type: 'string',
366
- labelKey: 'cfg.f.qwen_authUsername',
367
- groupKey: 'cfg.group.browser',
368
- },
369
- {
370
- path: 'qwen.auth.password',
371
- type: 'string',
372
- labelKey: 'cfg.f.qwen_authPassword',
373
- groupKey: 'cfg.group.browser',
374
- },
375
- {
376
- path: 'glm.auth.username',
377
- type: 'string',
378
- labelKey: 'cfg.f.glm_authUsername',
379
- groupKey: 'cfg.group.browser',
380
- },
381
- {
382
- path: 'glm.auth.password',
383
- type: 'string',
384
- labelKey: 'cfg.f.glm_authPassword',
385
- groupKey: 'cfg.group.browser',
386
- },
387
- {
388
- path: 'provider',
389
- type: 'enum',
390
- values: [...PROVIDER_IDS],
391
- labelKey: 'cfg.f.provider',
392
- groupKey: 'cfg.group.browser',
393
- },
394
- {
395
- path: 'qwen.model',
396
- type: 'string',
397
- labelKey: 'cfg.f.qwen_model',
398
- groupKey: 'cfg.group.browser',
399
- },
400
- {
401
- path: 'qwen.contextLimit',
402
- type: 'number',
403
- min: 10_000,
404
- max: 10_000_000,
405
- labelKey: 'cfg.f.qwen_contextLimit',
406
- groupKey: 'cfg.group.browser',
407
- },
408
- {
409
- path: 'qwen.minSendIntervalMs',
410
- type: 'number',
411
- min: 0,
412
- max: 600000,
413
- labelKey: 'cfg.f.qwen_minSendIntervalMs',
414
- groupKey: 'cfg.group.browser',
415
- },
416
- {
417
- path: 'qwen.answerTimeoutMs',
418
- type: 'number',
419
- min: 1000,
420
- max: 3600000,
421
- labelKey: 'cfg.f.qwen_answerTimeoutMs',
422
- groupKey: 'cfg.group.browser',
423
- },
424
- {
425
- path: 'glm.model',
426
- type: 'string',
427
- labelKey: 'cfg.f.glm_model',
428
- groupKey: 'cfg.group.browser',
429
- },
430
- {
431
- path: 'glm.contextLimit',
432
- type: 'number',
433
- min: 10_000,
434
- max: 10_000_000,
435
- labelKey: 'cfg.f.glm_contextLimit',
436
- groupKey: 'cfg.group.browser',
437
- },
438
- {
439
- path: 'glm.minSendIntervalMs',
440
- type: 'number',
441
- min: 0,
442
- max: 600000,
443
- labelKey: 'cfg.f.glm_minSendIntervalMs',
444
- groupKey: 'cfg.group.browser',
445
- },
446
- {
447
- path: 'glm.answerTimeoutMs',
448
- type: 'number',
449
- min: 1000,
450
- max: 3600000,
451
- labelKey: 'cfg.f.glm_answerTimeoutMs',
452
- groupKey: 'cfg.group.browser',
453
- },
454
- {
455
- path: 'debug',
456
- type: 'boolean',
457
- labelKey: 'cfg.f.debug',
458
- groupKey: 'cfg.group.agent',
459
- },
460
- {
461
- path: 'hotReload',
462
- type: 'boolean',
463
- labelKey: 'cfg.f.hotReload',
464
- groupKey: 'cfg.group.agent',
465
- },
466
- {
467
- path: 'undo.enabled',
468
- type: 'boolean',
469
- labelKey: 'cfg.f.undo_enabled',
470
- groupKey: 'cfg.group.undo',
471
- },
472
- {
473
- path: 'undo.maxBackups',
474
- type: 'number',
475
- min: 1,
476
- max: 100000,
477
- labelKey: 'cfg.f.undo_maxBackups',
478
- groupKey: 'cfg.group.undo',
479
- },
480
- {
481
- path: 'checkpoint.enabled',
482
- type: 'boolean',
483
- labelKey: 'cfg.f.checkpoint_enabled',
484
- groupKey: 'cfg.group.checkpoint',
485
- },
486
- {
487
- path: 'checkpoint.maxBackups',
488
- type: 'number',
489
- min: 1,
490
- max: 1000,
491
- labelKey: 'cfg.f.checkpoint_maxBackups',
492
- groupKey: 'cfg.group.checkpoint',
493
- },
494
- {
495
- path: 'transcript.enabled',
496
- type: 'boolean',
497
- labelKey: 'cfg.f.transcript_enabled',
498
- groupKey: 'cfg.group.transcript',
499
- },
500
- {
501
- path: 'browser.answerTimeoutMs',
502
- type: 'number',
503
- min: 1000,
504
- max: 3600000,
505
- labelKey: 'cfg.f.browser_answerTimeoutMs',
506
- groupKey: 'cfg.group.browser',
507
- },
508
- {
509
- path: 'browser.askRetries',
510
- type: 'number',
511
- min: 1,
512
- max: 100,
513
- labelKey: 'cfg.f.browser_askRetries',
514
- groupKey: 'cfg.group.browser',
515
- },
516
- {
517
- path: 'browser.stabilityChecks',
518
- type: 'number',
519
- min: 1,
520
- max: 100,
521
- labelKey: 'cfg.f.browser_stabilityChecks',
522
- groupKey: 'cfg.group.browser',
523
- },
524
- {
525
- path: 'browser.stabilityDelayMs',
526
- type: 'number',
527
- min: 0,
528
- max: 60000,
529
- labelKey: 'cfg.f.browser_stabilityDelayMs',
530
- groupKey: 'cfg.group.browser',
531
- },
532
- {
533
- path: 'browser.minSendIntervalMs',
534
- type: 'number',
535
- min: 0,
536
- max: 600000,
537
- labelKey: 'cfg.f.browser_minSendIntervalMs',
538
- groupKey: 'cfg.group.browser',
539
- },
540
- {
541
- path: 'browser.thinkingExtraMs',
542
- type: 'number',
543
- min: 0,
544
- max: 600000,
545
- labelKey: 'cfg.f.browser_thinkingExtraMs',
546
- groupKey: 'cfg.group.browser',
547
- },
548
- {
549
- path: 'browser.rateLimitWaitMs',
550
- type: 'number',
551
- min: 0,
552
- max: 3600000,
553
- labelKey: 'cfg.f.browser_rateLimitWaitMs',
554
- groupKey: 'cfg.group.browser',
555
- },
556
- {
557
- path: 'browser.maxRateLimitRetries',
558
- type: 'number',
559
- min: 0,
560
- max: 100,
561
- labelKey: 'cfg.f.browser_maxRateLimitRetries',
562
- groupKey: 'cfg.group.browser',
563
- },
564
- {
565
- path: 'browser.maxIncompleteRetries',
566
- type: 'number',
567
- min: 0,
568
- max: 100,
569
- labelKey: 'cfg.f.browser_maxIncompleteRetries',
570
- groupKey: 'cfg.group.browser',
571
- },
572
- {
573
- path: 'browser.incompleteWaitMs',
574
- type: 'number',
575
- min: 0,
576
- max: 600000,
577
- labelKey: 'cfg.f.browser_incompleteWaitMs',
578
- groupKey: 'cfg.group.browser',
579
- },
580
- {
581
- path: 'browser.autoContinue',
582
- type: 'boolean',
583
- labelKey: 'cfg.f.browser_autoContinue',
584
- groupKey: 'cfg.group.browser',
585
- },
586
- {
587
- path: 'browser.sendTelemetry',
588
- type: 'boolean',
589
- labelKey: 'cfg.f.browser_sendTelemetry',
590
- groupKey: 'cfg.group.browser',
591
- },
592
- {
593
- path: 'browser.loginTimeoutMs',
594
- type: 'number',
595
- min: 30_000,
596
- max: 3_600_000,
597
- labelKey: 'cfg.f.browser_loginTimeoutMs',
598
- groupKey: 'cfg.group.browser',
599
- },
600
- {
601
- path: 'browser.continueMinGapMs',
602
- type: 'number',
603
- min: 0,
604
- max: 600000,
605
- labelKey: 'cfg.f.browser_continueMinGapMs',
606
- groupKey: 'cfg.group.browser',
607
- },
608
- {
609
- path: 'browser.askDeadlineMs',
610
- type: 'number',
611
- min: 30000,
612
- max: 1800000,
613
- labelKey: 'cfg.f.browser_askDeadlineMs',
614
- groupKey: 'cfg.group.browser',
615
- },
616
- {
617
- path: 'browser.maxAfterToolRetries',
618
- type: 'number',
619
- min: 0,
620
- max: 50,
621
- labelKey: 'cfg.f.browser_maxAfterToolRetries',
622
- groupKey: 'cfg.group.browser',
623
- },
624
- {
625
- path: 'browser.autoCompact',
626
- type: 'boolean',
627
- labelKey: 'cfg.f.browser_autoCompact',
628
- groupKey: 'cfg.group.browser',
629
- },
630
- {
631
- path: 'browser.autoCompactPct',
632
- type: 'number',
633
- min: 50,
634
- max: 100,
635
- labelKey: 'cfg.f.browser_autoCompactPct',
636
- groupKey: 'cfg.group.browser',
637
- },
638
- {
639
- path: 'browser.resendPromptOnResume',
640
- type: 'boolean',
641
- labelKey: 'cfg.f.browser_resendPromptOnResume',
642
- groupKey: 'cfg.group.browser',
643
- },
644
- {
645
- path: 'browser.subagents',
646
- type: 'boolean',
647
- labelKey: 'cfg.f.browser_subagents',
648
- groupKey: 'cfg.group.browser',
649
- },
650
- {
651
- path: 'browser.maxSubagents',
652
- type: 'number',
653
- min: 0,
654
- max: 20,
655
- labelKey: 'cfg.f.browser_maxSubagents',
656
- groupKey: 'cfg.group.browser',
657
- },
658
- {
659
- path: 'browser.deepThinking',
660
- type: 'boolean',
661
- labelKey: 'cfg.f.browser_deepThinking',
662
- groupKey: 'cfg.group.browser',
663
- },
664
- {
665
- path: 'browser.webSearch',
666
- type: 'boolean',
667
- labelKey: 'cfg.f.browser_webSearch',
668
- groupKey: 'cfg.group.browser',
669
- },
670
- {
671
- path: 'ui.contextLimit',
672
- type: 'number',
673
- min: 10_000,
674
- max: 10_000_000,
675
- labelKey: 'cfg.f.ui_contextLimit',
676
- groupKey: 'cfg.group.ui',
677
- },
678
- ];
679
- export function getConfigField(path) {
680
- return CONFIG_SCHEMA.find((f) => f.path === path);
681
- }
682
296
  export function getByPath(obj, path) {
683
297
  const parts = path.split('.');
684
298
  // Two guards, both load-bearing. UNSAFE_PATH_KEYS keeps a dotted path off
@@ -0,0 +1,27 @@
1
+ // N2 group (e): the DEV-ONLY self-development commands — /improve, /backlog,
2
+ // /self-review, /self-fix, /self-done, /self-list, /self-diff, /self-apply.
3
+ //
4
+ // They are registry entries with `devOnly: true`, so the guard that used to be
5
+ // a separate pre-check in the main loop is the SAME data the dispatch reads:
6
+ // a regular user gets `msg.dev_only`, dev mode runs the handler. The list here
7
+ // and `DEV_ONLY_COMMANDS` in src/commands.ts are kept in sync by a unit test.
8
+ export function createDevCommands(deps) {
9
+ return [
10
+ { name: 'improve', devOnly: true, handler: (c) => deps.improve(c.rest) },
11
+ { name: 'backlog', devOnly: true, handler: (c) => deps.backlog(c.rest) },
12
+ {
13
+ name: 'self-review',
14
+ devOnly: true,
15
+ handler: (c) => deps.selfReview(c.rest),
16
+ },
17
+ { name: 'self-fix', devOnly: true, handler: (c) => deps.selfFix(c.rest) },
18
+ { name: 'self-done', devOnly: true, handler: () => deps.selfDone() },
19
+ { name: 'self-list', devOnly: true, handler: () => deps.selfList() },
20
+ { name: 'self-diff', devOnly: true, handler: (c) => deps.selfDiff(c.rest) },
21
+ {
22
+ name: 'self-apply',
23
+ devOnly: true,
24
+ handler: (c) => deps.selfApply(c.rest),
25
+ },
26
+ ];
27
+ }
@@ -0,0 +1,96 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ // N4: in dev mode a source file that is NOT hot-reloadable silently keeps
4
+ // running its OLD copy after an edit — the fix "does not work" until zames is
5
+ // restarted. Only `browser.ts` used to be watched. This module computes the
6
+ // FULL set of such files from the source tree, so an edit to index.ts,
7
+ // run-task.ts, a provider browser, browser-login.ts, send-*.ts or the Ink TUI
8
+ // is reported too.
9
+ /**
10
+ * Names (without extension) of the modules that /reload can hot-swap. Kept
11
+ * here so the watcher and `index.ts`'s RELOADABLE list cannot drift: the
12
+ * test asserts every entry is a real module.
13
+ */
14
+ export const HOT_RELOADABLE = new Set([
15
+ 'mcp',
16
+ 'tools',
17
+ 'extraTools',
18
+ 'agent-loop',
19
+ 'system-prompt',
20
+ 'gitTools',
21
+ 'web',
22
+ 'self-review',
23
+ 'diff',
24
+ 'undo',
25
+ 'checkpoint',
26
+ 'transcript',
27
+ 'spinner',
28
+ 'config',
29
+ 'fsutil',
30
+ 'backlog',
31
+ 'commands',
32
+ 'run-task',
33
+ 'attach-refs',
34
+ ]);
35
+ /** A source file in `src/` (recursively) whose basename — without `.ts`/`.tsx` — is not hot-reloadable. */
36
+ export function nonReloadableSources(srcDir, hot = HOT_RELOADABLE) {
37
+ const out = [];
38
+ const walk = (dir) => {
39
+ let entries;
40
+ try {
41
+ entries = readdirSync(dir);
42
+ }
43
+ catch {
44
+ return;
45
+ }
46
+ for (const name of entries) {
47
+ const full = path.join(dir, name);
48
+ let isDir;
49
+ try {
50
+ isDir = statSync(full).isDirectory();
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ if (isDir) {
56
+ walk(full);
57
+ continue;
58
+ }
59
+ const m = /\.(ts|tsx)$/.exec(name);
60
+ if (!m)
61
+ continue;
62
+ const base = name.slice(0, -m[0].length);
63
+ // `index.ts` IS the entry point (it is not reloaded either), so it is
64
+ // included like any other non-reloadable module.
65
+ if (!hot.has(base))
66
+ out.push(full);
67
+ }
68
+ };
69
+ walk(srcDir);
70
+ return out.sort();
71
+ }
72
+ /**
73
+ * Compare the mtimes of the watched files with the previous snapshot and
74
+ * return the CHANGED ones (first call initializes the snapshot and returns
75
+ * an empty list).
76
+ */
77
+ export function diffChanged(files, previous) {
78
+ const changed = [];
79
+ for (const f of files) {
80
+ let mtime;
81
+ try {
82
+ mtime = statSync(f).mtimeMs;
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ const prev = previous.get(f);
88
+ if (prev === undefined)
89
+ previous.set(f, mtime);
90
+ else if (mtime !== prev) {
91
+ previous.set(f, mtime);
92
+ changed.push(f);
93
+ }
94
+ }
95
+ return changed;
96
+ }
@@ -0,0 +1,80 @@
1
+ // N19: the FIRST-RUN WIZARD decision, as a pure state machine.
2
+ //
3
+ // A new user is dropped straight into DeepSeek with no explanation: which
4
+ // language, which provider, why DeepSeek needs a real sign-in while Qwen/GLM
5
+ // have guest limits. The wizard is shown ONLY on a truly fresh install (no
6
+ // `~/.zames/config.json`), never in a non-TTY / one-shot / non-interactive
7
+ // login run, and Esc accepts the defaults.
8
+ //
9
+ // Only the STEP LOGIC lives here (unit-tested); the Ink widgets (SelectList,
10
+ // LoginForm) already exist and are rendered by the caller.
11
+ export function initialWizardState() {
12
+ return { step: 0, locale: null, provider: null, needsLogin: false };
13
+ }
14
+ /**
15
+ * Should the wizard run at all? A fresh HOME only: an existing config means
16
+ * the operator already made these choices, and a non-interactive run (a pipe,
17
+ * `--task`, `ZAMES_NONINTERACTIVE_LOGIN=1`) must never block on a modal.
18
+ */
19
+ export function shouldRunWizard(opts) {
20
+ if (opts.configExists)
21
+ return false;
22
+ if (!opts.isTTY)
23
+ return false;
24
+ if (opts.nonInteractiveLogin)
25
+ return false;
26
+ if (opts.oneShot)
27
+ return false;
28
+ return true;
29
+ }
30
+ /** The step currently shown. */
31
+ export function currentStep(s) {
32
+ if (s.step === 0)
33
+ return { kind: 'language' };
34
+ if (s.step === 1)
35
+ return { kind: 'provider' };
36
+ if (s.step === 2 && s.provider && s.needsLogin) {
37
+ return { kind: 'login', provider: s.provider };
38
+ }
39
+ return { kind: 'done' };
40
+ }
41
+ /** Pick the interface language, then move on. */
42
+ export function chooseLanguage(s, locale) {
43
+ return { ...s, locale, step: 1 };
44
+ }
45
+ /**
46
+ * Pick a provider. DeepSeek ALWAYS needs a real sign-in; Qwen and GLM can be
47
+ * tried as a guest, but the wizard says honestly that the guest tier is a few
48
+ * turns a day (Qwen) and that GLM gates the first send with a captcha.
49
+ */
50
+ export function chooseProvider(s, provider) {
51
+ return {
52
+ ...s,
53
+ provider,
54
+ needsLogin: provider === 'deepseek' || provider === 'glm',
55
+ step: 2,
56
+ };
57
+ }
58
+ /** The login step finished (or was skipped): the wizard is over. */
59
+ export function finishWizard(s) {
60
+ return { ...s, step: 3 };
61
+ }
62
+ /** Esc at any step accepts the current defaults (never blocks the first run). */
63
+ export function acceptDefaults(s) {
64
+ return {
65
+ locale: s.locale ?? 'en',
66
+ provider: s.provider ?? 'deepseek',
67
+ needsLogin: false,
68
+ step: 3,
69
+ };
70
+ }
71
+ /** One-line honest note per provider, shown under the choice. */
72
+ export function providerNote(provider) {
73
+ if (provider === 'deepseek') {
74
+ return 'DeepSeek needs a real sign-in (a fresh headless login may ask for a captcha in a visible window).';
75
+ }
76
+ if (provider === 'qwen') {
77
+ return 'Qwen works as a guest for a few messages a day; sign in for long tasks.';
78
+ }
79
+ return 'GLM gates the first send with a slider captcha: a visible window (--headed) is the practical mode.';
80
+ }