jeo-code 0.6.22 → 0.6.24

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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.ja.md +6 -2
  3. package/README.ko.md +6 -2
  4. package/README.md +6 -2
  5. package/README.zh.md +6 -2
  6. package/package.json +1 -1
  7. package/src/agent/config-schema.ts +12 -0
  8. package/src/agent/session.ts +10 -3
  9. package/src/agent/state.ts +19 -14
  10. package/src/ai/index.ts +1 -0
  11. package/src/ai/model-catalog.ts +121 -1
  12. package/src/ai/model-discovery.ts +55 -3
  13. package/src/ai/model-manager.ts +43 -11
  14. package/src/ai/model-registry.ts +2 -0
  15. package/src/ai/provider-status.ts +45 -7
  16. package/src/ai/providers/anthropic-compatible.ts +27 -0
  17. package/src/ai/providers/anthropic.ts +3 -1
  18. package/src/ai/providers/antigravity.ts +31 -6
  19. package/src/ai/providers/gemini.ts +45 -4
  20. package/src/ai/providers/kimi.ts +18 -0
  21. package/src/ai/providers/lmstudio.ts +8 -0
  22. package/src/ai/providers/ollama.ts +17 -5
  23. package/src/ai/providers/openai-compatible-catalog.ts +83 -0
  24. package/src/ai/providers/openai-compatible.ts +34 -0
  25. package/src/ai/providers/openai-responses.ts +11 -0
  26. package/src/ai/providers/openai.ts +115 -7
  27. package/src/ai/providers/xai.ts +18 -0
  28. package/src/ai/register-providers.ts +18 -0
  29. package/src/ai/think-tags.ts +84 -0
  30. package/src/ai/types.ts +11 -1
  31. package/src/auth/flows/index.ts +3 -3
  32. package/src/auth/index.ts +4 -1
  33. package/src/auth/oauth.ts +3 -3
  34. package/src/auth/refresh.ts +5 -0
  35. package/src/auth/storage.ts +12 -1
  36. package/src/commands/auth.ts +21 -2
  37. package/src/commands/launch/flags.ts +5 -1
  38. package/src/commands/launch/input.ts +13 -0
  39. package/src/commands/launch.ts +307 -26
  40. package/src/commands/setup.ts +3 -2
  41. package/src/tui/app.ts +61 -41
  42. package/src/tui/components/ascii-art.ts +91 -124
  43. package/src/tui/components/autocomplete.ts +16 -0
  44. package/src/tui/components/forge.ts +1 -1
  45. package/src/tui/components/provider-picker.ts +162 -0
  46. package/src/tui/components/slash.ts +2 -2
  47. package/src/tui/components/transcript.ts +7 -0
  48. package/src/tui/components/welcome.ts +8 -8
  49. package/src/tui/components/width.ts +21 -0
@@ -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" },
@@ -156,6 +156,13 @@ export function formatTranscript(messages: readonly Message[], opts: TranscriptO
156
156
  ? ""
157
157
  : m.content;
158
158
  if (!reason.trim()) continue;
159
+ // Persisted thinking (gjc "think → answer" order): show the turn's reasoning,
160
+ // dimmed, above the reply so the durable record carries it across /resume + export.
161
+ if (m.reasoning?.trim()) {
162
+ if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
163
+ lines.push(dim(`${unicode ? "◇" : "*"} thinking`));
164
+ for (const l of clipBody(m.reasoning.trim(), bodyCap)) lines.push(dim(l));
165
+ }
159
166
  if (lines.length > 0 && lines[lines.length - 1] !== "") {
160
167
  lines.push("");
161
168
  }
@@ -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
@@ -246,3 +246,24 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
246
246
  }
247
247
  return out;
248
248
  }
249
+
250
+ /**
251
+ * Single-slot memo for the live-frame wrap. The TUI's 120ms spinner tick re-renders
252
+ * the whole frame ~8×/s, but the reasoning / tool-output stream text only changes when
253
+ * a new delta arrives — so re-wrapping (grapheme-segmenting the up-to-16KB tail through
254
+ * `wrapTextWithAnsi`) on every idle tick is the hottest avoidable per-tick cost. This
255
+ * caches the most recent `key → value`: an unchanged frame reuses the prior wrap instead
256
+ * of recomputing it. One slot suffices — between two consecutive ticks the key (wrap
257
+ * width + text) is identical on the common path; a real change recomputes once.
258
+ */
259
+ export function lastValueCache<T>(): (key: string, compute: () => T) => T {
260
+ let lastKey: string | undefined;
261
+ let lastValue: T;
262
+ return (key, compute) => {
263
+ if (key !== lastKey) {
264
+ lastKey = key;
265
+ lastValue = compute();
266
+ }
267
+ return lastValue;
268
+ };
269
+ }