jeo-code 0.6.23 → 0.6.26

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.
@@ -412,128 +412,91 @@ export async function animateFrames(stage: AsciiStage, opts: AnimateFramesOption
412
412
  }
413
413
  return total;
414
414
  }
415
- export const DNA_CLAW_ART: string[] = [
416
- " ╭╯ ◆ ◆ ╰╮ ",
417
- " ╭╯ ╱╲ ╱╲ ╰╮ ",
418
- " ║ ╲ ╱ ║ ",
419
- " ╰╮ ╳ ╳ ╭╯ ",
420
- " ╰╮ ╱ ╲ ╭╯ ",
421
- " ╚══○ ○══╝ ",
422
- " ║ ║ ",
423
- " [ DNA Claw ] "
415
+ /** The compact jeo forge mark: a clean, wordless pictograph of the mascot — the
416
+ * neon crayfish (가재) from assets/character.png, read for its signature feature:
417
+ * the two raised pincer CLAWS (집게). Read top→bottom: the open pincer jaws
418
+ * (◣◣ / ◢◢) reaching up, the claw arms (◆══╲ ╱══◆) angling into the body where
419
+ * the blue knuckles meet, the head with its glowing eye/terminal cluster
420
+ * (◉◉◉), then the rounded carapace/tail (╲▔▔╱). The mark is purely symbolic —
421
+ * NO embedded lettering (the brand wordmark lives in the welcome header, not
422
+ * under the emblem); the JEO identity is carried by the crayfish-claw silhouette
423
+ * alone. Width-1 glyphs only (box drawing + geometrics) so padding/centering
424
+ * math stays exact. Frame 0 is the static symbol. */
425
+ export const FORGE_MARK_ART: string[] = [
426
+ " ◣◣ ◢◢ ",
427
+ " ◆══╲ ╱══◆ ",
428
+ " ╲◉◉◉╱ ",
429
+ " ╲▔▔╱ "
424
430
  ];
425
431
 
426
- export const DNA_CLAW_ART_ASCII: string[] = [
427
- " /{ * * }\\ ",
428
- " /{ / \\ / \\ }\\ ",
429
- " | \\ X / | ",
430
- " \\{ X X }/ ",
431
- " \\{ / X \\ }/ ",
432
- " \\==o o==/ ",
433
- " | | ",
434
- " [ DNA Claw ] "
432
+ export const FORGE_MARK_ART_ASCII: string[] = [
433
+ " // \\\\ ",
434
+ " o==\\ /==o ",
435
+ " \\ooo/ ",
436
+ " \\__/ "
435
437
  ];
436
438
 
437
- /** Twist animation frames for the compact DNA Claw: the claw silhouette stays
438
- * fixed while the inner helix lattice rotates. Frame 0 === DNA_CLAW_ART, so a
439
- * frameless render is byte-identical to the static symbol. All lines are the
440
- * same width (18) and every glyph is display-width 1. */
441
- export const DNA_CLAW_FRAMES: string[][] = [
442
- DNA_CLAW_ART,
439
+ /** Claw-snap blink frames for the compact crayfish forge mark: the arms, head and
440
+ * carapace stay fixed while the two pincer jaws SNAP shut (◣◣/◢◢ open ◢◣/◢◣
441
+ * closed), so the crayfish "clicks" its claws. Frame 0 === FORGE_MARK_ART, so a
442
+ * frameless render is byte-identical to the static symbol. All lines share the
443
+ * same width and width-1 glyphs. */
444
+ export const FORGE_MARK_FRAMES: string[][] = [
445
+ FORGE_MARK_ART,
443
446
  [
444
- " ╭╯ ◆ ◆ ╰╮ ",
445
- " ╭╯ ╲╱ ╲╱ ╰╮ ",
446
- " ║ ╳ ╳ ",
447
- " ╰╮ ╱ ╳ ╲ ╭╯ ",
448
- " ╰╮ ╳ ╳ ╭╯ ",
449
- " ╚══○ ○══╝ ",
450
- " ║ ║ ",
451
- " [ DNA Claw ] "
452
- ],
453
- [
454
- " ╭╯ ◆ ◆ ╰╮ ",
455
- " ╭╯ ╱╲ ╱╲ ╰╮ ",
456
- " ║ ╳ ╳ ║ ",
457
- " ╰╮ ╲ ╳ ╱ ╭╯ ",
458
- " ╰╮ ╳ ╳ ╭╯ ",
459
- " ╚══○ ○══╝ ",
460
- " ║ ║ ",
461
- " [ DNA Claw ] "
447
+ " ◢◣ ◢◣ ",
448
+ " ◆══╲ ╱══◆ ",
449
+ " ╲◉◉◉╱ ",
450
+ " ╲▔▔╱ "
462
451
  ]
463
452
  ];
464
453
 
465
- export const DNA_CLAW_FRAMES_ASCII: string[][] = [
466
- DNA_CLAW_ART_ASCII,
467
- [
468
- " /{ * * }\\ ",
469
- " /{ \\ / \\ / }\\ ",
470
- " | X X | ",
471
- " \\{ / X \\ }/ ",
472
- " \\{ X X }/ ",
473
- " \\==o o==/ ",
474
- " | | ",
475
- " [ DNA Claw ] "
476
- ],
454
+ export const FORGE_MARK_FRAMES_ASCII: string[][] = [
455
+ FORGE_MARK_ART_ASCII,
477
456
  [
478
- " /{ * * }\\ ",
479
- " /{ / \\ / \\ }\\ ",
480
- " | X X | ",
481
- " \\{ \\ X / }/ ",
482
- " \\{ X X }/ ",
483
- " \\==o o==/ ",
484
- " | | ",
485
- " [ DNA Claw ] "
457
+ " >< >< ",
458
+ " o==\\ /==o ",
459
+ " \\ooo/ ",
460
+ " \\__/ "
486
461
  ]
487
462
  ];
488
463
 
489
- /** Number of twist frames in the compact DNA Claw animation cycle. */
490
- export function dnaClawFrameCount(): number {
491
- return DNA_CLAW_FRAMES.length;
464
+ /** Number of blink frames in the compact forge-mark animation cycle. */
465
+ export function forgeMarkFrameCount(): number {
466
+ return FORGE_MARK_FRAMES.length;
492
467
  }
493
468
 
494
- /** Grand hero variant for the welcome forge box (gjc-style spacious banner):
495
- * a wide claw whose pincers frame a twisting DNA helix. Width-1 glyphs only
496
- * (box drawing + diagonals + geometrics) so padding/centering math stays exact. */
497
- export const DNA_CLAW_ART_GRAND: string[] = [
498
- " ◆◆ ◆◆ ",
499
- " ╭──╯╰──╮ ╭──╯╰──╮ ",
500
- " ╭╯ ╰╮ ╲╲ ╱╱ ╭╯ ╰╮ ",
501
- " ╭╯ ║ ╲╳╳╱ ║ ╰╮ ",
502
- " ║ ║ ╳╳ ║ ║ ",
503
- " ║ ║ ╱╳╳╲ ║ ║ ",
504
- " ╰╮ ║ ╱╱ ╲╲ ║ ╭╯ ",
505
- " ╰╮ ║ ╲╲ ╱╱ ║ ╭╯ ",
506
- " ╰──╮ ║ ╲╳╳╱ ║ ╭──╯ ",
507
- " ╰════○ ╳╳ ○════╯ ",
508
- " ║ ╱╳╳╲ ║ ",
509
- " [ D N A · C L A W ] "
469
+ /** Grand hero variant for the welcome forge box (gjc-style spacious banner): the
470
+ * same mascot crayfish rendered large and wordless the two raised pincer claws
471
+ * (◣◣ / ◢◢, the 집게 feature) on wide arms (◆══╲ ╱══◆), the head with its glowing
472
+ * eye/terminal cluster (◉ ◉), and the broad rounded carapace/tail (╲▔▔▔▔▔╱).
473
+ * Purely symbolic — NO embedded lettering or caption. Width-1 glyphs only so
474
+ * padding/centering math stays exact. */
475
+ export const FORGE_MARK_ART_GRAND: string[] = [
476
+ " ◣◣ ◢◢ ",
477
+ " ◆══╲ ╱══◆ ",
478
+ " ╲ ◉ ◉ ╱ ",
479
+ " ╲▔▔▔▔▔▔▔▔▔╱ "
510
480
  ];
511
481
 
512
- export const DNA_CLAW_ART_GRAND_ASCII: string[] = [
513
- " ** ** ",
514
- " /--'`--\\ /--'`--\\ ",
515
- " /' `\\ \\\\ // /' `\\ ",
516
- " /' | \\XX/ | `\\ ",
517
- " | | XX | | ",
518
- " | | /XX\\ | | ",
519
- " \\, | // \\\\ | ,/ ",
520
- " \\, | \\\\ // | ,/ ",
521
- " \\--, | \\XX/ | ,--/ ",
522
- " \\====o XX o====/ ",
523
- " | /XX\\ | ",
524
- " [ D N A . C L A W ] "
482
+ export const FORGE_MARK_ART_GRAND_ASCII: string[] = [
483
+ " // \\\\ ",
484
+ " o==\\ /==o ",
485
+ " \\ o o o / ",
486
+ " \\_________/ "
525
487
  ];
526
488
 
527
- // Bounded memo of fully-rendered DNA Claw frames keyed by every input that affects
489
+
490
+ // Bounded memo of fully-rendered forge-mark frames keyed by every input that affects
528
491
  // output (grand/unicode/cols/color/colorLevel/phase/frame). The live HUD cycles a
529
- // FIXED ~60-frame set (3 twists × 20 gradient phases) at ~120ms; without this each
530
- // recurrence recomputed per-line animatedGradientText (ANSI gradient) from scratch.
531
- // The memo makes the 2nd+ cycle O(1) lookups, cutting steady-state HUD CPU. LRU-capped.
532
- const dnaClawMemo = new Map<string, string[]>();
533
- const DNA_CLAW_MEMO_CAP = 256;
534
- const EMPTY_DNA_FRAME: string[] = [];
492
+ // FIXED frame set (blink × gradient phases) at ~120ms; without this each recurrence
493
+ // recomputed per-line animatedGradientText (ANSI gradient) from scratch. The memo
494
+ // makes the 2nd+ cycle O(1) lookups, cutting steady-state HUD CPU. LRU-capped.
495
+ const forgeMarkMemo = new Map<string, string[]>();
496
+ const FORGE_MARK_MEMO_CAP = 256;
497
+ const EMPTY_FORGE_FRAME: string[] = [];
535
498
 
536
- export function renderDnaClaw(opts: {
499
+ export function renderForgeMark(opts: {
537
500
  cols?: number;
538
501
  phase?: number;
539
502
  unicode?: boolean;
@@ -541,34 +504,35 @@ export function renderDnaClaw(opts: {
541
504
  colorLevel?: ColorLevel;
542
505
  /** Grand hero variant (welcome forge box); default is the compact in-turn symbol. */
543
506
  grand?: boolean;
544
- /** Twist-animation frame (compact symbol only; wraps). The helix lattice rotates
545
- * while the claw silhouette stays fixed — combined with the flowing gradient
546
- * `phase` this animates the forge identity without any frame-count growth. */
507
+ /** Blink-animation frame (compact symbol only; wraps). The cursor blinks and the
508
+ * status lamps swap while the window silhouette stays fixed — combined with the
509
+ * flowing gradient `phase` this animates the forge identity without any
510
+ * frame-count growth. */
547
511
  frame?: number;
548
512
  }): string[] {
549
513
  const memoKey = `${opts.grand ? "g" : "c"}|${opts.unicode !== false ? 1 : 0}|${opts.cols ?? -1}|${opts.color !== false ? 1 : 0}|${opts.colorLevel ?? ColorLevel.TrueColor}|${opts.phase ?? 0}|${opts.frame ?? 0}`;
550
- const memoHit = dnaClawMemo.get(memoKey);
514
+ const memoHit = forgeMarkMemo.get(memoKey);
551
515
  if (memoHit) return memoHit;
552
516
  const useUnicode = opts.unicode !== false;
553
517
  let source: string[];
554
518
  if (opts.grand) {
555
- source = useUnicode ? DNA_CLAW_ART_GRAND : DNA_CLAW_ART_GRAND_ASCII;
519
+ source = useUnicode ? FORGE_MARK_ART_GRAND : FORGE_MARK_ART_GRAND_ASCII;
556
520
  } else {
557
- const frames = useUnicode ? DNA_CLAW_FRAMES : DNA_CLAW_FRAMES_ASCII;
521
+ const frames = useUnicode ? FORGE_MARK_FRAMES : FORGE_MARK_FRAMES_ASCII;
558
522
  const f = Math.abs(Math.trunc(opts.frame ?? 0)) % frames.length;
559
523
  source = frames[f]!;
560
524
  }
561
525
  const width = Math.max(0, ...source.map(l => l.length));
562
526
 
563
527
  if (opts.cols !== undefined && opts.cols < width) {
564
- dnaClawMemo.set(memoKey, EMPTY_DNA_FRAME);
565
- return EMPTY_DNA_FRAME;
528
+ forgeMarkMemo.set(memoKey, EMPTY_FORGE_FRAME);
529
+ return EMPTY_FORGE_FRAME;
566
530
  }
567
531
 
568
532
  const phase = opts.phase ?? 0;
569
533
  const useColor = opts.color !== false;
570
534
  const colorLevel = opts.colorLevel ?? ColorLevel.TrueColor;
571
- const palette = DNA_FLOW_PALETTE;
535
+ const palette = FORGE_FLOW_PALETTE;
572
536
 
573
537
  const result = source.map((line, idx) => {
574
538
  const padded = line.length < width ? line + " ".repeat(width - line.length) : line;
@@ -578,19 +542,19 @@ export function renderDnaClaw(opts: {
578
542
  return animatedGradientText(padded, palette, phase + idx * 0.07, { colorLevel });
579
543
  });
580
544
 
581
- if (dnaClawMemo.size >= DNA_CLAW_MEMO_CAP) {
582
- const oldest = dnaClawMemo.keys().next().value;
583
- if (oldest !== undefined) dnaClawMemo.delete(oldest);
545
+ if (forgeMarkMemo.size >= FORGE_MARK_MEMO_CAP) {
546
+ const oldest = forgeMarkMemo.keys().next().value;
547
+ if (oldest !== undefined) forgeMarkMemo.delete(oldest);
584
548
  }
585
- dnaClawMemo.set(memoKey, result);
549
+ forgeMarkMemo.set(memoKey, result);
586
550
  return result;
587
551
  }
588
552
 
589
- /** The jeo identity palette — the mascot's synthwave neon read straight off the
590
- * character: blue lens → violet gown → pink lens. Shared by the claw art and the
591
- * forge-card border flow so the whole brand glows in the wizard's signature
592
- * blue→violet→pink (the dual neon lenses bracketing the gown). */
593
- export const DNA_FLOW_PALETTE: readonly string[] = ["#48dbfb", "#8e44ad", "#f368e0"];
553
+ /** The jeo identity palette — the mascot crayfish's synthwave neon read straight
554
+ * off the character: blue antennae glow → violet carapace → pink claw tips.
555
+ * Shared by the forge mark and the forge-card border flow so the whole brand
556
+ * glows in the crayfish-wizard's signature blue→violet→pink shell sheen. */
557
+ export const FORGE_FLOW_PALETTE: readonly string[] = ["#48dbfb", "#8e44ad", "#f368e0"];
594
558
 
595
559
  /** Width-1 forge title-mark glyph cycling the mascot's `jeo>` terminal prompt:
596
560
  * a prompt caret then a blinking block cursor (filled → hollow), echoing the
@@ -601,6 +565,6 @@ export function forgeBeat(frame: number, unicode = true): string {
601
565
  return beats[Math.abs(Math.trunc(frame)) % beats.length]!;
602
566
  }
603
567
 
604
- export function dnaClawHeight(): number {
605
- return DNA_CLAW_ART.length;
568
+ export function forgeMarkHeight(): number {
569
+ return FORGE_MARK_ART.length;
606
570
  }
@@ -7,6 +7,12 @@ import { SelectList, renderSelectList, type SelectItem, type RenderSelectOptions
7
7
  import type { ProviderStatus } from "../../ai/provider-status";
8
8
  import type { ProviderName } from "../../ai/types";
9
9
  import { companyLabel } from "../../ai/model-catalog";
10
+ import { SUBSCRIPTION_PROVIDER_NAMES } from "../../ai/providers/openai-compatible-catalog";
11
+
12
+ /** True for subscription/plan-tier providers (coding-plan, portal, token-plan, code). */
13
+ export function isSubscriptionProvider(name: ProviderName): boolean {
14
+ return (SUBSCRIPTION_PROVIDER_NAMES as readonly string[]).includes(name);
15
+ }
10
16
 
11
17
  /** Right-aligned hint for a provider row: credential kind + base URL + readiness. */
12
18
  export function providerHint(s: ProviderStatus, unicode = true): string {
@@ -43,3 +49,159 @@ export function providerPicker(statuses: ProviderStatus[], unicode = true): Sele
43
49
  export function renderProviderPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
44
50
  return renderSelectList(list, { title: "Select a provider", rows: 8, ...opts });
45
51
  }
52
+ /** Relative expiry label for a stored OAuth token, e.g. "expires in 42m" / "expired". */
53
+ export function loginExpiryLabel(expires: number | undefined, now: number = Date.now()): string | undefined {
54
+ if (!expires) return undefined;
55
+ const ms = expires - now;
56
+ if (ms <= 0) return "expired";
57
+ const mins = Math.round(ms / 60000);
58
+ if (mins < 60) return `expires in ${mins}m`;
59
+ return `expires in ${Math.round(mins / 60)}h`;
60
+ }
61
+
62
+ /** Right-aligned hint for a `/login` row: live OAuth login status (account + expiry)
63
+ * rather than generic readiness. Logged-in providers show a check + account/expiry;
64
+ * others show a muted "not logged in". gjc-parity for the login selector. */
65
+ export function loginHint(s: ProviderStatus, unicode = true): string {
66
+ if (!s.loggedIn) return unicode ? "\u00b7 not logged in" : "not logged in";
67
+ const parts: string[] = [unicode ? "\u2713 logged in" : "logged in"];
68
+ if (s.oauthEmail) parts.push(s.oauthEmail);
69
+ const expiry = loginExpiryLabel(s.oauthExpires);
70
+ if (expiry) parts.push(expiry);
71
+ return parts.join(" \u00b7 ");
72
+ }
73
+
74
+ /** Build `/login` choices: logged-in providers first, each row badged with its live
75
+ * OAuth login status (account/expiry). Pure builder mirroring gjc's OAuth selector. */
76
+ export function buildLoginChoices(statuses: ProviderStatus[], unicode = true): SelectItem<ProviderName>[] {
77
+ const sorted = [...statuses].sort((a, b) => (!!a.loggedIn === !!b.loggedIn ? 0 : a.loggedIn ? -1 : 1));
78
+ return sorted.map(s => ({
79
+ value: s.name,
80
+ label: `${s.name} (${companyLabel(s.name)})`,
81
+ group: s.loggedIn ? "logged in" : "not logged in",
82
+ hint: loginHint(s, unicode),
83
+ }));
84
+ }
85
+
86
+ /** Construct a ready-to-drive `SelectList` for the `/login` flow. */
87
+ export function loginPicker(statuses: ProviderStatus[], unicode = true): SelectList<ProviderName> {
88
+ return new SelectList(buildLoginChoices(statuses, unicode));
89
+ }
90
+
91
+ /** Right-aligned hint for a subscription-provider row: whether its key/token is stored,
92
+ * plus the env var that seeds it. Subscriptions authenticate by token, not OAuth. */
93
+ export function subscriptionHint(s: ProviderStatus, unicode = true): string {
94
+ const set = s.kind === "api_key";
95
+ const badge = set ? (unicode ? "\u2713 active" : "active") : (unicode ? "\u00b7 no token" : "no token");
96
+ return s.envVar ? `${badge} \u00b7 ${s.envVar}` : badge;
97
+ }
98
+
99
+ /** Build the combined "OAuth / subscription" login choices: OAuth providers (logged-in
100
+ * first, badged with account/expiry) followed by subscription-tier providers (active
101
+ * first, badged with token status). Pure builder mirroring gjc's onboarding selector. */
102
+ export function buildSubscriptionLoginChoices(
103
+ oauthStatuses: ProviderStatus[],
104
+ subscriptionStatuses: ProviderStatus[],
105
+ unicode = true,
106
+ ): SelectItem<ProviderName>[] {
107
+ const oauth = [...oauthStatuses]
108
+ .sort((a, b) => (!!a.loggedIn === !!b.loggedIn ? 0 : a.loggedIn ? -1 : 1))
109
+ .map(s => ({
110
+ value: s.name,
111
+ label: `${s.name} (${companyLabel(s.name)})`,
112
+ group: "OAuth login",
113
+ hint: loginHint(s, unicode),
114
+ }));
115
+ const subs = [...subscriptionStatuses]
116
+ .sort((a, b) => ((a.kind === "api_key") === (b.kind === "api_key") ? 0 : a.kind === "api_key" ? -1 : 1))
117
+ .map(s => ({
118
+ value: s.name,
119
+ label: `${s.name} (${companyLabel(s.name)})`,
120
+ group: "subscription / plan",
121
+ hint: subscriptionHint(s, unicode),
122
+ }));
123
+ return [...oauth, ...subs];
124
+ }
125
+
126
+ /** Construct a ready-to-drive `SelectList` for the combined OAuth / subscription login flow. */
127
+ export function subscriptionLoginPicker(
128
+ oauthStatuses: ProviderStatus[],
129
+ subscriptionStatuses: ProviderStatus[],
130
+ unicode = true,
131
+ ): SelectList<ProviderName> {
132
+ return new SelectList(buildSubscriptionLoginChoices(oauthStatuses, subscriptionStatuses, unicode));
133
+ }
134
+
135
+ /** Render a login picker `SelectList` with a sensible default title. */
136
+ export function renderLoginPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
137
+ return renderSelectList(list, { title: "Select provider to login", rows: 8, ...opts });
138
+ }
139
+
140
+ /** The ways to onboard a provider, mirroring gjc's `/provider` onboarding selector:
141
+ * log in to an OAuth/subscription provider, register an API-compatible endpoint, or
142
+ * store an API key for one of the bundled API-key-only providers (groq, deepseek, …). */
143
+ export type OnboardingAction = "oauth-login" | "api-key" | "api-add";
144
+
145
+ /** Build the bare-`/provider` onboarding choices (gjc-parity interactive selector).
146
+ * Pure builder: OAuth-login first (the common path), then API-key providers, then a
147
+ * custom API-compatible endpoint. */
148
+ export function buildOnboardingChoices(unicode = true): SelectItem<OnboardingAction>[] {
149
+ const arrow = unicode ? "\u2192 " : "";
150
+ return [
151
+ {
152
+ value: "oauth-login",
153
+ label: "Login with OAuth / subscription",
154
+ hint: `${arrow}OAuth providers + subscription / plan tokens`,
155
+ },
156
+ {
157
+ value: "api-key",
158
+ label: "Set an API key for a provider",
159
+ hint: `${arrow}groq, deepseek, mistral, openrouter, …`,
160
+ },
161
+ {
162
+ value: "api-add",
163
+ label: "Add an API-compatible endpoint",
164
+ hint: `${arrow}/provider add --base-url <url>`,
165
+ },
166
+ ];
167
+ }
168
+
169
+ /** Construct a ready-to-drive `SelectList` for the bare-`/provider` onboarding flow. */
170
+ export function onboardingPicker(unicode = true): SelectList<OnboardingAction> {
171
+ return new SelectList(buildOnboardingChoices(unicode));
172
+ }
173
+
174
+ /** Render the onboarding picker `SelectList` with a sensible default title. */
175
+ export function renderOnboardingPicker(list: SelectList<OnboardingAction>, opts: RenderSelectOptions = {}): string[] {
176
+ return renderSelectList(list, { title: "Provider onboarding \u2191\u2193 move \u00b7 Enter select \u00b7 Esc cancel", rows: 4, ...opts });
177
+ }
178
+
179
+ /** Right-aligned hint for an API-key provider row: whether a key is stored, plus the
180
+ * env var that seeds it. Mirrors `loginHint` but for keyed (no-OAuth) providers. */
181
+ export function apiKeyHint(s: ProviderStatus, unicode = true): string {
182
+ const set = s.kind === "api_key";
183
+ const badge = set ? (unicode ? "\u2713 key set" : "key set") : (unicode ? "\u00b7 no key" : "no key");
184
+ return s.envVar ? `${badge} \u00b7 ${s.envVar}` : badge;
185
+ }
186
+
187
+ /** Build `/provider` API-key choices: providers with a stored key first, each badged with
188
+ * its key status + env var. Pure builder mirroring the OAuth login selector. */
189
+ export function buildApiKeyChoices(statuses: ProviderStatus[], unicode = true): SelectItem<ProviderName>[] {
190
+ const sorted = [...statuses].sort((a, b) => (a.kind === "api_key") === (b.kind === "api_key") ? 0 : a.kind === "api_key" ? -1 : 1);
191
+ return sorted.map(s => ({
192
+ value: s.name,
193
+ label: `${s.name} (${companyLabel(s.name)})`,
194
+ group: s.kind === "api_key" ? "key set" : "needs key",
195
+ hint: apiKeyHint(s, unicode),
196
+ }));
197
+ }
198
+
199
+ /** Construct a ready-to-drive `SelectList` for the API-key onboarding flow. */
200
+ export function apiKeyPicker(statuses: ProviderStatus[], unicode = true): SelectList<ProviderName> {
201
+ return new SelectList(buildApiKeyChoices(statuses, unicode));
202
+ }
203
+
204
+ /** Render the API-key provider picker `SelectList` with a sensible default title. */
205
+ export function renderApiKeyPicker(list: SelectList<ProviderName>, opts: RenderSelectOptions = {}): string[] {
206
+ return renderSelectList(list, { title: "Select a provider to key", rows: 8, ...opts });
207
+ }
@@ -37,8 +37,8 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
37
37
  { command: "/goal", usage: "/goal <condition>", description: "Set a natural language stop condition for the session", group: "session" },
38
38
  { command: "/model", usage: "/model [id|#N|save|thinking <level>|subagent <role> <model|#N|thinking L>]", description: "Show/switch model; picker can apply to default or any subagent role and set thinking", group: "models" },
39
39
  { command: "/fast", usage: "/fast [on|off|status]", description: "Toggle fast thinking mode when the active model supports it", group: "models" },
40
- { command: "/provider", usage: "/provider [login [name] | add --base-url <url> [--model <m>]]", description: "Provider onboarding: `login [name]` starts OAuth; `add --base-url <url>` registers an OpenAI-compatible endpoint. Switch the active model/provider with /model", group: "models" },
41
- { command: "/login", usage: "/login [provider]", description: "OAuth login (alias of /provider login)", group: "models" },
40
+ { command: "/provider", usage: "/provider [login [name] | key [name] [key] | add --base-url <url> [--model <m>]]", description: "Provider onboarding: `login [name]` starts OAuth; `key [name]` stores an API key (groq, deepseek, …); `add --base-url <url>` registers an OpenAI-compatible endpoint. Switch the active model/provider with /model", group: "models" },
41
+ { command: "/login", usage: "/login [provider]", description: "OAuth login — opens a provider picker showing live login status (account · expiry) for each provider (alias of /provider login)", group: "models" },
42
42
  { command: "/logout", usage: "/logout <anthropic|openai|gemini|antigravity>", description: "Remove the stored OAuth token for a provider", group: "models" },
43
43
  { command: "/roles", usage: "/roles [tier model]", description: "Show or set model role tiers (smol/slow/plan)", group: "models" },
44
44
  { command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (minimal/low/medium/high/xhigh)", group: "models" },
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { renderDnaClaw, DNA_CLAW_ART_GRAND } from "./ascii-art";
2
+ import { renderForgeMark, FORGE_MARK_ART_GRAND } from "./ascii-art";
3
3
  import { truncate, isTTY } from "../terminal";
4
4
  import { detectColorLevel, ColorLevel } from "./color";
5
5
 
@@ -13,7 +13,7 @@ export interface WelcomeData {
13
13
  contextFiles?: string[]; // project context file paths (render basenames)
14
14
  recentSessions?: { name: string; timeAgo: string }[];
15
15
  cols?: number; // default 80
16
- /** Gradient phase [0..1) for the DNA Claw symbol — drives the launch sweep animation. */
16
+ /** Gradient phase [0..1) for the forge mark — drives the launch sweep animation. */
17
17
  phase?: number;
18
18
  /** Lit-edge painter (top border + left edge); theme accent. Default gray. */
19
19
  accent?: (s: string) => string;
@@ -42,7 +42,7 @@ function padLine(line: string, width: number, align: "left" | "center" | "right"
42
42
  /**
43
43
  * The gjc-style hero welcome box ("JEO forge"): one outer box with the version
44
44
  * embedded in the top border and a SINGLE CENTERED column inside — brand line,
45
- * tagline, the grand DNA Claw symbol (flowing gradient on capable terminals),
45
+ * tagline, the grand jeo forge mark (flowing gradient on capable terminals),
46
46
  * and the model/provider pills. Workspace details and key hints intentionally
47
47
  * live elsewhere (footer/status bar), matching the gjc forge banner.
48
48
  */
@@ -57,8 +57,8 @@ export function renderWelcome(d: WelcomeData): string[] {
57
57
 
58
58
  // The banner fills the full terminal width (gjc forge: flush with the input box and
59
59
  // status bar below it). `cols - 1` leaves the last column free so a full-width row
60
- // never wraps; the DNA-claw + pills stay centered inside the box.
61
- const grandWidth = Math.max(...DNA_CLAW_ART_GRAND.map(l => l.length));
60
+ // never wraps; the forge mark + pills stay centered inside the box.
61
+ const grandWidth = Math.max(...FORGE_MARK_ART_GRAND.map(l => l.length));
62
62
  // Title rides ON the top border: `─── jeo v{version} · JEO forge ───`. Defined
63
63
  // once here so the width calc and the border render below can't drift.
64
64
  const titleDashes = 3;
@@ -93,10 +93,10 @@ export function renderWelcome(d: WelcomeData): string[] {
93
93
  const bottomBorderPlain = g.bl + g.h.repeat(inner) + g.br;
94
94
  const bottomBorderLine = shadow(bottomBorderPlain);
95
95
 
96
- // Grand symbol when the box is wide enough; compact DNA Claw otherwise.
96
+ // Grand symbol when the box is wide enough; compact forge mark otherwise.
97
97
  const colorLevel = useColor ? detectColorLevel(process.env, isTTY()) : ColorLevel.None;
98
98
  const grand = inner >= grandWidth;
99
- const artLines = renderDnaClaw({
99
+ const artLines = renderForgeMark({
100
100
  color: useColor,
101
101
  phase: d.phase ?? 0,
102
102
  unicode,
@@ -136,7 +136,7 @@ export function renderWelcome(d: WelcomeData): string[] {
136
136
  }
137
137
 
138
138
  /**
139
- * Launch animation: sweep the DNA Claw's gradient through `cycles` FULL palette
139
+ * Launch animation: sweep the forge mark's gradient through `cycles` FULL palette
140
140
  * cycles by re-printing the welcome box in place (cursor-up rewrites, same row
141
141
  * count every frame). The loop is SEAMLESS — the phase wraps exactly at each
142
142
  * cycle boundary with a constant frame delay, so consecutive cycles join with