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
package/src/commands/launch.ts
CHANGED
|
@@ -17,10 +17,10 @@ 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
|
-
import { staticCompletionContext, readlineCompleter, formatCompletionPreview, tokenize, type CompletionContext } from "../tui/components/autocomplete";
|
|
23
|
+
import { staticCompletionContext, readlineCompleter, formatCompletionPreview, formatMidTurnHint, tokenize, type CompletionContext } from "../tui/components/autocomplete";
|
|
24
24
|
import { normalizeBaseUrl } from "./setup-helpers";
|
|
25
25
|
import { EVOLUTION_STAGES, animateAsciiArt } from "../tui/components/ascii-art";
|
|
26
26
|
import { getEvolutionTip } from "../tui/components/evolution";
|
|
@@ -41,6 +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, SUBSCRIPTION_PROVIDER_NAMES } from "../ai/providers/openai-compatible-catalog";
|
|
44
45
|
|
|
45
46
|
import { allSubagentRoles, getSubagentRole, resolveSubagentModel, resolveSubagentMaxSteps, resolveSubagentThinking, parseMaxSteps, withSubagentSetting, clearSubagentSetting } from "../agent/subagents";
|
|
46
47
|
import { SelectList, renderSelectList, type SelectItem } from "../tui/components/select-list";
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
} from "../tui/components/config-panel";
|
|
58
59
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
59
60
|
|
|
60
|
-
import {
|
|
61
|
+
import { loginPicker, renderLoginPicker, onboardingPicker, renderOnboardingPicker, apiKeyPicker, renderApiKeyPicker, subscriptionLoginPicker, type OnboardingAction } from "../tui/components/provider-picker";
|
|
61
62
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
62
63
|
import { categoryBadge } from "../tui/components/category-index";
|
|
63
64
|
import { renderInputFrame, verticalCursorOffset } from "../tui/components/input-box";
|
|
@@ -134,6 +135,7 @@ import {
|
|
|
134
135
|
captureLivePromptInputChunk,
|
|
135
136
|
restoreQueuedLinesToPrefill,
|
|
136
137
|
createInFlightAbortHarness,
|
|
138
|
+
classifyMidTurnLine,
|
|
137
139
|
} from "./launch/input";
|
|
138
140
|
import {
|
|
139
141
|
gatedStdout,
|
|
@@ -198,6 +200,7 @@ export {
|
|
|
198
200
|
captureLivePromptInputChunk,
|
|
199
201
|
restoreQueuedLinesToPrefill,
|
|
200
202
|
createInFlightAbortHarness,
|
|
203
|
+
classifyMidTurnLine,
|
|
201
204
|
|
|
202
205
|
gatedStdout,
|
|
203
206
|
formatTaskSubEvent,
|
|
@@ -211,7 +214,10 @@ export {
|
|
|
211
214
|
currentAtLabelFn as currentAtLabel,
|
|
212
215
|
};
|
|
213
216
|
export function normalizeSlashAlias(input: string): string {
|
|
214
|
-
|
|
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)}`;
|
|
215
221
|
if (input === "/settings") return "/config";
|
|
216
222
|
if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
|
|
217
223
|
if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
|
|
@@ -219,7 +225,12 @@ export function normalizeSlashAlias(input: string): string {
|
|
|
219
225
|
return input;
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
// Per-provider starting model for `--provider <name>` / role pinning. Catalog
|
|
229
|
+
// OpenAI-compatible providers supply their own default; built-ins use this map.
|
|
230
|
+
const STATIC_PROVIDER_DEFAULT: Partial<Record<ProviderName, string>> = { anthropic: "sonnet", openai: "gpt-5.5", gemini: "flash", antigravity: "antigravity/gemini-3-pro-high", ollama: "fast", lmstudio: "lmstudio/local-model", xai: "grok-4.3", kimi: "kimi-k2-0711-preview" };
|
|
231
|
+
function providerDefaultModel(p: ProviderName): string {
|
|
232
|
+
return openaiCompatDef(p)?.defaultModel ?? STATIC_PROVIDER_DEFAULT[p] ?? "";
|
|
233
|
+
}
|
|
223
234
|
|
|
224
235
|
|
|
225
236
|
export function formatResumeHint(sessionId: string): string {
|
|
@@ -263,7 +274,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
263
274
|
const defaultModel = cfg.defaultModel;
|
|
264
275
|
const initialSessionModel =
|
|
265
276
|
flags.model ??
|
|
266
|
-
(flags.modelRole ? resolveRoleModel(flags.modelRole, cfg) : flags.provider ?
|
|
277
|
+
(flags.modelRole ? resolveRoleModel(flags.modelRole, cfg) : flags.provider ? providerDefaultModel(flags.provider) : undefined);
|
|
267
278
|
if (flags.provider && initialSessionModel) {
|
|
268
279
|
const { provider } = await describeModel(initialSessionModel);
|
|
269
280
|
if (provider !== flags.provider) {
|
|
@@ -472,6 +483,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
472
483
|
// Full untruncated output of the most recent tool call — the clipped forge
|
|
473
484
|
// card's `⟦Ctrl+O for more⟧` hint resolves here.
|
|
474
485
|
let lastToolDetail: { tool: string; output: string } | null = null;
|
|
486
|
+
// Accumulated reasoning/thinking for the in-flight turn (the model's thought before its
|
|
487
|
+
// answer). Captured from the reasoning stream and persisted on the assistant message so
|
|
488
|
+
// it survives /resume + export (gjc "think → answer" record). Reset at each turn start.
|
|
489
|
+
let lastTurnReasoning = "";
|
|
475
490
|
/** Wrap turn events so EVERY sink (TUI or plain stream) records the last full
|
|
476
491
|
* tool output for the Ctrl+O detail view. */
|
|
477
492
|
const withToolDetailCapture = (base: ReturnType<LaunchTui["events"]>): ReturnType<LaunchTui["events"]> => ({
|
|
@@ -480,6 +495,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
480
495
|
lastToolDetail = { tool, output };
|
|
481
496
|
base.onToolResult?.(tool, success, output);
|
|
482
497
|
},
|
|
498
|
+
onReasoningStream: (textSoFar: string) => {
|
|
499
|
+
// textSoFar is the cumulative thought for the current step; keep the latest
|
|
500
|
+
// non-empty value (the thought immediately preceding the turn's answer).
|
|
501
|
+
if (textSoFar.trim()) lastTurnReasoning = textSoFar;
|
|
502
|
+
base.onReasoningStream?.(textSoFar);
|
|
503
|
+
},
|
|
483
504
|
});
|
|
484
505
|
/** Compose a session-persistence flush into onStep so each completed step is
|
|
485
506
|
* written as it lands (durability across mid-turn interruption) without
|
|
@@ -547,6 +568,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
547
568
|
// Clears the live next-prompt draft — used after a mid-turn Enter is lifted into
|
|
548
569
|
// the steering inbox so the consumed line does not also become the next prompt.
|
|
549
570
|
let queueBusyClear: (() => void) | undefined;
|
|
571
|
+
// Routes a command-shaped (/… or $…) mid-turn draft into the idle loop's
|
|
572
|
+
// pending-line queue so it runs as a real COMMAND at the turn boundary,
|
|
573
|
+
// instead of being steered into the model as literal text.
|
|
574
|
+
let queueBusyCommand: ((line: string) => void) | undefined;
|
|
550
575
|
let interactiveTurnActive = false;
|
|
551
576
|
|
|
552
577
|
|
|
@@ -577,6 +602,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
577
602
|
// AFTER compaction (which mutates history) and consumed by the post-turn
|
|
578
603
|
// persistence block below.
|
|
579
604
|
let beforeLen = history.length;
|
|
605
|
+
lastTurnReasoning = ""; // fresh turn: capture this turn's thinking from scratch
|
|
580
606
|
// Incremental session persistence (durability across mid-turn interruption):
|
|
581
607
|
// persistTurnTail() flushes history messages added since the last flush — called
|
|
582
608
|
// right after the user prompt, on every onStep boundary, and once post-turn — so
|
|
@@ -675,12 +701,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
675
701
|
if (typedEnter) {
|
|
676
702
|
const line = (queueBusySnapshot?.().text ?? "").trim();
|
|
677
703
|
if (line) {
|
|
678
|
-
|
|
704
|
+
// A mid-turn /command or $skill is NOT a query for the model — steering it
|
|
705
|
+
// would send the literal "/model" / "$skill" text to the LLM. Recognize it
|
|
706
|
+
// and run it as a real COMMAND: queue it for the idle dispatcher and stop the
|
|
707
|
+
// turn so it runs at once (below). Plain queries still steer into the running
|
|
708
|
+
// turn. JEO_NO_STEER=1 disables both (legacy draft-only).
|
|
679
709
|
queueBusyClear?.();
|
|
680
710
|
tui.setLivePromptInput("");
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
711
|
+
tui.setLivePromptHint([]);
|
|
712
|
+
if (classifyMidTurnLine(line) === "command") {
|
|
713
|
+
// Run it as a real COMMAND: queue it for immediate dispatch by the prompt
|
|
714
|
+
// loop and abort the turn (the same controller Esc uses). The abort ends a
|
|
715
|
+
// streaming turn at once and cancels any further steps; a running tool still
|
|
716
|
+
// finishes first (jeo's abort is step-level, like Esc). The queued command is
|
|
717
|
+
// then auto-dispatched — no second Enter. JEO_NO_MIDTURN_DISPATCH=1 keeps the
|
|
718
|
+
// legacy behavior (queue to prefill, no interrupt, press Enter to run).
|
|
719
|
+
queueBusyCommand?.(line);
|
|
720
|
+
if (jeoEnv("NO_MIDTURN_DISPATCH") === "1") {
|
|
721
|
+
tui.events().onNotice?.(`⌘ queued ${line} — press Enter after this turn to run`);
|
|
722
|
+
} else {
|
|
723
|
+
tui.events().onNotice?.(`⌘ ${line} — interrupting the turn to run it`);
|
|
724
|
+
harness.controller.abort();
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
steerInbox.push(line);
|
|
728
|
+
// Surface the steered query as a `user` card in scrollback so it reads
|
|
729
|
+
// as an accepted input that started work — not just a transient notice.
|
|
730
|
+
tui.flushSteerCard(line);
|
|
731
|
+
}
|
|
684
732
|
return;
|
|
685
733
|
}
|
|
686
734
|
}
|
|
@@ -691,7 +739,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
691
739
|
// suppressed for the whole turn). On Enter the draft is lifted into the steering
|
|
692
740
|
// inbox and surfaces as a `user` card (above). JEO_NO_LIVE_DRAFT=1 opts out.
|
|
693
741
|
if (captured && jeoEnv("NO_LIVE_DRAFT") !== "1") {
|
|
694
|
-
|
|
742
|
+
const draft = queueBusySnapshot?.().text ?? "";
|
|
743
|
+
tui.setLivePromptInput(draft);
|
|
744
|
+
// Mid-turn command preview: as you type a /command or $skill DURING a turn,
|
|
745
|
+
// show its matches above the input box so command input visibly reacts
|
|
746
|
+
// (idle-prompt parity). Cleared the moment the draft stops being command-shaped.
|
|
747
|
+
tui.setLivePromptHint(
|
|
748
|
+
/^\s*[/$]/.test(draft) ? formatMidTurnHint(draft.trimStart(), completionContext(), 5) : [],
|
|
749
|
+
);
|
|
695
750
|
}
|
|
696
751
|
},
|
|
697
752
|
onAbortNotice: msg => {
|
|
@@ -851,8 +906,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
851
906
|
// this only covers the tail — net content is the full turn either way.
|
|
852
907
|
try {
|
|
853
908
|
await persistTurnTail();
|
|
854
|
-
|
|
855
|
-
|
|
909
|
+
const assistantMsg: Message = lastTurnReasoning.trim()
|
|
910
|
+
? { role: "assistant", content: reply, reasoning: lastTurnReasoning }
|
|
911
|
+
: { role: "assistant", content: reply };
|
|
912
|
+
history.push(assistantMsg);
|
|
913
|
+
if (sessionId) await appendMessage(sessionId, assistantMsg, cwd);
|
|
856
914
|
if (tui) tui.finish(reply);
|
|
857
915
|
} finally {
|
|
858
916
|
if (tui) interactiveTurnActive = false;
|
|
@@ -1038,7 +1096,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1038
1096
|
// gjc-style fresh-start clear so the banner opens atop a clean screen. TTY only,
|
|
1039
1097
|
// never mid-turn (scrollback flood). ponytail: add an opt-out env if anyone misses their scrollback.
|
|
1040
1098
|
if (process.stdout.isTTY) process.stdout.write(clearScreen());
|
|
1041
|
-
// Launch sweep: the
|
|
1099
|
+
// Launch sweep: the forge mark's gradient loops seamlessly (default 2 full
|
|
1042
1100
|
// cycles, JEO_WELCOME_ANIM_CYCLES overrides), ending on the static banner.
|
|
1043
1101
|
// Truecolor TTYs only; JEO_NO_WELCOME_ANIM=1 opts out.
|
|
1044
1102
|
const sweepable =
|
|
@@ -1356,6 +1414,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1356
1414
|
// only captures the line submitted while it is registered; orphan lines emit
|
|
1357
1415
|
// 'line' instead), so queue those and serve them before prompting again.
|
|
1358
1416
|
const pendingStdinLines: string[] = [];
|
|
1417
|
+
// Commands submitted mid-turn (/… or $…) land here; the prompt loop dispatches them
|
|
1418
|
+
// IMMEDIATELY on its next iteration, bypassing the "new input first" prefill contract
|
|
1419
|
+
// (the user explicitly invoked them — no second Enter).
|
|
1420
|
+
const pendingMidTurnCommands: string[] = [];
|
|
1359
1421
|
const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: "", pastedLines: [], inPaste: false };
|
|
1360
1422
|
queueBusyInput = (chunk: string) => captureLivePromptInputChunk(queuedPromptInput, chunk);
|
|
1361
1423
|
queueBusyPasteActive = () => queuedPromptInput.inPaste;
|
|
@@ -1363,6 +1425,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1363
1425
|
text: queuedPromptInput.partial,
|
|
1364
1426
|
});
|
|
1365
1427
|
queueBusyClear = () => { queuedPromptInput.partial = ""; };
|
|
1428
|
+
queueBusyCommand = (line: string) => {
|
|
1429
|
+
// NO_MIDTURN_DISPATCH=1 keeps the legacy prefill path (tee up, press Enter); the
|
|
1430
|
+
// default routes to the immediate-dispatch queue served at the top of the loop.
|
|
1431
|
+
(jeoEnv("NO_MIDTURN_DISPATCH") === "1" ? pendingStdinLines : pendingMidTurnCommands).push(line);
|
|
1432
|
+
};
|
|
1366
1433
|
// Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
|
|
1367
1434
|
// and replays pasted lines as synthetic keypresses, emitting paste-start /
|
|
1368
1435
|
// paste-end around them. Lines submitted INSIDE that window are intentional
|
|
@@ -2142,13 +2209,112 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2142
2209
|
|
|
2143
2210
|
|
|
2144
2211
|
const pickCloudProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
|
|
2145
|
-
const cloud = new Set(
|
|
2146
|
-
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);
|
|
2147
2313
|
let chosen: ProviderName | undefined;
|
|
2148
2314
|
await runSelectPicker(
|
|
2149
2315
|
(cols, rows) =>
|
|
2150
|
-
|
|
2151
|
-
title: "Select
|
|
2316
|
+
renderApiKeyPicker(list, {
|
|
2317
|
+
title: "Select a provider to key \u2191\u2193 move \u00b7 Enter select \u00b7 Esc cancel",
|
|
2152
2318
|
cols,
|
|
2153
2319
|
rows: Math.max(4, Math.min(rows, 8)),
|
|
2154
2320
|
unicode: true,
|
|
@@ -2188,9 +2354,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2188
2354
|
return false;
|
|
2189
2355
|
},
|
|
2190
2356
|
);
|
|
2191
|
-
return chosen &&
|
|
2357
|
+
return chosen && keyed.has(chosen) ? chosen as AuthProvider : undefined;
|
|
2192
2358
|
};
|
|
2193
2359
|
|
|
2360
|
+
|
|
2194
2361
|
if (previewEnabled) {
|
|
2195
2362
|
process.once("exit", () => out.write("\x1b[?25h")); // safety net: never leave the cursor hidden
|
|
2196
2363
|
const footerKeypressHandler = (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
|
|
@@ -2482,7 +2649,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2482
2649
|
// Box mode: NO raw `jeo>` prompt at all — the boxed footer IS the input UI
|
|
2483
2650
|
// (gating already suppresses readline echo, the empty prompt guarantees no
|
|
2484
2651
|
// raw CLI input line can ever flash). Legacy prompt only without the box.
|
|
2485
|
-
const rawText =
|
|
2652
|
+
const rawText = pendingMidTurnCommands.length
|
|
2653
|
+
? (disarmPreview(), pendingMidTurnCommands.shift()!)
|
|
2654
|
+
: await promptInput(previewEnabled ? "" : "\njeo> ");
|
|
2486
2655
|
if (rawText.includes("\u0003")) forceExitFromCtrlC();
|
|
2487
2656
|
const raw = rawText.trim();
|
|
2488
2657
|
disarmPreview();
|
|
@@ -2936,40 +3105,152 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2936
3105
|
}
|
|
2937
3106
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
2938
3107
|
const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
|
|
2939
|
-
|
|
3108
|
+
let name = (tokens[0] ?? "").toLowerCase();
|
|
2940
3109
|
// gjc-parity (semantic): /provider is ONBOARDING ONLY — set up OAuth credentials
|
|
2941
3110
|
// or an API-compatible endpoint. Switching the active provider/model lives in /model.
|
|
2942
3111
|
const providerOnboardingUsage = (): string[] => [
|
|
2943
3112
|
"Provider onboarding — set up credentials or an API-compatible endpoint:",
|
|
2944
|
-
" 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, …)",
|
|
2945
3116
|
" API-compatible : /provider add --base-url <url> [--model <model>] [--compat openai] (reads OPENAI_API_KEY)",
|
|
2946
3117
|
" show current / clear: /provider add · /provider add clear",
|
|
2947
3118
|
" Logout : /logout <provider>",
|
|
3119
|
+
" Headless OAuth : paste the redirect URL or code when the login prompt asks.",
|
|
2948
3120
|
"Switch the active model or provider with /model.",
|
|
2949
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
|
+
}
|
|
2950
3186
|
// `/provider login|auth [name]` → run OAuth login from the REPL.
|
|
2951
3187
|
if (name === "login" || name === "auth") {
|
|
2952
3188
|
const cloud = ["anthropic", "openai", "gemini", "antigravity"] as const;
|
|
2953
|
-
|
|
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));
|
|
2954
3191
|
if (!target) {
|
|
2955
3192
|
const statuses = await describeAllProviders();
|
|
2956
3193
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
2957
3194
|
target = await pickCloudProvider(statuses);
|
|
2958
3195
|
} else {
|
|
2959
3196
|
console.log("Log in to which provider?");
|
|
3197
|
+
console.log(" OAuth:");
|
|
2960
3198
|
cloud.forEach((p, i) => {
|
|
2961
3199
|
const st = statuses.find(s => s.name === p);
|
|
2962
|
-
|
|
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}`);
|
|
2963
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
|
+
}
|
|
2964
3211
|
const ans = (await promptInput(`Choose [1-${cloud.length}] or name (blank to cancel): `)).trim().toLowerCase();
|
|
2965
3212
|
const byNum: Record<string, string> = Object.fromEntries(cloud.map((p, i) => [String(i + 1), p]));
|
|
2966
|
-
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);
|
|
2967
3214
|
}
|
|
2968
3215
|
if (!target) {
|
|
2969
3216
|
console.log("(cancelled)");
|
|
2970
3217
|
continue;
|
|
2971
3218
|
}
|
|
2972
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
|
+
}
|
|
2973
3254
|
console.log(`Starting OAuth login for ${target}…`);
|
|
2974
3255
|
try {
|
|
2975
3256
|
const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
|
|
@@ -3216,7 +3497,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3216
3497
|
// No model given → the provider's first live model, provider-qualified.
|
|
3217
3498
|
chosenModel = qualifyModelId(forProvider[0]!.model, want);
|
|
3218
3499
|
} else {
|
|
3219
|
-
chosenModel =
|
|
3500
|
+
chosenModel = providerDefaultModel(want);
|
|
3220
3501
|
}
|
|
3221
3502
|
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
3222
3503
|
console.log(`${role.title} pinned to ${want} via model ${chosenModel} — saved to ~/.jeo/config.json`);
|
package/src/commands/setup.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
OAUTH_FLOW_REGISTRY,
|
|
9
9
|
openInBrowser,
|
|
10
10
|
type AuthProvider,
|
|
11
|
+
type OAuthProvider,
|
|
11
12
|
type OAuthController,
|
|
12
13
|
} from "../auth";
|
|
13
14
|
import {
|
|
@@ -118,7 +119,7 @@ export async function runSetupCommand(): Promise<void> {
|
|
|
118
119
|
const key = await rl.question(`${choice} API key [${current.providers[choice] ? "********" : "None"}]: `);
|
|
119
120
|
if (key.trim()) next.providers[choice] = key.trim();
|
|
120
121
|
} else {
|
|
121
|
-
const flow = OAUTH_FLOW_REGISTRY[choice as
|
|
122
|
+
const flow = OAUTH_FLOW_REGISTRY[choice as OAuthProvider];
|
|
122
123
|
if (!flow.verifiedEndToEnd && flow.note) console.log(`Note: ${flow.note}`);
|
|
123
124
|
// Abort the pending "Paste redirect URL…" question once the flow settles —
|
|
124
125
|
// otherwise it survives the SUCCESS/FAILED result, reprints its prompt, and
|
|
@@ -138,7 +139,7 @@ export async function runSetupCommand(): Promise<void> {
|
|
|
138
139
|
try {
|
|
139
140
|
let email: string | undefined;
|
|
140
141
|
try {
|
|
141
|
-
({ email } = await interactiveLogin(choice as
|
|
142
|
+
({ email } = await interactiveLogin(choice as OAuthProvider, ctrl));
|
|
142
143
|
} finally {
|
|
143
144
|
// Must fire BEFORE the catch's API-key question below, or that
|
|
144
145
|
// question queues behind the stale paste prompt.
|