jeo-code 0.6.23 → 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 +11 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/ai/provider-status.ts +19 -0
- package/src/ai/providers/openai-compatible-catalog.ts +19 -8
- package/src/ai/providers/openai-compatible.ts +4 -1
- package/src/ai/providers/openai-responses.ts +11 -0
- package/src/ai/providers/openai.ts +99 -7
- package/src/ai/register-providers.ts +1 -1
- package/src/ai/types.ts +5 -0
- package/src/commands/auth.ts +4 -2
- package/src/commands/launch.ts +230 -15
- package/src/tui/app.ts +11 -11
- package/src/tui/components/ascii-art.ts +85 -122
- package/src/tui/components/provider-picker.ts +162 -0
- package/src/tui/components/slash.ts +2 -2
- package/src/tui/components/welcome.ts +8 -8
package/src/commands/launch.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
|
17
17
|
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
18
18
|
import { formatForgeBox } from "../tui/components/forge";
|
|
19
19
|
import { interactiveOAuthLogin } from "./auth";
|
|
20
|
-
import { logoutOAuth } from "../auth";
|
|
20
|
+
import { logoutOAuth, OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS, setApiKey } from "../auth";
|
|
21
21
|
import type { AuthProvider } from "../auth";
|
|
22
22
|
import { matchSlash, isSlashAttempt, suggestSlashCommands, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
|
|
23
23
|
import { staticCompletionContext, readlineCompleter, formatCompletionPreview, formatMidTurnHint, tokenize, type CompletionContext } from "../tui/components/autocomplete";
|
|
@@ -41,7 +41,7 @@ import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLev
|
|
|
41
41
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
42
42
|
|
|
43
43
|
import { listAliases } from "../ai/model-registry";
|
|
44
|
-
import { openaiCompatDef } from "../ai/providers/openai-compatible-catalog";
|
|
44
|
+
import { openaiCompatDef, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
|
|
45
45
|
|
|
46
46
|
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting } from "../agent/subagents";
|
|
47
47
|
import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
} from "../tui/components/config-panel";
|
|
59
59
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
60
60
|
|
|
61
|
-
import {
|
|
61
|
+
import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
|
|
62
62
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
63
63
|
import { categoryBadge } from "../tui/components/category-index";
|
|
64
64
|
import { renderInputFrame, verticalCursorOffset } from "../tui/components/input-box";
|
|
@@ -214,7 +214,10 @@ export {
|
|
|
214
214
|
currentAtLabelFn as currentAtLabel,
|
|
215
215
|
};
|
|
216
216
|
export function normalizeSlashAlias(input: string): string {
|
|
217
|
-
|
|
217
|
+
// gjc-parity: bare `/login` opens the provider onboarding selector (same as bare
|
|
218
|
+
// `/provider`); `/login <provider|args>` is the direct OAuth-login alias.
|
|
219
|
+
if (input === "/login") return "/provider";
|
|
220
|
+
if (input.startsWith("/login ")) return `/provider login${input.slice("/login".length)}`;
|
|
218
221
|
if (input === "/settings") return "/config";
|
|
219
222
|
if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
|
|
220
223
|
if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
|
|
@@ -1093,7 +1096,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1093
1096
|
// gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
|
|
1094
1097
|
// never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
|
|
1095
1098
|
if (process.stdout.isTTY) process.stdout.write(clearScreen());
|
|
1096
|
-
// Launch sweep: the
|
|
1099
|
+
// Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
|
|
1097
1100
|
// cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
|
|
1098
1101
|
// Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
|
|
1099
1102
|
const sweepable =
|
|
@@ -2206,13 +2209,112 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2206
2209
|
|
|
2207
2210
|
|
|
2208
2211
|
const pickCloudProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
|
|
2209
|
-
const cloud = new Set(
|
|
2210
|
-
const
|
|
2212
|
+
const cloud = new Set<string>(OAUTH_PROVIDERS); // OAuth-login providers (anthropic/openai/gemini/antigravity)
|
|
2213
|
+
const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // subscription/plan products (token-keyed)
|
|
2214
|
+
const list = subscriptionLoginPicker(
|
|
2215
|
+
statuses.filter(s => cloud.has(s.name)),
|
|
2216
|
+
statuses.filter(s => subs.has(s.name)),
|
|
2217
|
+
true,
|
|
2218
|
+
);
|
|
2219
|
+
let chosen: ProviderName | undefined;
|
|
2220
|
+
await runSelectPicker(
|
|
2221
|
+
(cols, rows) =>
|
|
2222
|
+
renderLoginPicker(list, {
|
|
2223
|
+
title: "Login with OAuth / subscription ↑↓ move · Enter select · Esc cancel",
|
|
2224
|
+
cols,
|
|
2225
|
+
rows: Math.max(4, Math.min(rows, 8)),
|
|
2226
|
+
unicode: true,
|
|
2227
|
+
color: true,
|
|
2228
|
+
}),
|
|
2229
|
+
(ch, key) => {
|
|
2230
|
+
if (key?.name === "up") {
|
|
2231
|
+
list.up();
|
|
2232
|
+
return false;
|
|
2233
|
+
}
|
|
2234
|
+
if (key?.name === "down") {
|
|
2235
|
+
list.down();
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
if (key?.name === "pageup") {
|
|
2239
|
+
list.page(-1, 4);
|
|
2240
|
+
return false;
|
|
2241
|
+
}
|
|
2242
|
+
if (key?.name === "pagedown") {
|
|
2243
|
+
list.page(1, 4);
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
if (key?.name === "backspace") {
|
|
2247
|
+
list.backspace();
|
|
2248
|
+
return false;
|
|
2249
|
+
}
|
|
2250
|
+
if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
2254
|
+
chosen = list.selected()?.value;
|
|
2255
|
+
return true;
|
|
2256
|
+
}
|
|
2257
|
+
if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
|
|
2258
|
+
list.typeChar(ch);
|
|
2259
|
+
}
|
|
2260
|
+
return false;
|
|
2261
|
+
},
|
|
2262
|
+
);
|
|
2263
|
+
return chosen && (cloud.has(chosen) || subs.has(chosen)) ? chosen as AuthProvider : undefined;
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
// Bare `/provider` opens gjc's interactive onboarding selector: choose between
|
|
2267
|
+
// OAuth/subscription login and registering an API-compatible endpoint. Returns the
|
|
2268
|
+
// picked action, or undefined when cancelled (Esc/Ctrl+C). TTY only — callers fall
|
|
2269
|
+
// back to the printed usage in non-interactive mode.
|
|
2270
|
+
const pickOnboardingAction = async (): Promise<OnboardingAction | undefined> => {
|
|
2271
|
+
const list = onboardingPicker(true);
|
|
2272
|
+
let chosen: OnboardingAction | undefined;
|
|
2273
|
+
await runSelectPicker(
|
|
2274
|
+
(cols, rows) =>
|
|
2275
|
+
renderOnboardingPicker(list, {
|
|
2276
|
+
cols,
|
|
2277
|
+
rows: Math.max(4, Math.min(rows, 6)),
|
|
2278
|
+
unicode: true,
|
|
2279
|
+
color: true,
|
|
2280
|
+
}),
|
|
2281
|
+
(ch, key) => {
|
|
2282
|
+
if (key?.name === "up") {
|
|
2283
|
+
list.up();
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
if (key?.name === "down") {
|
|
2287
|
+
list.down();
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
|
|
2291
|
+
return true;
|
|
2292
|
+
}
|
|
2293
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
2294
|
+
chosen = list.selected()?.value;
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
|
|
2298
|
+
list.typeChar(ch);
|
|
2299
|
+
}
|
|
2300
|
+
return false;
|
|
2301
|
+
},
|
|
2302
|
+
);
|
|
2303
|
+
return chosen;
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
// API-key onboarding: pick one of the bundled API-key-only providers (groq, deepseek,
|
|
2307
|
+
// mistral, …) to store a key for. Returns the picked provider, or undefined on cancel.
|
|
2308
|
+
// TTY only — the caller prints scriptable guidance otherwise.
|
|
2309
|
+
const pickApiKeyProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
|
|
2310
|
+
const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // surfaced under OAuth/subscription login instead
|
|
2311
|
+
const keyed = new Set<string>(API_KEY_ONLY_PROVIDERS);
|
|
2312
|
+
const list = apiKeyPicker(statuses.filter(s => keyed.has(s.name) && !subs.has(s.name)), true);
|
|
2211
2313
|
let chosen: ProviderName | undefined;
|
|
2212
2314
|
await runSelectPicker(
|
|
2213
2315
|
(cols, rows) =>
|
|
2214
|
-
|
|
2215
|
-
title: "Select
|
|
2316
|
+
renderApiKeyPicker(list, {
|
|
2317
|
+
title: "Select a provider to key \u2191\u2193 move \u00b7 Enter select \u00b7 Esc cancel",
|
|
2216
2318
|
cols,
|
|
2217
2319
|
rows: Math.max(4, Math.min(rows, 8)),
|
|
2218
2320
|
unicode: true,
|
|
@@ -2252,9 +2354,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2252
2354
|
return false;
|
|
2253
2355
|
},
|
|
2254
2356
|
);
|
|
2255
|
-
return chosen &&
|
|
2357
|
+
return chosen && keyed.has(chosen) ? chosen as AuthProvider : undefined;
|
|
2256
2358
|
};
|
|
2257
2359
|
|
|
2360
|
+
|
|
2258
2361
|
if (previewEnabled) {
|
|
2259
2362
|
process.once("exit", () => out.write("\x1b[?25h")); // safety net: never leave the cursor hidden
|
|
2260
2363
|
const footerKeypressHandler = (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
|
|
@@ -3002,40 +3105,152 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3002
3105
|
}
|
|
3003
3106
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
3004
3107
|
const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
|
|
3005
|
-
|
|
3108
|
+
let name = (tokens[0] ?? "").toLowerCase();
|
|
3006
3109
|
// gjc-parity (semantic): /provider is ONBOARDING ONLY — set up OAuth credentials
|
|
3007
3110
|
// or an API-compatible endpoint. Switching the active provider/model lives in /model.
|
|
3008
3111
|
const providerOnboardingUsage = (): string[] => [
|
|
3009
3112
|
"Provider onboarding — set up credentials or an API-compatible endpoint:",
|
|
3010
|
-
" OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity] (alias: /login)",
|
|
3113
|
+
" OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity|<subscription>] (alias: /login)",
|
|
3114
|
+
" subscriptions (token): alibaba-coding-plan, qwen-portal, xiaomi-token-plan-*, minimax-code*",
|
|
3115
|
+
" API key (cloud) : /provider key [provider] [key] (groq, deepseek, mistral, openrouter, …)",
|
|
3011
3116
|
" API-compatible : /provider add --base-url <url> [--model <model>] [--compat openai] (reads OPENAI_API_KEY)",
|
|
3012
3117
|
" show current / clear: /provider add · /provider add clear",
|
|
3013
3118
|
" Logout : /logout <provider>",
|
|
3119
|
+
" Headless OAuth : paste the redirect URL or code when the login prompt asks.",
|
|
3014
3120
|
"Switch the active model or provider with /model.",
|
|
3015
3121
|
];
|
|
3122
|
+
// Bare `/provider` in an interactive TTY → gjc's interactive onboarding selector
|
|
3123
|
+
// (OAuth login vs API-compatible endpoint). The choice routes into the same
|
|
3124
|
+
// `login`/`add` branches below; cancel falls through to the printed readiness +
|
|
3125
|
+
// usage. Non-TTY / `help` keep the static panel (scriptable, unchanged).
|
|
3126
|
+
if (!name && process.stdin.isTTY && process.stdout.isTTY) {
|
|
3127
|
+
const action = await pickOnboardingAction();
|
|
3128
|
+
if (action === "oauth-login") name = "login";
|
|
3129
|
+
else if (action === "api-key") name = "key";
|
|
3130
|
+
else if (action === "api-add") {
|
|
3131
|
+
console.log("Add an API-compatible endpoint:");
|
|
3132
|
+
console.log(" /provider add --base-url <url> [--model <model>] [--compat openai]");
|
|
3133
|
+
console.log(" reads OPENAI_API_KEY · show current / clear: /provider add · /provider add clear");
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
// action === undefined (cancelled) → fall through to the readiness panel.
|
|
3137
|
+
}
|
|
3138
|
+
// `/provider key [name] [key]` → store an API key for an API-key-only provider
|
|
3139
|
+
// (groq/deepseek/mistral/…). Interactive: pick the provider, then paste the key.
|
|
3140
|
+
if (name === "key") {
|
|
3141
|
+
const keyed = new Set<string>(API_KEY_ONLY_PROVIDERS);
|
|
3142
|
+
let target = tokens.slice(1).map(t => t.toLowerCase()).find(t => keyed.has(t)) as AuthProvider | undefined;
|
|
3143
|
+
// A trailing token after the provider name is treated as the key itself.
|
|
3144
|
+
const inlineKey = target ? tokens.slice(1).filter(t => t.toLowerCase() !== target).pop() : undefined;
|
|
3145
|
+
if (!target) {
|
|
3146
|
+
const statuses = await describeAllProviders();
|
|
3147
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3148
|
+
target = await pickApiKeyProvider(statuses);
|
|
3149
|
+
} else {
|
|
3150
|
+
console.log("Set an API key for which provider?");
|
|
3151
|
+
console.log(` ${API_KEY_ONLY_PROVIDERS.filter(p => !(SUBSCRIPTION_PROVIDER_NAMES as readonly string[]).includes(p)).join(", ")}`);
|
|
3152
|
+
console.log(" Subscription / plan products use /provider login (token).");
|
|
3153
|
+
console.log(" Usage: /provider key <provider> <api-key> (or set <PROVIDER>_API_KEY)");
|
|
3154
|
+
}
|
|
3155
|
+
if (!target) {
|
|
3156
|
+
console.log("(cancelled)");
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
const envVar = `${target.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
3161
|
+
let apiKey = inlineKey;
|
|
3162
|
+
if (!apiKey) {
|
|
3163
|
+
apiKey = (await promptInput(`Paste ${target} API key (blank to cancel): `)).trim();
|
|
3164
|
+
}
|
|
3165
|
+
if (!apiKey) {
|
|
3166
|
+
console.log("(cancelled — no key entered)");
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
await setApiKey(target, apiKey);
|
|
3170
|
+
console.log(`[SUCCESS] Stored ${target} API key in ~/.jeo/config.json (also reads ${envVar}).`);
|
|
3171
|
+
const live = await refreshLiveModelsCache();
|
|
3172
|
+
const after = (await describeAllProviders()).find(s => s.name === target);
|
|
3173
|
+
if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
|
|
3174
|
+
const forProvider = live.filter(r => r.provider === target);
|
|
3175
|
+
if (forProvider.some(r => r.ok && r.models.length > 0)) {
|
|
3176
|
+
lastPickIndex = flattenModels(forProvider);
|
|
3177
|
+
const viaCatalog = forProvider.some(r => r.fallback);
|
|
3178
|
+
console.log(` ${viaCatalog ? "catalog" : "live"} ${target} models → /model #N${viaCatalog ? " (live list endpoint unavailable; showing known models)" : ""}`);
|
|
3179
|
+
logLines(formatPickListWithCapabilities(lastPickIndex, { cap: 12 }));
|
|
3180
|
+
} else {
|
|
3181
|
+
const failed = forProvider.find(r => !r.ok);
|
|
3182
|
+
if (failed?.error) console.log(` live ${target} models unavailable: ${failed.error}`);
|
|
3183
|
+
}
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3016
3186
|
// `/provider login|auth [name]` → run OAuth login from the REPL.
|
|
3017
3187
|
if (name === "login" || name === "auth") {
|
|
3018
3188
|
const cloud = ["anthropic", "openai", "gemini", "antigravity"] as const;
|
|
3019
|
-
|
|
3189
|
+
const subs = new Set<string>(SUBSCRIPTION_PROVIDER_NAMES); // token-keyed subscription/plan products
|
|
3190
|
+
let target = tokens.slice(1).map(t => t.toLowerCase()).find(t => (cloud as readonly string[]).includes(t) || subs.has(t));
|
|
3020
3191
|
if (!target) {
|
|
3021
3192
|
const statuses = await describeAllProviders();
|
|
3022
3193
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3023
3194
|
target = await pickCloudProvider(statuses);
|
|
3024
3195
|
} else {
|
|
3025
3196
|
console.log("Log in to which provider?");
|
|
3197
|
+
console.log(" OAuth:");
|
|
3026
3198
|
cloud.forEach((p, i) => {
|
|
3027
3199
|
const st = statuses.find(s => s.name === p);
|
|
3028
|
-
|
|
3200
|
+
const badge = st?.loggedIn
|
|
3201
|
+
? `\u2713 logged in${st.oauthEmail ? ` (${st.oauthEmail})` : ""}`
|
|
3202
|
+
: "\u00b7 not logged in";
|
|
3203
|
+
console.log(` ${i + 1}) ${p.padEnd(10)} ${badge}`);
|
|
3029
3204
|
});
|
|
3205
|
+
console.log(" Subscription / plan (token):");
|
|
3206
|
+
for (const p of SUBSCRIPTION_PROVIDER_NAMES) {
|
|
3207
|
+
const st = statuses.find(s => s.name === p);
|
|
3208
|
+
const badge = st?.kind === "api_key" ? "\u2713 active" : "\u00b7 no token";
|
|
3209
|
+
console.log(` ${p.padEnd(22)} ${badge} (set ${st?.envVar ?? `${p.toUpperCase().replace(/-/g, "_")}_API_KEY`})`);
|
|
3210
|
+
}
|
|
3030
3211
|
const ans = (await promptInput(`Choose [1-${cloud.length}] or name (blank to cancel): `)).trim().toLowerCase();
|
|
3031
3212
|
const byNum: Record<string, string> = Object.fromEntries(cloud.map((p, i) => [String(i + 1), p]));
|
|
3032
|
-
target = byNum[ans] ?? ((cloud as readonly string[]).includes(ans) ? ans : undefined);
|
|
3213
|
+
target = byNum[ans] ?? ((cloud as readonly string[]).includes(ans) || subs.has(ans) ? ans : undefined);
|
|
3033
3214
|
}
|
|
3034
3215
|
if (!target) {
|
|
3035
3216
|
console.log("(cancelled)");
|
|
3036
3217
|
continue;
|
|
3037
3218
|
}
|
|
3038
3219
|
}
|
|
3220
|
+
// Subscription/plan providers authenticate by token (not OAuth): prompt for the key
|
|
3221
|
+
// and store it like `/provider key`, then refresh + list models.
|
|
3222
|
+
if (subs.has(target)) {
|
|
3223
|
+
const sub = target as AuthProvider;
|
|
3224
|
+
const envVar = `${sub.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
3225
|
+
let token = tokens.slice(1).filter(t => t.toLowerCase() !== sub).pop();
|
|
3226
|
+
if (!token) {
|
|
3227
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3228
|
+
token = (await promptInput(`Paste ${sub} subscription token (blank to cancel): `)).trim();
|
|
3229
|
+
} else {
|
|
3230
|
+
console.log(`Set the ${sub} subscription token with: /provider login ${sub} <token> (or set ${envVar}).`);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (!token) {
|
|
3234
|
+
console.log("(cancelled — no token entered)");
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
await setApiKey(sub, token);
|
|
3238
|
+
console.log(`[SUCCESS] Stored ${sub} subscription token in ~/.jeo/config.json (also reads ${envVar}).`);
|
|
3239
|
+
const live = await refreshLiveModelsCache();
|
|
3240
|
+
const after = (await describeAllProviders()).find(s => s.name === sub);
|
|
3241
|
+
if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
|
|
3242
|
+
const forProvider = live.filter(r => r.provider === sub);
|
|
3243
|
+
if (forProvider.some(r => r.ok && r.models.length > 0)) {
|
|
3244
|
+
lastPickIndex = flattenModels(forProvider);
|
|
3245
|
+
const viaCatalog = forProvider.some(r => r.fallback);
|
|
3246
|
+
console.log(` ${viaCatalog ? "catalog" : "live"} ${sub} models → /model #N${viaCatalog ? " (live list endpoint unavailable; showing known models)" : ""}`);
|
|
3247
|
+
logLines(formatPickListWithCapabilities(lastPickIndex, { cap: 12 }));
|
|
3248
|
+
} else {
|
|
3249
|
+
const failed = forProvider.find(r => !r.ok);
|
|
3250
|
+
if (failed?.error) console.log(` live ${sub} models unavailable: ${failed.error}`);
|
|
3251
|
+
}
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3039
3254
|
console.log(`Starting OAuth login for ${target}…`);
|
|
3040
3255
|
try {
|
|
3041
3256
|
const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
|
package/src/tui/app.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Spinner } from "./components/spinner";
|
|
|
16
16
|
import { ToolList } from "./components/tool-list";
|
|
17
17
|
import { StreamRegion } from "./components/stream";
|
|
18
18
|
import { renderFooter, type FooterData } from "./components/footer";
|
|
19
|
-
import {
|
|
19
|
+
import { renderForgeMark, forgeMarkHeight, forgeMarkFrameCount, forgeBeat, FORGE_FLOW_PALETTE } from "./components/ascii-art";
|
|
20
20
|
import { evolutionTrack, createStageProgress, type StageProgress, transitionMessage } from "./components/evolution";
|
|
21
21
|
import type { TaskSubEvent } from "../agent/task-tool";
|
|
22
22
|
import { supportsUnicode } from "./components/capability";
|
|
@@ -1186,11 +1186,11 @@ export class LaunchTui {
|
|
|
1186
1186
|
index: i + 1,
|
|
1187
1187
|
color: this.theme.color,
|
|
1188
1188
|
dim,
|
|
1189
|
-
//
|
|
1189
|
+
// Forge-flow identity on LIVE cards only: the flowing neon gradient rides
|
|
1190
1190
|
// the card border and the prompt beat marks the title. Flushed/final cards
|
|
1191
1191
|
// stay static. Suppressed while `dim` (in-flight shading takes precedence).
|
|
1192
1192
|
...(anim && !dim
|
|
1193
|
-
? { flow: { palette:
|
|
1193
|
+
? { flow: { palette: FORGE_FLOW_PALETTE, phase: anim.phase, colorLevel: anim.colorLevel }, titleMark: anim.beat }
|
|
1194
1194
|
: {}),
|
|
1195
1195
|
}));
|
|
1196
1196
|
}
|
|
@@ -1443,9 +1443,9 @@ export class LaunchTui {
|
|
|
1443
1443
|
const innerWidth = !fit ? cols : this.inline ? cols - 1 : cols - 4;
|
|
1444
1444
|
|
|
1445
1445
|
// Resolve the current (monotonic) stage for the track; announce a transition
|
|
1446
|
-
// once when it first advances. The header art is the
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1446
|
+
// once when it first advances. The header art is the jeo forge mark — a
|
|
1447
|
+
// prompt-cursor blink combined with the flowing gradient phase. Both are
|
|
1448
|
+
// quantized (2 blink frames × 20 gradient phases), so the cache
|
|
1449
1449
|
// recomputes at most once per changed tick and stays a single slot (O(1)).
|
|
1450
1450
|
const stepNow = this.footer.step || 0;
|
|
1451
1451
|
const idx = this.progress.observe(stepNow, this.footer.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
@@ -1455,16 +1455,16 @@ export class LaunchTui {
|
|
|
1455
1455
|
this.appendLedger(`${arrow} ${transitionMessage(idx)}\n`, "notice");
|
|
1456
1456
|
}
|
|
1457
1457
|
const showArt = fit && !this.inline && rows >= 18 && cols >= 40;
|
|
1458
|
-
// One int key folds both animation axes:
|
|
1458
|
+
// One int key folds both animation axes: blink frame advances every 3 ticks,
|
|
1459
1459
|
// gradient phase cycles 20 quantized steps (tickCount*0.05 % 1).
|
|
1460
|
-
const twist = isThinking ? Math.trunc(this.tickCount / 3) %
|
|
1460
|
+
const twist = isThinking ? Math.trunc(this.tickCount / 3) % forgeMarkFrameCount() : 0;
|
|
1461
1461
|
const qPhase = isThinking ? this.tickCount % 20 : 0;
|
|
1462
1462
|
const effFrame = twist * 100 + qPhase;
|
|
1463
1463
|
if (showArt && (idx !== this.cachedStageIndex || cols !== this.cachedCols || effFrame !== this.cachedFrame)) {
|
|
1464
|
-
// Commit the cache keys only AFTER the render succeeds: if
|
|
1464
|
+
// Commit the cache keys only AFTER the render succeeds: if renderForgeMark ever
|
|
1465
1465
|
// throws (resize race, bad gradient level), pre-committed keys would mark the
|
|
1466
1466
|
// STALE art as current and freeze the header at an old frame forever.
|
|
1467
|
-
const art =
|
|
1467
|
+
const art = renderForgeMark({
|
|
1468
1468
|
cols: innerWidth,
|
|
1469
1469
|
phase: qPhase * 0.05,
|
|
1470
1470
|
frame: twist,
|
|
@@ -1479,7 +1479,7 @@ export class LaunchTui {
|
|
|
1479
1479
|
this.cachedCols = cols;
|
|
1480
1480
|
this.cachedFrame = effFrame;
|
|
1481
1481
|
}
|
|
1482
|
-
const artLinesCount = showArt ?
|
|
1482
|
+
const artLinesCount = showArt ? forgeMarkHeight() : 0;
|
|
1483
1483
|
const trackCount = showArt ? 1 : 0;
|
|
1484
1484
|
const headerHeight = artLinesCount + trackCount + (showArt ? 1 : 0);
|
|
1485
1485
|
|