jeo-code 0.6.22 → 0.6.23
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 +15 -0
- package/README.ja.md +5 -1
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/README.zh.md +5 -1
- 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 +26 -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 +72 -0
- package/src/ai/providers/openai-compatible.ts +31 -0
- package/src/ai/providers/openai.ts +23 -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 +6 -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 +19 -2
- package/src/commands/launch/flags.ts +5 -1
- package/src/commands/launch/input.ts +13 -0
- package/src/commands/launch.ts +78 -12
- package/src/commands/setup.ts +3 -2
- package/src/tui/app.ts +51 -31
- package/src/tui/components/ascii-art.ts +11 -7
- package/src/tui/components/autocomplete.ts +16 -0
- package/src/tui/components/forge.ts +1 -1
- package/src/tui/components/transcript.ts +7 -0
- package/src/tui/components/width.ts +21 -0
|
@@ -4,6 +4,7 @@ import { readSse } from "../sse";
|
|
|
4
4
|
import { providerHttpError } from "./errors";
|
|
5
5
|
import { codexResponsesCall, codexResponsesStream } from "./openai-responses";
|
|
6
6
|
import { serializeToolCalls, serializeAccumulatedToolCalls } from "../../agent/tool-schemas";
|
|
7
|
+
import { createThinkSplitter } from "../think-tags";
|
|
7
8
|
|
|
8
9
|
export function openaiRequest(messages: Message[], options: CallOptions, credential: Credential, stream: boolean): { url: string; headers: Record<string, string>; body: string } {
|
|
9
10
|
const model = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
|
|
@@ -22,9 +23,11 @@ export function openaiRequest(messages: Message[], options: CallOptions, credent
|
|
|
22
23
|
: msg.content;
|
|
23
24
|
openaiMessages.push({ role: msg.role, content });
|
|
24
25
|
}
|
|
25
|
-
// Reasoning models (o-series, gpt-5 family) take max_completion_tokens + reasoning_effort
|
|
26
|
+
// Reasoning models (o-series, gpt-5+ family) take max_completion_tokens + reasoning_effort
|
|
26
27
|
// and reject temperature; classic chat models (gpt-4o, …) take max_tokens + temperature.
|
|
27
|
-
|
|
28
|
+
// Digit-count agnostic (gpt-6/o10 stay reasoning) — mirrors inferCatalogMetadata.
|
|
29
|
+
const gptMajorMatch = model.match(/^gpt-(\d+)/);
|
|
30
|
+
const isReasoning = /^o\d/.test(model) || (gptMajorMatch ? Number(gptMajorMatch[1]) >= 5 : false);
|
|
28
31
|
const payload: Record<string, unknown> = {
|
|
29
32
|
model,
|
|
30
33
|
messages: openaiMessages,
|
|
@@ -102,19 +105,30 @@ export const openaiAdapter: ProviderAdapter = {
|
|
|
102
105
|
if (!response.body) return;
|
|
103
106
|
let yieldedAny = false;
|
|
104
107
|
let finishReason: string | undefined;
|
|
108
|
+
// Split inline <think>…</think> (DeepSeek-R1/Qwen-style local models) out of the
|
|
109
|
+
// visible answer and onto the reasoning channel. No-op for models that never emit it.
|
|
110
|
+
const think = createThinkSplitter(options.onReasoning);
|
|
105
111
|
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
106
112
|
for await (const data of readSse(response.body)) {
|
|
107
|
-
let chunk: { choices?: { delta?: { content?: string; tool_calls?: { index?: number; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
113
|
+
let chunk: { choices?: { delta?: { content?: string; reasoning_content?: string; reasoning?: string; reasoning_text?: string; tool_calls?: { index?: number; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
108
114
|
try {
|
|
109
115
|
chunk = JSON.parse(data);
|
|
110
116
|
} catch {
|
|
111
117
|
continue;
|
|
112
118
|
}
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
const raw = chunk.choices?.[0]?.delta?.content;
|
|
120
|
+
if (raw) {
|
|
121
|
+
const visible = think.push(raw);
|
|
122
|
+
if (visible) {
|
|
123
|
+
yieldedAny = true;
|
|
124
|
+
yield visible;
|
|
125
|
+
}
|
|
117
126
|
}
|
|
127
|
+
// Structured reasoning channel (DeepSeek `reasoning_content`, OpenRouter/xAI
|
|
128
|
+
// `reasoning`): a SEPARATE field from content, so it bypasses the <think> splitter.
|
|
129
|
+
const d = chunk.choices?.[0]?.delta;
|
|
130
|
+
const reason = d?.reasoning_content ?? d?.reasoning ?? d?.reasoning_text;
|
|
131
|
+
if (reason) options.onReasoning?.(reason);
|
|
118
132
|
const tcs = chunk.choices?.[0]?.delta?.tool_calls;
|
|
119
133
|
if (tcs) {
|
|
120
134
|
for (const tc of tcs) {
|
|
@@ -128,6 +142,8 @@ export const openaiAdapter: ProviderAdapter = {
|
|
|
128
142
|
if (chunk.choices?.[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
|
|
129
143
|
if (chunk.usage) options.onUsage?.({ inputTokens: chunk.usage.prompt_tokens, outputTokens: chunk.usage.completion_tokens });
|
|
130
144
|
}
|
|
145
|
+
const trailing = think.flush();
|
|
146
|
+
if (trailing) { yieldedAny = true; yield trailing; }
|
|
131
147
|
// Native tool calls stream as tool_calls argument fragments — re-serialize once at end.
|
|
132
148
|
const envelope = serializeAccumulatedToolCalls(toolAcc);
|
|
133
149
|
if (envelope) { yieldedAny = true; yield envelope; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Credential } from "../../auth";
|
|
2
|
+
import { makeOpenAICompatibleAdapter } from "./openai-compatible";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* xAI (Grok) — OpenAI-compatible cloud API at https://api.x.ai/v1, keyed by
|
|
6
|
+
* XAI_API_KEY (or `providers.xai`). The credential (an api_key bearer) is passed
|
|
7
|
+
* through; grok reasoning models (grok-4.3, grok-4-fast-*, grok-code-fast-1) stream
|
|
8
|
+
* reasoning via `reasoning_content`, which the openai adapter routes to onReasoning.
|
|
9
|
+
*/
|
|
10
|
+
export const XAI_BASE_URL = "https://api.x.ai/v1";
|
|
11
|
+
|
|
12
|
+
export const xaiAdapter = makeOpenAICompatibleAdapter({ name: "xai", baseUrl: XAI_BASE_URL });
|
|
13
|
+
|
|
14
|
+
/** Credential carrier for xAI calls — an api_key bearer (the adapter only reads the
|
|
15
|
+
* token); a keyless `none` when no key is set. */
|
|
16
|
+
export function xaiCredential(key: string | undefined): Credential {
|
|
17
|
+
return key ? { kind: "api_key", provider: "openai", token: key } : { kind: "none", provider: "openai" };
|
|
18
|
+
}
|
|
@@ -12,10 +12,28 @@ import { anthropicAdapter } from "./providers/anthropic";
|
|
|
12
12
|
import { openaiAdapter } from "./providers/openai";
|
|
13
13
|
import { geminiAdapter } from "./providers/gemini";
|
|
14
14
|
import { ollamaAdapter } from "./providers/ollama";
|
|
15
|
+
import { lmstudioAdapter } from "./providers/lmstudio";
|
|
16
|
+
import { xaiAdapter } from "./providers/xai";
|
|
15
17
|
import { antigravityAdapter } from "./providers/antigravity";
|
|
18
|
+
import { kimiAdapter } from "./providers/kimi";
|
|
19
|
+
import { makeOpenAICompatibleAdapter } from "./providers/openai-compatible";
|
|
20
|
+
import { makeAnthropicCompatibleAdapter } from "./providers/anthropic-compatible";
|
|
21
|
+
import { OPENAI_COMPAT_PROVIDERS } from "./providers/openai-compatible-catalog";
|
|
16
22
|
|
|
17
23
|
providerRegistry.register("anthropic", anthropicAdapter);
|
|
18
24
|
providerRegistry.register("openai", openaiAdapter);
|
|
19
25
|
providerRegistry.register("gemini", geminiAdapter);
|
|
20
26
|
providerRegistry.register("antigravity", antigravityAdapter);
|
|
21
27
|
providerRegistry.register("ollama", ollamaAdapter);
|
|
28
|
+
providerRegistry.register("lmstudio", lmstudioAdapter);
|
|
29
|
+
providerRegistry.register("xai", xaiAdapter);
|
|
30
|
+
providerRegistry.register("kimi", kimiAdapter);
|
|
31
|
+
|
|
32
|
+
// gjc-style data-driven providers: every catalog entry gets a thin factory adapter,
|
|
33
|
+
// selected by wire protocol. Add a provider by adding ONE catalog row.
|
|
34
|
+
for (const def of OPENAI_COMPAT_PROVIDERS) {
|
|
35
|
+
const adapter = def.protocol === "anthropic"
|
|
36
|
+
? makeAnthropicCompatibleAdapter({ name: def.name, baseUrl: def.baseUrl })
|
|
37
|
+
: makeOpenAICompatibleAdapter({ name: def.name, baseUrl: def.baseUrl });
|
|
38
|
+
providerRegistry.register(def.name, adapter);
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming `<think>…</think>` splitter for OpenAI-compatible / Ollama models.
|
|
3
|
+
*
|
|
4
|
+
* Many open/local reasoning models (DeepSeek-R1, Qwen "thinking", QwQ, …) do NOT
|
|
5
|
+
* expose a separate reasoning channel; they inline their chain-of-thought as
|
|
6
|
+
* `<think>…</think>` inside the normal content stream. Without splitting, that
|
|
7
|
+
* reasoning is dumped into the answer as literal text. This stateful splitter
|
|
8
|
+
* routes think-tag content to `onReasoning` (the dimmed live trace) and returns
|
|
9
|
+
* only the user-visible answer text — handling tags that straddle chunk
|
|
10
|
+
* boundaries, so it is safe to feed raw streamed deltas one at a time.
|
|
11
|
+
*
|
|
12
|
+
* Passthrough is near-free: text with no `<think>` tag flows through unchanged
|
|
13
|
+
* (only a trailing partial-tag fragment is briefly buffered).
|
|
14
|
+
*/
|
|
15
|
+
const OPEN = "<think>";
|
|
16
|
+
const CLOSE = "</think>";
|
|
17
|
+
|
|
18
|
+
/** Longest suffix of `s` that is a non-empty proper prefix of `tag` (0 if none). */
|
|
19
|
+
function partialTail(s: string, tag: string): number {
|
|
20
|
+
const max = Math.min(s.length, tag.length - 1);
|
|
21
|
+
for (let k = max; k > 0; k--) {
|
|
22
|
+
if (s.endsWith(tag.slice(0, k))) return k;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ThinkSplitter {
|
|
28
|
+
/** Feed one streamed delta; returns the visible (answer) text to yield. */
|
|
29
|
+
push(delta: string): string;
|
|
30
|
+
/** Flush any buffered partial tag at stream end; returns trailing visible text. */
|
|
31
|
+
flush(): string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createThinkSplitter(onReasoning?: (delta: string) => void): ThinkSplitter {
|
|
35
|
+
let inThink = false;
|
|
36
|
+
let pending = ""; // a tail that might be the start of an OPEN/CLOSE tag
|
|
37
|
+
|
|
38
|
+
const push = (delta: string): string => {
|
|
39
|
+
let s = pending + delta;
|
|
40
|
+
pending = "";
|
|
41
|
+
let visible = "";
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (!inThink) {
|
|
44
|
+
const idx = s.indexOf(OPEN);
|
|
45
|
+
if (idx === -1) {
|
|
46
|
+
const tail = partialTail(s, OPEN);
|
|
47
|
+
visible += s.slice(0, s.length - tail);
|
|
48
|
+
pending = s.slice(s.length - tail);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
visible += s.slice(0, idx);
|
|
52
|
+
s = s.slice(idx + OPEN.length);
|
|
53
|
+
inThink = true;
|
|
54
|
+
} else {
|
|
55
|
+
const idx = s.indexOf(CLOSE);
|
|
56
|
+
if (idx === -1) {
|
|
57
|
+
const tail = partialTail(s, CLOSE);
|
|
58
|
+
const think = s.slice(0, s.length - tail);
|
|
59
|
+
if (think) onReasoning?.(think);
|
|
60
|
+
pending = s.slice(s.length - tail);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const think = s.slice(0, idx);
|
|
64
|
+
if (think) onReasoning?.(think);
|
|
65
|
+
s = s.slice(idx + CLOSE.length);
|
|
66
|
+
inThink = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return visible;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const flush = (): string => {
|
|
73
|
+
const out = pending;
|
|
74
|
+
pending = "";
|
|
75
|
+
// An unterminated tail is literal content: emit it on whichever channel was open.
|
|
76
|
+
if (inThink) {
|
|
77
|
+
if (out) onReasoning?.(out);
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { push, flush };
|
|
84
|
+
}
|
package/src/ai/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Credential } from "../auth";
|
|
2
2
|
|
|
3
|
-
export type ProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama";
|
|
3
|
+
export type ProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama" | "lmstudio" | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax";
|
|
4
4
|
|
|
5
5
|
/** An image attached to a (user) message — base64 payload + IANA media type. */
|
|
6
6
|
export interface ImageAttachment {
|
|
@@ -17,6 +17,11 @@ export interface Message {
|
|
|
17
17
|
* these alongside `content`; history bookkeeping (compaction, transcripts)
|
|
18
18
|
* keeps treating `content` as the message body. */
|
|
19
19
|
images?: ImageAttachment[];
|
|
20
|
+
/** Persisted reasoning/thinking text for an assistant turn (the thought before the
|
|
21
|
+
* answer). Survives /resume + export so the durable record shows "think → answer".
|
|
22
|
+
* Display-only: NOT replayed to providers (anthropic/gemini thinking replay needs
|
|
23
|
+
* the original signed block, which the streaming path does not capture). */
|
|
24
|
+
reasoning?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface Usage {
|
package/src/auth/flows/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Per-provider OAuth login + refresh dispatch. */
|
|
2
|
-
import type { AuthProvider } from "../storage";
|
|
2
|
+
import type { AuthProvider, OAuthProvider } from "../storage";
|
|
3
3
|
import type { OAuthController, OAuthCredentials } from "../types";
|
|
4
4
|
import { loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
5
5
|
import { loginOpenAI, refreshOpenAIToken } from "./openai";
|
|
@@ -7,7 +7,7 @@ import { loginGoogle, refreshGoogleToken } from "./google";
|
|
|
7
7
|
import { loginAntigravity, refreshAntigravityToken } from "./antigravity";
|
|
8
8
|
|
|
9
9
|
export interface OAuthFlow {
|
|
10
|
-
readonly provider:
|
|
10
|
+
readonly provider: OAuthProvider;
|
|
11
11
|
readonly label: string;
|
|
12
12
|
/** Run the interactive browser/PKCE login. */
|
|
13
13
|
login(ctrl: OAuthController): Promise<OAuthCredentials>;
|
|
@@ -19,7 +19,7 @@ export interface OAuthFlow {
|
|
|
19
19
|
readonly note?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export const OAUTH_FLOW_REGISTRY: Record<
|
|
22
|
+
export const OAUTH_FLOW_REGISTRY: Record<OAuthProvider, OAuthFlow> = {
|
|
23
23
|
anthropic: {
|
|
24
24
|
provider: "anthropic",
|
|
25
25
|
label: "Anthropic (Claude Pro/Max)",
|
package/src/auth/index.ts
CHANGED
|
@@ -6,8 +6,11 @@ export {
|
|
|
6
6
|
setOauthCredential,
|
|
7
7
|
clearOauthToken,
|
|
8
8
|
setApiKey,
|
|
9
|
+
isOAuthProvider,
|
|
10
|
+
OAUTH_PROVIDERS,
|
|
11
|
+
API_KEY_ONLY_PROVIDERS,
|
|
9
12
|
} from "./storage";
|
|
10
|
-
export type { AuthProvider, Credential, AuthSnapshot } from "./storage";
|
|
13
|
+
export type { AuthProvider, OAuthProvider, Credential, AuthSnapshot } from "./storage";
|
|
11
14
|
export {
|
|
12
15
|
OAUTH_FLOWS,
|
|
13
16
|
openInBrowser,
|
package/src/auth/oauth.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthProvider } from "./storage";
|
|
1
|
+
import type { AuthProvider, OAuthProvider } from "./storage";
|
|
2
2
|
import { setOauthToken, clearOauthToken, setOauthCredential } from "./storage";
|
|
3
3
|
import { OAUTH_FLOW_REGISTRY } from "./flows";
|
|
4
4
|
import type { OAuthController } from "./types";
|
|
@@ -10,7 +10,7 @@ export interface OauthFlowDef {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Metadata kept for help text / manual-paste fallback. */
|
|
13
|
-
export const OAUTH_FLOWS: Record<
|
|
13
|
+
export const OAUTH_FLOWS: Record<OAuthProvider, OauthFlowDef> = {
|
|
14
14
|
anthropic: {
|
|
15
15
|
label: "Anthropic Console (Claude)",
|
|
16
16
|
authorizeUrl: "https://claude.ai/oauth/authorize",
|
|
@@ -64,7 +64,7 @@ export async function openInBrowser(url: string): Promise<void> {
|
|
|
64
64
|
* the local callback server, wait for the code (or manual paste), exchange it,
|
|
65
65
|
* and persist the full credential set (access + refresh + expiry).
|
|
66
66
|
*/
|
|
67
|
-
export async function interactiveLogin(provider:
|
|
67
|
+
export async function interactiveLogin(provider: OAuthProvider, ctrl: OAuthController): Promise<{ email?: string }> {
|
|
68
68
|
const flow = OAUTH_FLOW_REGISTRY[provider];
|
|
69
69
|
const creds = await flow.login(ctrl);
|
|
70
70
|
await setOauthCredential(provider, {
|
package/src/auth/refresh.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
acquireLock,
|
|
8
8
|
releaseLock,
|
|
9
9
|
type AuthProvider,
|
|
10
|
+
isOAuthProvider,
|
|
10
11
|
type Credential,
|
|
11
12
|
} from "./storage";
|
|
12
13
|
import { OAUTH_FLOW_REGISTRY } from "./flows";
|
|
@@ -24,6 +25,10 @@ export interface RefreshResult {
|
|
|
24
25
|
* Mirrors gjc's auth-broker refresher semantics (single source of truth).
|
|
25
26
|
*/
|
|
26
27
|
export async function refreshOAuthToken(provider: AuthProvider): Promise<RefreshResult> {
|
|
28
|
+
// API-key-only providers (xai/kimi) have no OAuth flow — nothing to refresh.
|
|
29
|
+
if (!isOAuthProvider(provider)) {
|
|
30
|
+
return { refreshed: false, reason: "no_oauth_token", credential: await resolveCredential(provider) };
|
|
31
|
+
}
|
|
27
32
|
await acquireLock(provider);
|
|
28
33
|
try {
|
|
29
34
|
const stored = await getStoredOAuth(provider);
|
package/src/auth/storage.ts
CHANGED
|
@@ -5,7 +5,18 @@ import { readGlobalConfig, readRawGlobalConfig, saveConfigPatch, type StoredOAut
|
|
|
5
5
|
import { jeoEnv } from "../util/env";
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Providers with an interactive OAuth login + refresh flow. */
|
|
9
|
+
export type OAuthProvider = "anthropic" | "openai" | "gemini" | "antigravity";
|
|
10
|
+
/** Every provider jeo resolves a credential for: OAuth-capable ∪ API-key-only. */
|
|
11
|
+
export type AuthProvider = OAuthProvider | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax";
|
|
12
|
+
|
|
13
|
+
export const OAUTH_PROVIDERS: readonly OAuthProvider[] = ["anthropic", "openai", "gemini", "antigravity"];
|
|
14
|
+
/** API-key-only providers (no OAuth flow) — resolved from config.providers / `<NAME>_API_KEY`. */
|
|
15
|
+
export const API_KEY_ONLY_PROVIDERS: readonly AuthProvider[] = ["xai", "kimi", "groq", "deepseek", "mistral", "openrouter", "together", "cerebras", "fireworks", "nvidia", "alibaba-coding-plan", "huggingface", "nanogpt", "qwen-portal", "synthetic", "venice", "zenmux", "qianfan", "xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn", "xiaomi-token-plan-sgp", "minimax-code", "minimax-code-cn", "zai", "minimax"];
|
|
16
|
+
/** Narrow an AuthProvider to the OAuth-capable subset (xai/kimi have no OAuth flow). */
|
|
17
|
+
export function isOAuthProvider(p: AuthProvider): p is OAuthProvider {
|
|
18
|
+
return (OAUTH_PROVIDERS as readonly string[]).includes(p);
|
|
19
|
+
}
|
|
9
20
|
|
|
10
21
|
export type Credential =
|
|
11
22
|
| { kind: "oauth"; provider: AuthProvider; token: string; projectId?: string }
|
package/src/commands/auth.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
logoutOAuth,
|
|
15
15
|
refreshOAuthToken,
|
|
16
16
|
snapshotProvider,
|
|
17
|
+
setApiKey,
|
|
18
|
+
isOAuthProvider,
|
|
19
|
+
API_KEY_ONLY_PROVIDERS,
|
|
17
20
|
type AuthProvider,
|
|
18
21
|
type OAuthController,
|
|
19
22
|
} from "../auth";
|
|
@@ -29,7 +32,7 @@ export async function runAuthCommand(args: string[]): Promise<void> {
|
|
|
29
32
|
process.exitCode = 1;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity"]
|
|
35
|
+
const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity", "xai", "kimi"];
|
|
33
36
|
/** True (and prints an error + sets exit code) when `p` is given but not a known provider. */
|
|
34
37
|
function rejectInvalidProvider(p: string | undefined): boolean {
|
|
35
38
|
if (p !== undefined && !(CLOUD_PROVIDERS as readonly string[]).includes(p)) {
|
|
@@ -53,7 +56,7 @@ async function runAuthStatus(): Promise<void> {
|
|
|
53
56
|
const cfg = await readGlobalConfig();
|
|
54
57
|
console.log("\n=== jeo auth status ===");
|
|
55
58
|
console.log("Provider API key OAuth");
|
|
56
|
-
for (const p of ["anthropic", "openai", "gemini", "antigravity"] as AuthProvider[]) {
|
|
59
|
+
for (const p of ["anthropic", "openai", "gemini", "antigravity", "xai", "kimi"] as AuthProvider[]) {
|
|
57
60
|
const snap = await snapshotProvider(p);
|
|
58
61
|
const key = p === "antigravity" ? "—" : (snap.apiKey ? "set" : "—");
|
|
59
62
|
let oauth = "—";
|
|
@@ -99,6 +102,17 @@ async function runAuthLogin(rest: string[]): Promise<void> {
|
|
|
99
102
|
rl.close();
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
105
|
+
// API-key-only providers (xai/kimi): no OAuth flow — store/guide an API key.
|
|
106
|
+
if ((API_KEY_ONLY_PROVIDERS as readonly string[]).includes(chosen)) {
|
|
107
|
+
rl.close();
|
|
108
|
+
if (manualToken) {
|
|
109
|
+
await setApiKey(chosen, manualToken.trim());
|
|
110
|
+
console.log(`[SUCCESS] Stored ${chosen.toUpperCase()}_API_KEY in ~/.jeo/config.json.`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`Provider '${chosen}' is API-key only (no OAuth flow). Set ${chosen.toUpperCase()}_API_KEY, or run 'jeo auth login ${chosen} --token <key>'.`);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
102
116
|
|
|
103
117
|
// Non-interactive paste path (`--token`): store as a manual bearer.
|
|
104
118
|
if (manualToken) {
|
|
@@ -213,6 +227,9 @@ export async function interactiveOAuthLogin(
|
|
|
213
227
|
prompt: OAuthPrompt,
|
|
214
228
|
log: (s: string) => void = console.log,
|
|
215
229
|
): Promise<{ email?: string }> {
|
|
230
|
+
if (!isOAuthProvider(provider)) {
|
|
231
|
+
throw new Error(`Provider '${provider}' is API-key only (no OAuth flow) — set ${provider.toUpperCase()}_API_KEY or run 'jeo auth login ${provider} --token <key>'.`);
|
|
232
|
+
}
|
|
216
233
|
const flow = OAUTH_FLOW_REGISTRY[provider];
|
|
217
234
|
log(`\n=== OAuth login — ${flow.label} ===`);
|
|
218
235
|
if (!flow.verifiedEndToEnd && flow.note) log(`Note: ${flow.note}`);
|
|
@@ -51,7 +51,11 @@ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefin
|
|
|
51
51
|
const supported = catalogMetadata(modelId)?.thinking ?? [];
|
|
52
52
|
if (supported.includes("minimal")) return "minimal";
|
|
53
53
|
if (supported.includes("low")) return "low";
|
|
54
|
-
|
|
54
|
+
// Digit-count agnostic (gemini-10+ / 2.6+ stay reasoning) — mirrors the gates in
|
|
55
|
+
// gemini.ts and inferCatalogMetadata. Last resort for prefixed ids (models/gemini-…)
|
|
56
|
+
// the catalog lookup above misses; catalogued ids already returned via thinking caps.
|
|
57
|
+
const g = modelId.toLowerCase().match(/gemini-(\d+)(?:\.(\d+))?/);
|
|
58
|
+
if (g && (Number(g[1]) >= 3 || (Number(g[1]) === 2 && Number(g[2] ?? 0) >= 5))) return "minimal";
|
|
55
59
|
return undefined;
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -395,3 +395,16 @@ export function createInFlightAbortHarness(opts: AbortHarnessOptions = {}): InFl
|
|
|
395
395
|
},
|
|
396
396
|
};
|
|
397
397
|
}
|
|
398
|
+
|
|
399
|
+
/** Classify a mid-turn Enter draft. `/` (slash command) and `$` (skill) are jeo's
|
|
400
|
+
* command sigils: such a line must run as a COMMAND, never be steered as literal text
|
|
401
|
+
* into the running model. Anything else is a STEER query fed to the live turn; blank
|
|
402
|
+
* is EMPTY (ignored). Pure + exported so the live-turn handler and tests can't drift. */
|
|
403
|
+
export function classifyMidTurnLine(line: string): "command" | "steer" | "empty" {
|
|
404
|
+
const t = line.trim();
|
|
405
|
+
if (!t) return "empty";
|
|
406
|
+
// A lone sigil ("/" or "$") has no command name — ignore it instead of aborting the
|
|
407
|
+
// running turn to dispatch an empty command (a stray slash should not interrupt work).
|
|
408
|
+
if (t === "/" || t === "$") return "empty";
|
|
409
|
+
return /^[/$]/.test(t) ? "command" : "steer";
|
|
410
|
+
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { interactiveOAuthLogin } from "./auth";
|
|
|
20
20
|
import { logoutOAuth } 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 } 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";
|
|
@@ -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,
|
|
@@ -219,7 +222,12 @@ export function normalizeSlashAlias(input: string): string {
|
|
|
219
222
|
return input;
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
|
|
225
|
+
// Per-provider starting model for `--provider <name>` / role pinning. Catalog
|
|
226
|
+
// OpenAI-compatible providers supply their own default; built-ins use this map.
|
|
227
|
+
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" };
|
|
228
|
+
function providerDefaultModel(p: ProviderName): string {
|
|
229
|
+
return openaiCompatDef(p)?.defaultModel ?? STATIC_PROVIDER_DEFAULT[p] ?? "";
|
|
230
|
+
}
|
|
223
231
|
|
|
224
232
|
|
|
225
233
|
export function formatResumeHint(sessionId: string): string {
|
|
@@ -263,7 +271,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
263
271
|
const defaultModel = cfg.defaultModel;
|
|
264
272
|
const initialSessionModel =
|
|
265
273
|
flags.model ??
|
|
266
|
-
(flags.modelRole ? resolveRoleModel(flags.modelRole, cfg) : flags.provider ?
|
|
274
|
+
(flags.modelRole ? resolveRoleModel(flags.modelRole, cfg) : flags.provider ? providerDefaultModel(flags.provider) : undefined);
|
|
267
275
|
if (flags.provider && initialSessionModel) {
|
|
268
276
|
const { provider } = await describeModel(initialSessionModel);
|
|
269
277
|
if (provider !== flags.provider) {
|
|
@@ -472,6 +480,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
472
480
|
// Full untruncated output of the most recent tool call — the clipped forge
|
|
473
481
|
// card's `⟦Ctrl+O for more⟧` hint resolves here.
|
|
474
482
|
let lastToolDetail: { tool: string; output: string } | null = null;
|
|
483
|
+
// Accumulated reasoning/thinking for the in-flight turn (the model's thought before its
|
|
484
|
+
// answer). Captured from the reasoning stream and persisted on the assistant message so
|
|
485
|
+
// it survives /resume + export (gjc "think → answer" record). Reset at each turn start.
|
|
486
|
+
let lastTurnReasoning = "";
|
|
475
487
|
/** Wrap turn events so EVERY sink (TUI or plain stream) records the last full
|
|
476
488
|
* tool output for the Ctrl+O detail view. */
|
|
477
489
|
const withToolDetailCapture = (base: ReturnType<LaunchTui["events"]>): ReturnType<LaunchTui["events"]> => ({
|
|
@@ -480,6 +492,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
480
492
|
lastToolDetail = { tool, output };
|
|
481
493
|
base.onToolResult?.(tool, success, output);
|
|
482
494
|
},
|
|
495
|
+
onReasoningStream: (textSoFar: string) => {
|
|
496
|
+
// textSoFar is the cumulative thought for the current step; keep the latest
|
|
497
|
+
// non-empty value (the thought immediately preceding the turn's answer).
|
|
498
|
+
if (textSoFar.trim()) lastTurnReasoning = textSoFar;
|
|
499
|
+
base.onReasoningStream?.(textSoFar);
|
|
500
|
+
},
|
|
483
501
|
});
|
|
484
502
|
/** Compose a session-persistence flush into onStep so each completed step is
|
|
485
503
|
* written as it lands (durability across mid-turn interruption) without
|
|
@@ -547,6 +565,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
547
565
|
// Clears the live next-prompt draft — used after a mid-turn Enter is lifted into
|
|
548
566
|
// the steering inbox so the consumed line does not also become the next prompt.
|
|
549
567
|
let queueBusyClear: (() => void) | undefined;
|
|
568
|
+
// Routes a command-shaped (/… or $…) mid-turn draft into the idle loop's
|
|
569
|
+
// pending-line queue so it runs as a real COMMAND at the turn boundary,
|
|
570
|
+
// instead of being steered into the model as literal text.
|
|
571
|
+
let queueBusyCommand: ((line: string) => void) | undefined;
|
|
550
572
|
let interactiveTurnActive = false;
|
|
551
573
|
|
|
552
574
|
|
|
@@ -577,6 +599,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
577
599
|
// AFTER compaction (which mutates history) and consumed by the post-turn
|
|
578
600
|
// persistence block below.
|
|
579
601
|
let beforeLen = history.length;
|
|
602
|
+
lastTurnReasoning = ""; // fresh turn: capture this turn's thinking from scratch
|
|
580
603
|
// Incremental session persistence (durability across mid-turn interruption):
|
|
581
604
|
// persistTurnTail() flushes history messages added since the last flush — called
|
|
582
605
|
// right after the user prompt, on every onStep boundary, and once post-turn — so
|
|
@@ -675,12 +698,34 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
675
698
|
if (typedEnter) {
|
|
676
699
|
const line = (queueBusySnapshot?.().text ?? "").trim();
|
|
677
700
|
if (line) {
|
|
678
|
-
|
|
701
|
+
// A mid-turn /command or $skill is NOT a query for the model — steering it
|
|
702
|
+
// would send the literal "/model" / "$skill" text to the LLM. Recognize it
|
|
703
|
+
// and run it as a real COMMAND: queue it for the idle dispatcher and stop the
|
|
704
|
+
// turn so it runs at once (below). Plain queries still steer into the running
|
|
705
|
+
// turn. JEO_NO_STEER=1 disables both (legacy draft-only).
|
|
679
706
|
queueBusyClear?.();
|
|
680
707
|
tui.setLivePromptInput("");
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
708
|
+
tui.setLivePromptHint([]);
|
|
709
|
+
if (classifyMidTurnLine(line) === "command") {
|
|
710
|
+
// Run it as a real COMMAND: queue it for immediate dispatch by the prompt
|
|
711
|
+
// loop and abort the turn (the same controller Esc uses). The abort ends a
|
|
712
|
+
// streaming turn at once and cancels any further steps; a running tool still
|
|
713
|
+
// finishes first (jeo's abort is step-level, like Esc). The queued command is
|
|
714
|
+
// then auto-dispatched — no second Enter. JEO_NO_MIDTURN_DISPATCH=1 keeps the
|
|
715
|
+
// legacy behavior (queue to prefill, no interrupt, press Enter to run).
|
|
716
|
+
queueBusyCommand?.(line);
|
|
717
|
+
if (jeoEnv("NO_MIDTURN_DISPATCH") === "1") {
|
|
718
|
+
tui.events().onNotice?.(`⌘ queued ${line} — press Enter after this turn to run`);
|
|
719
|
+
} else {
|
|
720
|
+
tui.events().onNotice?.(`⌘ ${line} — interrupting the turn to run it`);
|
|
721
|
+
harness.controller.abort();
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
steerInbox.push(line);
|
|
725
|
+
// Surface the steered query as a `user` card in scrollback so it reads
|
|
726
|
+
// as an accepted input that started work — not just a transient notice.
|
|
727
|
+
tui.flushSteerCard(line);
|
|
728
|
+
}
|
|
684
729
|
return;
|
|
685
730
|
}
|
|
686
731
|
}
|
|
@@ -691,7 +736,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
691
736
|
// suppressed for the whole turn). On Enter the draft is lifted into the steering
|
|
692
737
|
// inbox and surfaces as a `user` card (above). JEO_NO_LIVE_DRAFT=1 opts out.
|
|
693
738
|
if (captured && jeoEnv("NO_LIVE_DRAFT") !== "1") {
|
|
694
|
-
|
|
739
|
+
const draft = queueBusySnapshot?.().text ?? "";
|
|
740
|
+
tui.setLivePromptInput(draft);
|
|
741
|
+
// Mid-turn command preview: as you type a /command or $skill DURING a turn,
|
|
742
|
+
// show its matches above the input box so command input visibly reacts
|
|
743
|
+
// (idle-prompt parity). Cleared the moment the draft stops being command-shaped.
|
|
744
|
+
tui.setLivePromptHint(
|
|
745
|
+
/^\s*[/$]/.test(draft) ? formatMidTurnHint(draft.trimStart(), completionContext(), 5) : [],
|
|
746
|
+
);
|
|
695
747
|
}
|
|
696
748
|
},
|
|
697
749
|
onAbortNotice: msg => {
|
|
@@ -851,8 +903,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
851
903
|
// this only covers the tail — net content is the full turn either way.
|
|
852
904
|
try {
|
|
853
905
|
await persistTurnTail();
|
|
854
|
-
|
|
855
|
-
|
|
906
|
+
const assistantMsg: Message = lastTurnReasoning.trim()
|
|
907
|
+
? { role: "assistant", content: reply, reasoning: lastTurnReasoning }
|
|
908
|
+
: { role: "assistant", content: reply };
|
|
909
|
+
history.push(assistantMsg);
|
|
910
|
+
if (sessionId) await appendMessage(sessionId, assistantMsg, cwd);
|
|
856
911
|
if (tui) tui.finish(reply);
|
|
857
912
|
} finally {
|
|
858
913
|
if (tui) interactiveTurnActive = false;
|
|
@@ -1356,6 +1411,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1356
1411
|
// only captures the line submitted while it is registered; orphan lines emit
|
|
1357
1412
|
// 'line' instead), so queue those and serve them before prompting again.
|
|
1358
1413
|
const pendingStdinLines: string[] = [];
|
|
1414
|
+
// Commands submitted mid-turn (/… or $…) land here; the prompt loop dispatches them
|
|
1415
|
+
// IMMEDIATELY on its next iteration, bypassing the "new input first" prefill contract
|
|
1416
|
+
// (the user explicitly invoked them — no second Enter).
|
|
1417
|
+
const pendingMidTurnCommands: string[] = [];
|
|
1359
1418
|
const queuedPromptInput: PromptInputQueue = { pendingLines: pendingStdinLines, partial: "", pastedLines: [], inPaste: false };
|
|
1360
1419
|
queueBusyInput = (chunk: string) => captureLivePromptInputChunk(queuedPromptInput, chunk);
|
|
1361
1420
|
queueBusyPasteActive = () => queuedPromptInput.inPaste;
|
|
@@ -1363,6 +1422,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1363
1422
|
text: queuedPromptInput.partial,
|
|
1364
1423
|
});
|
|
1365
1424
|
queueBusyClear = () => { queuedPromptInput.partial = ""; };
|
|
1425
|
+
queueBusyCommand = (line: string) => {
|
|
1426
|
+
// NO_MIDTURN_DISPATCH=1 keeps the legacy prefill path (tee up, press Enter); the
|
|
1427
|
+
// default routes to the immediate-dispatch queue served at the top of the loop.
|
|
1428
|
+
(jeoEnv("NO_MIDTURN_DISPATCH") === "1" ? pendingStdinLines : pendingMidTurnCommands).push(line);
|
|
1429
|
+
};
|
|
1366
1430
|
// Bracketed-paste line routing at the PROMPT: readline strips the 2004 markers
|
|
1367
1431
|
// and replays pasted lines as synthetic keypresses, emitting paste-start /
|
|
1368
1432
|
// paste-end around them. Lines submitted INSIDE that window are intentional
|
|
@@ -2482,7 +2546,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2482
2546
|
// Box mode: NO raw `jeo>` prompt at all — the boxed footer IS the input UI
|
|
2483
2547
|
// (gating already suppresses readline echo, the empty prompt guarantees no
|
|
2484
2548
|
// raw CLI input line can ever flash). Legacy prompt only without the box.
|
|
2485
|
-
const rawText =
|
|
2549
|
+
const rawText = pendingMidTurnCommands.length
|
|
2550
|
+
? (disarmPreview(), pendingMidTurnCommands.shift()!)
|
|
2551
|
+
: await promptInput(previewEnabled ? "" : "\njeo> ");
|
|
2486
2552
|
if (rawText.includes("\u0003")) forceExitFromCtrlC();
|
|
2487
2553
|
const raw = rawText.trim();
|
|
2488
2554
|
disarmPreview();
|
|
@@ -3216,7 +3282,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3216
3282
|
// No model given → the provider's first live model, provider-qualified.
|
|
3217
3283
|
chosenModel = qualifyModelId(forProvider[0]!.model, want);
|
|
3218
3284
|
} else {
|
|
3219
|
-
chosenModel =
|
|
3285
|
+
chosenModel = providerDefaultModel(want);
|
|
3220
3286
|
}
|
|
3221
3287
|
await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, role.id, { model: chosenModel }) }));
|
|
3222
3288
|
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.
|