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.
- package/CHANGELOG.md +26 -0
- package/README.ja.md +6 -2
- package/README.ko.md +6 -2
- package/README.md +6 -2
- package/README.zh.md +6 -2
- package/package.json +1 -1
- package/src/agent/config-schema.ts +12 -0
- package/src/agent/session.ts +10 -3
- package/src/agent/state.ts +19 -14
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog.ts +121 -1
- package/src/ai/model-discovery.ts +55 -3
- package/src/ai/model-manager.ts +43 -11
- package/src/ai/model-registry.ts +2 -0
- package/src/ai/provider-status.ts +45 -7
- package/src/ai/providers/anthropic-compatible.ts +27 -0
- package/src/ai/providers/anthropic.ts +3 -1
- package/src/ai/providers/antigravity.ts +31 -6
- package/src/ai/providers/gemini.ts +45 -4
- package/src/ai/providers/kimi.ts +18 -0
- package/src/ai/providers/lmstudio.ts +8 -0
- package/src/ai/providers/ollama.ts +17 -5
- package/src/ai/providers/openai-compatible-catalog.ts +83 -0
- package/src/ai/providers/openai-compatible.ts +34 -0
- package/src/ai/providers/openai-responses.ts +11 -0
- package/src/ai/providers/openai.ts +115 -7
- package/src/ai/providers/xai.ts +18 -0
- package/src/ai/register-providers.ts +18 -0
- package/src/ai/think-tags.ts +84 -0
- package/src/ai/types.ts +11 -1
- package/src/auth/flows/index.ts +3 -3
- package/src/auth/index.ts +4 -1
- package/src/auth/oauth.ts +3 -3
- package/src/auth/refresh.ts +5 -0
- package/src/auth/storage.ts +12 -1
- package/src/commands/auth.ts +21 -2
- package/src/commands/launch/flags.ts +5 -1
- package/src/commands/launch/input.ts +13 -0
- package/src/commands/launch.ts +307 -26
- package/src/commands/setup.ts +3 -2
- package/src/tui/app.ts +61 -41
- package/src/tui/components/ascii-art.ts +91 -124
- package/src/tui/components/autocomplete.ts +16 -0
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/provider-picker.ts +162 -0
- package/src/tui/components/slash.ts +2 -2
- package/src/tui/components/transcript.ts +7 -0
- package/src/tui/components/welcome.ts +8 -8
- 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 {
|
|
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
|
|
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
|
|
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
|
|
61
|
-
const grandWidth = Math.max(...
|
|
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
|
|
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 =
|
|
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
|
|
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
|
+
}
|