jeo-code 0.6.3 → 0.6.4
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 +16 -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/engine.ts +82 -26
- package/src/agent/goal-verifier.ts +115 -0
- package/src/agent/model-recency.ts +1 -1
- package/src/agent/tools.ts +77 -17
- package/src/commands/launch.ts +218 -136
- package/src/tui/app.ts +87 -25
- package/src/tui/components/autocomplete.ts +6 -4
- package/src/tui/components/config-panel.ts +2 -2
- package/src/tui/components/markdown-text.ts +19 -5
- package/src/tui/components/slash.ts +25 -2
package/src/commands/launch.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { formatForgeBox } from "../tui/components/forge";
|
|
|
19
19
|
import { interactiveOAuthLogin } from "./auth";
|
|
20
20
|
import { logoutOAuth } from "../auth";
|
|
21
21
|
import type { AuthProvider } from "../auth";
|
|
22
|
-
import { matchSlash, isSlashAttempt, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
|
|
22
|
+
import { matchSlash, isSlashAttempt, suggestSlashCommands, formatSlashCommandList, formatSlashPreview, slashPreviewMatches, activeTriggerToken, tabCompleteSelection, type SlashCommandInfo } from "../tui/components/slash";
|
|
23
23
|
import { staticCompletionContext, readlineCompleter, formatCompletionPreview, tokenize, type CompletionContext } from "../tui/components/autocomplete";
|
|
24
|
+
import { normalizeBaseUrl } from "./setup-helpers";
|
|
24
25
|
import { EVOLUTION_STAGES, animateAsciiArt } from "../tui/components/ascii-art";
|
|
25
26
|
import { getEvolutionTip } from "../tui/components/evolution";
|
|
26
27
|
import { renderWelcome, playWelcomeSweep } from "../tui/components/welcome";
|
|
@@ -37,6 +38,7 @@ import { readGlobalConfig, saveConfigPatch } from "../agent/state";
|
|
|
37
38
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
38
39
|
import { describeModel, describeAllProviders, thinkingMaxTokens, discoverModels, flattenModels, resolveSelection, catalogMetadata, resolveRoleModel, CODEX_MODELS, qualifyModelId } from "../ai";
|
|
39
40
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
41
|
+
import { readGoalState, writeGoalState, clearGoalState, verifyGoal } from "../agent/goal-verifier";
|
|
40
42
|
|
|
41
43
|
import { listAliases } from "../ai/model-registry";
|
|
42
44
|
|
|
@@ -45,7 +47,6 @@ import { SelectList, renderSelectList, type SelectItem } from "../tui/components
|
|
|
45
47
|
import {
|
|
46
48
|
formatModelLine,
|
|
47
49
|
formatProviderPanel,
|
|
48
|
-
emitLoginCleanup,
|
|
49
50
|
formatAgentsPanel,
|
|
50
51
|
formatAgentDetail,
|
|
51
52
|
formatConfigPanel,
|
|
@@ -56,7 +57,7 @@ import {
|
|
|
56
57
|
} from "../tui/components/config-panel";
|
|
57
58
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
58
59
|
|
|
59
|
-
import { providerPicker, renderProviderPicker
|
|
60
|
+
import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
|
|
60
61
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
61
62
|
import { categoryBadge } from "../tui/components/category-index";
|
|
62
63
|
import { renderInputFrame } from "../tui/components/input-box";
|
|
@@ -1549,14 +1550,48 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1549
1550
|
// Todos checklist used to end a finished turn stuck at "✓0 ◐1 ·4 / 5"
|
|
1550
1551
|
// because nothing ever forced the model to update item statuses).
|
|
1551
1552
|
let turnTodos: { title: string; status: string }[] = [];
|
|
1552
|
-
const onBeforeDone = (): string | null => {
|
|
1553
|
+
const onBeforeDone = async (reason: string): Promise<string | null> => {
|
|
1553
1554
|
const unfinished = turnTodos.filter(t => t.status !== "done");
|
|
1554
|
-
if (turnTodos.length
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1555
|
+
if (turnTodos.length > 0 && unfinished.length > 0) {
|
|
1556
|
+
return (
|
|
1557
|
+
`Your todo list still shows ${unfinished.length} unfinished item(s): ${unfinished.map(t => `"${t.title}"`).join(", ")}. ` +
|
|
1558
|
+
`Reconcile the plan first — call the todo tool resending the FULL list with every actually-completed item marked "done" ` +
|
|
1559
|
+
`(drop items that no longer apply), then call done again.`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const goalState = await readGoalState(cwd);
|
|
1564
|
+
if (goalState && goalState.condition) {
|
|
1565
|
+
const reBlockCount = goalState.verdicts.filter(v => v.verdict === "NOT_MET" || v.verdict === "IMPOSSIBLE").length;
|
|
1566
|
+
const MAX_RE_BLOCKS = 2;
|
|
1567
|
+
|
|
1568
|
+
if (reBlockCount >= MAX_RE_BLOCKS) {
|
|
1569
|
+
if (tui) tui.events().onNotice?.(`[Goal Verifier] Re-block cap of ${MAX_RE_BLOCKS} reached. Auto-allowing done.`);
|
|
1570
|
+
else console.log(`[Goal Verifier] Re-block cap of ${MAX_RE_BLOCKS} reached. Auto-allowing done.`);
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (tui) tui.events().onNotice?.("[Goal Verifier] Running goal verification...");
|
|
1575
|
+
const verdict = await verifyGoal(goalState.condition, history, sessionModel);
|
|
1576
|
+
|
|
1577
|
+
goalState.verdicts.push({
|
|
1578
|
+
at: Date.now(),
|
|
1579
|
+
verdict: verdict.verdict,
|
|
1580
|
+
gap: verdict.reason,
|
|
1581
|
+
});
|
|
1582
|
+
await writeGoalState(goalState, cwd);
|
|
1583
|
+
|
|
1584
|
+
if (verdict.verdict === "NOT_MET" || verdict.verdict === "IMPOSSIBLE") {
|
|
1585
|
+
const prefix = verdict.verdict === "IMPOSSIBLE" ? "[IMPOSSIBLE] " : "";
|
|
1586
|
+
return (
|
|
1587
|
+
`Goal verifier check failed: The goal "${goalState.condition}" is not yet met.\n` +
|
|
1588
|
+
`Reason: ${prefix}${verdict.reason}\n` +
|
|
1589
|
+
`Please address the remaining gaps and call done again.`
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return null;
|
|
1560
1595
|
};
|
|
1561
1596
|
const fullTools = {
|
|
1562
1597
|
...DEFAULT_TOOLS,
|
|
@@ -2473,7 +2508,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2473
2508
|
const caret = rli.line === line && typeof rli.cursor === "number" ? rli.cursor : line.length;
|
|
2474
2509
|
const { accent: boxAccent, shadow: boxShadow } = boxAccents(line);
|
|
2475
2510
|
const frame = renderInputFrame(expandSentinel(line), {
|
|
2476
|
-
cols
|
|
2511
|
+
// Cap the input box at 120 cols to match the live-turn box (renderLiveInputBox)
|
|
2512
|
+
// and the user-card width, so the box doesn't visibly jump width on the
|
|
2513
|
+
// idle→live transition on wide terminals. The status bar below stays full-width.
|
|
2514
|
+
cols: Math.min(120, cols),
|
|
2477
2515
|
color: true,
|
|
2478
2516
|
unicode: true,
|
|
2479
2517
|
accent: boxAccent,
|
|
@@ -3121,16 +3159,48 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3121
3159
|
// Idle-prompt resize: re-reserve the footer at the new terminal height so the
|
|
3122
3160
|
// fixed reservation stays accurate (otherwise the next paint would target the
|
|
3123
3161
|
// old row count and either over-shoot or under-paint the reserved region).
|
|
3124
|
-
|
|
3162
|
+
// gjc-style responsiveness: re-reserve the footer IMMEDIATELY on the first resize
|
|
3163
|
+
// event (leading edge → no lag), then cap re-reservations to ~30fps during a
|
|
3164
|
+
// drag-resize, with a trailing settle so the FINAL height is always reserved exactly.
|
|
3165
|
+
// Skip same-geometry events; each re-reserve disarms→arms so it must not run per-event.
|
|
3166
|
+
const RESIZE_THROTTLE_MS = 33;
|
|
3167
|
+
let idleResizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
3168
|
+
let lastIdleAt = 0;
|
|
3169
|
+
let lastIdleCols = process.stdout.columns ?? 80;
|
|
3170
|
+
let lastIdleRows = process.stdout.rows ?? 24;
|
|
3171
|
+
const reReserveFooter = () => {
|
|
3125
3172
|
if (!previewArmed) return;
|
|
3173
|
+
const cols = process.stdout.columns ?? 80;
|
|
3174
|
+
const rows = process.stdout.rows ?? 24;
|
|
3175
|
+
if (cols === lastIdleCols && rows === lastIdleRows) return;
|
|
3176
|
+
lastIdleCols = cols;
|
|
3177
|
+
lastIdleRows = rows;
|
|
3126
3178
|
try {
|
|
3127
3179
|
disarmPreview();
|
|
3128
3180
|
armPreview();
|
|
3129
3181
|
drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
|
|
3130
3182
|
} catch { /* ignore resize render races */ }
|
|
3131
3183
|
};
|
|
3184
|
+
const idleResizeHandler = () => {
|
|
3185
|
+
if (!previewArmed) return;
|
|
3186
|
+
const now = Date.now();
|
|
3187
|
+
if (now - lastIdleAt >= RESIZE_THROTTLE_MS) {
|
|
3188
|
+
lastIdleAt = now;
|
|
3189
|
+
reReserveFooter();
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
if (idleResizeTimer) clearTimeout(idleResizeTimer);
|
|
3193
|
+
idleResizeTimer = setTimeout(() => {
|
|
3194
|
+
idleResizeTimer = undefined;
|
|
3195
|
+
lastIdleAt = Date.now();
|
|
3196
|
+
reReserveFooter();
|
|
3197
|
+
}, RESIZE_THROTTLE_MS);
|
|
3198
|
+
};
|
|
3132
3199
|
process.stdout.on("resize", idleResizeHandler);
|
|
3133
|
-
promptListenerCleanups.push(() =>
|
|
3200
|
+
promptListenerCleanups.push(() => {
|
|
3201
|
+
process.stdout.off("resize", idleResizeHandler);
|
|
3202
|
+
if (idleResizeTimer) clearTimeout(idleResizeTimer);
|
|
3203
|
+
});
|
|
3134
3204
|
}
|
|
3135
3205
|
|
|
3136
3206
|
while (true) {
|
|
@@ -3548,6 +3618,32 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3548
3618
|
}
|
|
3549
3619
|
continue;
|
|
3550
3620
|
}
|
|
3621
|
+
if (input === "/goal" || input.startsWith("/goal ")) {
|
|
3622
|
+
const arg = input.substring(5).trim();
|
|
3623
|
+
if (!arg) {
|
|
3624
|
+
const current = await readGoalState(cwd);
|
|
3625
|
+
if (current) {
|
|
3626
|
+
console.log(`Current goal: "${current.condition}" (set at ${new Date(current.setAt).toLocaleTimeString()})`);
|
|
3627
|
+
} else {
|
|
3628
|
+
console.log("Usage: /goal <condition> (set a natural language stop condition for the session)");
|
|
3629
|
+
console.log(" /goal clear (clear the current goal)");
|
|
3630
|
+
}
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
if (arg === "clear" || arg === "none") {
|
|
3634
|
+
await clearGoalState(cwd);
|
|
3635
|
+
console.log("Goal cleared.");
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3638
|
+
const state = {
|
|
3639
|
+
condition: arg,
|
|
3640
|
+
setAt: Date.now(),
|
|
3641
|
+
verdicts: [],
|
|
3642
|
+
};
|
|
3643
|
+
await writeGoalState(state, cwd);
|
|
3644
|
+
console.log(`Goal set to: "${arg}"`);
|
|
3645
|
+
continue;
|
|
3646
|
+
}
|
|
3551
3647
|
// ---- gjc-parity inspection commands ------------------------------------
|
|
3552
3648
|
if (input === "/usage") {
|
|
3553
3649
|
const total = sessionUsage.inputTokens + sessionUsage.outputTokens;
|
|
@@ -3632,8 +3728,17 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3632
3728
|
}
|
|
3633
3729
|
if (input.startsWith("/provider") && (input === "/provider" || input[9] === " ")) {
|
|
3634
3730
|
const tokens = input.substring(9).trim().split(/\s+/).filter(Boolean);
|
|
3635
|
-
|
|
3636
|
-
|
|
3731
|
+
const name = (tokens[0] ?? "").toLowerCase();
|
|
3732
|
+
// gjc-parity (semantic): /provider is ONBOARDING ONLY — set up OAuth credentials
|
|
3733
|
+
// or an API-compatible endpoint. Switching the active provider/model lives in /model.
|
|
3734
|
+
const providerOnboardingUsage = (): string[] => [
|
|
3735
|
+
"Provider onboarding — set up credentials or an API-compatible endpoint:",
|
|
3736
|
+
" OAuth / subscription : /provider login [anthropic|openai|gemini|antigravity] (alias: /login)",
|
|
3737
|
+
" API-compatible : /provider add --base-url <url> [--model <model>] [--compat openai] (reads OPENAI_API_KEY)",
|
|
3738
|
+
" show current / clear: /provider add · /provider add clear",
|
|
3739
|
+
" Logout : /logout <provider>",
|
|
3740
|
+
"Switch the active model or provider with /model.",
|
|
3741
|
+
];
|
|
3637
3742
|
// `/provider login|auth [name]` → run OAuth login from the REPL.
|
|
3638
3743
|
if (name === "login" || name === "auth") {
|
|
3639
3744
|
const cloud = ["anthropic", "openai", "gemini", "antigravity"] as const;
|
|
@@ -3643,14 +3748,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3643
3748
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3644
3749
|
target = await pickCloudProvider(statuses);
|
|
3645
3750
|
} else {
|
|
3646
|
-
// No provider given → show current status and let the user pick.
|
|
3647
3751
|
console.log("Log in to which provider?");
|
|
3648
3752
|
cloud.forEach((p, i) => {
|
|
3649
3753
|
const st = statuses.find(s => s.name === p);
|
|
3650
3754
|
console.log(` ${i + 1}) ${p.padEnd(10)} ${st?.ready ? `✓ ${st.label}` : "· not ready"}`);
|
|
3651
3755
|
});
|
|
3652
|
-
const ans = (await promptInput(
|
|
3653
|
-
const byNum: Record<string, string> =
|
|
3756
|
+
const ans = (await promptInput(`Choose [1-${cloud.length}] or name (blank to cancel): `)).trim().toLowerCase();
|
|
3757
|
+
const byNum: Record<string, string> = Object.fromEntries(cloud.map((p, i) => [String(i + 1), p]));
|
|
3654
3758
|
target = byNum[ans] ?? ((cloud as readonly string[]).includes(ans) ? ans : undefined);
|
|
3655
3759
|
}
|
|
3656
3760
|
if (!target) {
|
|
@@ -3661,131 +3765,107 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3661
3765
|
console.log(`Starting OAuth login for ${target}…`);
|
|
3662
3766
|
try {
|
|
3663
3767
|
const { email } = await interactiveOAuthLogin(target as AuthProvider, rl);
|
|
3664
|
-
|
|
3665
|
-
// browser prompts / "waiting…" lines that clutter scrollback. Clear the
|
|
3666
|
-
// screen + scrollback and re-render the welcome (same path as /clear),
|
|
3667
|
-
// then a single concise confirmation. lastPickIndex is still seeded so
|
|
3668
|
-
// `/model #N` works; the verbose live-model dump is dropped (it cluttered).
|
|
3768
|
+
console.log(`[SUCCESS] OAuth login complete for ${target}${email ? ` (${email})` : ""}. Tokens saved to ~/.jeo/config.json.`);
|
|
3669
3769
|
const live = await refreshLiveModelsCache();
|
|
3670
3770
|
const after = (await describeAllProviders()).find(s => s.name === target);
|
|
3771
|
+
if (after) console.log(` status → ${after.name}: ${after.ready ? `✓ ${after.label}` : after.label}`);
|
|
3671
3772
|
const forProvider = live.filter(r => r.provider === target);
|
|
3672
|
-
if (forProvider.some(r => r.ok && r.models.length > 0))
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
{
|
|
3680
|
-
|
|
3681
|
-
write: line => console.log(line),
|
|
3682
|
-
},
|
|
3683
|
-
{
|
|
3684
|
-
isTty: process.stdout.isTTY === true,
|
|
3685
|
-
provider: target,
|
|
3686
|
-
email,
|
|
3687
|
-
ready: after?.ready ?? false,
|
|
3688
|
-
label: after?.label,
|
|
3689
|
-
welcomeLines: renderWelcome(welcomeData),
|
|
3690
|
-
},
|
|
3691
|
-
);
|
|
3773
|
+
if (forProvider.some(r => r.ok && r.models.length > 0)) {
|
|
3774
|
+
lastPickIndex = flattenModels(forProvider);
|
|
3775
|
+
const viaCatalog = forProvider.some(r => r.fallback);
|
|
3776
|
+
console.log(` ${viaCatalog ? "catalog" : "live"} ${target} models → /model #N${viaCatalog ? " (live list endpoint rejected this token; showing known models)" : ""}`);
|
|
3777
|
+
logLines(formatPickListWithCapabilities(lastPickIndex, { cap: 12 }));
|
|
3778
|
+
} else {
|
|
3779
|
+
const failed = forProvider.find(r => !r.ok);
|
|
3780
|
+
if (failed?.error) console.log(` live ${target} models unavailable: ${failed.error}`);
|
|
3781
|
+
}
|
|
3692
3782
|
} catch (err) {
|
|
3693
3783
|
console.log(`[FAILED] ${(err as Error).message} — or set ${target.toUpperCase()}_API_KEY.`);
|
|
3784
|
+
} finally {
|
|
3785
|
+
// The OAuth manual-code prompt opens an rl.question that is aborted the
|
|
3786
|
+
// instant the browser callback (or a failure) settles the flow. Bun leaves
|
|
3787
|
+
// that abandoned question's buffer behind, so the next prompt would adopt
|
|
3788
|
+
// it as residual prefill and resurrect the submitted "/provider login" in
|
|
3789
|
+
// the input box. Reset the readline line buffer so the prompt starts empty.
|
|
3790
|
+
const rli = rl as unknown as { line?: string; cursor?: number };
|
|
3791
|
+
rli.line = "";
|
|
3792
|
+
rli.cursor = 0;
|
|
3694
3793
|
}
|
|
3695
3794
|
continue;
|
|
3696
3795
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
label: c.label,
|
|
3707
|
-
hint: c.hint,
|
|
3708
|
-
group: c.group,
|
|
3709
|
-
}));
|
|
3710
|
-
const picked = await pickFromOptions("Select a provider ↑↓ move · Enter switch · Esc cancel", items);
|
|
3711
|
-
if (!picked) { console.log("(switch cancelled)"); continue; }
|
|
3712
|
-
name = picked.toLowerCase(); // fall through to the switch logic below
|
|
3713
|
-
} else {
|
|
3714
|
-
console.log("Providers (credential · base URL):");
|
|
3715
|
-
logLines(formatProviderPanel(statuses));
|
|
3716
|
-
console.log("Switch with: /provider <name> [model] · choose models: /model");
|
|
3796
|
+
// `/provider add --base-url <url> [--model <m>] [--compat openai]` → register an
|
|
3797
|
+
// OpenAI-compatible endpoint (sets openaiBaseUrl). gjc flag style; bare positional
|
|
3798
|
+
// URL/model still accepted for leniency. `/provider add clear` removes it.
|
|
3799
|
+
if (name === "add") {
|
|
3800
|
+
const rest = tokens.slice(1);
|
|
3801
|
+
if ((rest[0] ?? "").toLowerCase() === "clear") {
|
|
3802
|
+
await saveConfigPatch(() => ({ openaiBaseUrl: undefined }));
|
|
3803
|
+
await refreshLiveModelsCache();
|
|
3804
|
+
console.log("OpenAI-compatible base URL cleared — saved to ~/.jeo/config.json.");
|
|
3717
3805
|
continue;
|
|
3718
3806
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
let target = explicitModel ?? PROVIDER_DEFAULT[name];
|
|
3734
|
-
if (!explicitModel && providerPick.length && process.stdin.isTTY && process.stdout.isTTY) {
|
|
3735
|
-
const picked = await pickLiveProviderModel(name, providerPick, currentResolved, st && !st.ready && !selectableThoughNotReady(st) ? [name] : []);
|
|
3736
|
-
if (!picked) {
|
|
3737
|
-
console.log("(cancelled)");
|
|
3807
|
+
let baseUrl: string | undefined;
|
|
3808
|
+
let modelArg: string | undefined;
|
|
3809
|
+
let compat: string | undefined;
|
|
3810
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3811
|
+
const t = rest[i]!;
|
|
3812
|
+
if (t === "--base-url" || t === "--url") baseUrl = rest[++i];
|
|
3813
|
+
else if (t === "--model") modelArg = rest[++i];
|
|
3814
|
+
else if (t === "--compat") compat = (rest[++i] ?? "").toLowerCase();
|
|
3815
|
+
else if (t === "--api-key-env" || t === "--provider") { i++; /* gjc-only: jeo has no named-provider registry — ignored */ }
|
|
3816
|
+
else if (!t.startsWith("--") && baseUrl === undefined) baseUrl = t; // positional url
|
|
3817
|
+
else if (!t.startsWith("--") && modelArg === undefined) modelArg = t; // positional model
|
|
3818
|
+
}
|
|
3819
|
+
if (compat && compat !== "openai") {
|
|
3820
|
+
console.log(`Only --compat openai is supported (got '${compat}') — jeo registers a single OpenAI-compatible endpoint.`);
|
|
3738
3821
|
continue;
|
|
3739
3822
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
if (sel.kind === "index" || sel.kind === "match") {
|
|
3745
|
-
target = qualifyModelId(sel.entry.model, sel.entry.provider);
|
|
3746
|
-
if (st && !st.ready) {
|
|
3747
|
-
if (selectableThoughNotReady(st)) {
|
|
3748
|
-
console.log(notReadyWarning(st));
|
|
3749
|
-
} else {
|
|
3750
|
-
console.log(`Cannot select ${sel.entry.model}: ${name} is not ready (${st.label}). Set ${st.envVar ?? "the provider key"} first.`);
|
|
3751
|
-
continue;
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
} else if (sel.kind === "ambiguous") {
|
|
3755
|
-
console.log(`'${explicitModel}' matches ${sel.matches.length} ${name} models — be more specific:`);
|
|
3756
|
-
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
3823
|
+
if (!baseUrl) {
|
|
3824
|
+
const cur = (await readGlobalConfig()).openaiBaseUrl;
|
|
3825
|
+
console.log(cur ? `OpenAI-compatible base URL: ${cur}` : "No OpenAI-compatible base URL set.");
|
|
3826
|
+
console.log("Set one with: /provider add --base-url <url> [--model <model>] · clear with: /provider add clear");
|
|
3757
3827
|
continue;
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3828
|
+
}
|
|
3829
|
+
const url = normalizeBaseUrl(baseUrl, "");
|
|
3830
|
+
if (!url) {
|
|
3831
|
+
console.log("Provide a base URL, e.g. /provider add --base-url http://localhost:1234/v1");
|
|
3760
3832
|
continue;
|
|
3761
3833
|
}
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3834
|
+
await saveConfigPatch(() => ({ openaiBaseUrl: url }));
|
|
3835
|
+
if (modelArg) {
|
|
3836
|
+
const qualified = qualifyModelId(modelArg, "openai");
|
|
3837
|
+
sessionModel = qualified;
|
|
3838
|
+
await saveConfigPatch(raw => rememberModelPatch(raw, qualified));
|
|
3839
|
+
console.log(`OpenAI-compatible endpoint set: ${url} · default model ${qualified} — saved to ~/.jeo/config.json.`);
|
|
3840
|
+
} else {
|
|
3841
|
+
console.log(`OpenAI-compatible endpoint set: ${url} — saved to ~/.jeo/config.json.`);
|
|
3842
|
+
}
|
|
3843
|
+
const live = await refreshLiveModelsCache();
|
|
3844
|
+
const forProvider = live.filter(r => r.provider === "openai");
|
|
3845
|
+
const pick = flattenModels(forProvider);
|
|
3846
|
+
if (pick.length) {
|
|
3847
|
+
lastPickIndex = pick;
|
|
3848
|
+
console.log("Discovered models at this endpoint — select with /model #N:");
|
|
3849
|
+
logLines(formatPickListWithCapabilities(pick, { cap: 12 }));
|
|
3850
|
+
} else {
|
|
3851
|
+
const failed = forProvider.find(r => !r.ok);
|
|
3852
|
+
console.log(failed?.error ? ` could not list models: ${failed.error}` : " no models discovered yet — the endpoint may need a key (set OPENAI_API_KEY).");
|
|
3853
|
+
}
|
|
3770
3854
|
continue;
|
|
3771
3855
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3856
|
+
// Bare `/provider` (or `help`): show readiness + the onboarding usage. No switching.
|
|
3857
|
+
if (!name || name === "help") {
|
|
3858
|
+
const cfgNow = await readGlobalConfig();
|
|
3859
|
+
const statuses = await describeAllProviders(cfgNow);
|
|
3860
|
+
console.log("Providers (credential · base URL):");
|
|
3861
|
+
logLines(formatProviderPanel(statuses));
|
|
3862
|
+
for (const line of providerOnboardingUsage()) console.log(line);
|
|
3774
3863
|
continue;
|
|
3775
3864
|
}
|
|
3776
|
-
|
|
3777
|
-
//
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })} — saved as default`);
|
|
3781
|
-
// Show the provider's live, credentialed catalog so the user can pick a concrete id.
|
|
3782
|
-
if (providerPick.length) {
|
|
3783
|
-
lastPickIndex = providerPick;
|
|
3784
|
-
if (!pickedFromPicker) {
|
|
3785
|
-
console.log(`Live ${name} models — select with /model #N, /provider ${name} #N, or rerun /provider ${name} and use arrows+Enter:`);
|
|
3786
|
-
logLines(formatPickListWithCapabilities(lastPickIndex, { current: resolved }));
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3865
|
+
// Anything else (a provider name) used to switch the active provider — that moved
|
|
3866
|
+
// to /model (gjc semantics: /provider onboards, /model selects).
|
|
3867
|
+
console.log(`'/provider ${name}' no longer switches providers — provider/model selection moved to /model.`);
|
|
3868
|
+
console.log("Run /model to pick any provider's model (or /model <id|#N>). /provider now only onboards — see /provider help.");
|
|
3789
3869
|
continue;
|
|
3790
3870
|
}
|
|
3791
3871
|
if (input.startsWith("/logout") && (input === "/logout" || input[7] === " ")) {
|
|
@@ -3964,11 +4044,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3964
4044
|
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
3965
4045
|
continue;
|
|
3966
4046
|
} else if (sel.kind === "out-of-range") {
|
|
3967
|
-
console.log(`#${modelArg.slice(1)} is out of range (1-${sel.max}). Use /model
|
|
4047
|
+
console.log(`#${modelArg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
3968
4048
|
continue;
|
|
3969
4049
|
}
|
|
3970
4050
|
} else if (modelArg.startsWith("#")) {
|
|
3971
|
-
console.log("Use /model
|
|
4051
|
+
console.log("Use /model first to build the numbered live model list.");
|
|
3972
4052
|
continue;
|
|
3973
4053
|
}
|
|
3974
4054
|
// Persist a per-role model override to ~/.jeo/config.json (consumed by 'jeo team').
|
|
@@ -4038,11 +4118,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4038
4118
|
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4039
4119
|
continue;
|
|
4040
4120
|
} else if (sel.kind === "out-of-range") {
|
|
4041
|
-
console.log(`#${chosenModel.slice(1)} is out of range (1-${sel.max}). Use /model
|
|
4121
|
+
console.log(`#${chosenModel.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4042
4122
|
continue;
|
|
4043
4123
|
}
|
|
4044
4124
|
} else if (chosenModel.startsWith("#")) {
|
|
4045
|
-
console.log("Use /model
|
|
4125
|
+
console.log("Use /model first to build the numbered live model list.");
|
|
4046
4126
|
continue;
|
|
4047
4127
|
}
|
|
4048
4128
|
await saveConfigPatch(raw => ({ roles: { ...(raw.roles ?? {}), [tier]: chosenModel } }));
|
|
@@ -4123,12 +4203,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4123
4203
|
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4124
4204
|
continue;
|
|
4125
4205
|
} else if (sel.kind === "out-of-range") {
|
|
4126
|
-
console.log(`#${toSave.slice(1)} is out of range (1-${sel.max}). Use /model
|
|
4206
|
+
console.log(`#${toSave.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4127
4207
|
continue;
|
|
4128
4208
|
}
|
|
4129
4209
|
// kind "none" → treat `toSave` as a literal model id/alias.
|
|
4130
4210
|
} else if (toSave.startsWith("#")) {
|
|
4131
|
-
console.log("Use /model
|
|
4211
|
+
console.log("Use /model first to build the numbered list.");
|
|
4132
4212
|
continue;
|
|
4133
4213
|
}
|
|
4134
4214
|
// Fall back to the FRESH on-disk default (not the stale session-start snapshot) so a
|
|
@@ -4196,11 +4276,11 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4196
4276
|
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4197
4277
|
continue;
|
|
4198
4278
|
} else if (sel.kind === "out-of-range") {
|
|
4199
|
-
console.log(`#${roleModelArg.slice(1)} is out of range (1-${sel.max}). Use /model
|
|
4279
|
+
console.log(`#${roleModelArg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4200
4280
|
continue;
|
|
4201
4281
|
}
|
|
4202
4282
|
} else if (roleModelArg.startsWith("#")) {
|
|
4203
|
-
console.log("Use /model
|
|
4283
|
+
console.log("Use /model first to build the numbered list.");
|
|
4204
4284
|
continue;
|
|
4205
4285
|
}
|
|
4206
4286
|
if (roleModelArg) {
|
|
@@ -4251,12 +4331,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4251
4331
|
for (const e of sel.matches.slice(0, 12)) console.log(` #${e.index} ${e.model} (${e.provider})`);
|
|
4252
4332
|
continue;
|
|
4253
4333
|
} else if (sel.kind === "out-of-range") {
|
|
4254
|
-
console.log(`#${arg.slice(1)} is out of range (1-${sel.max}). Use /model
|
|
4334
|
+
console.log(`#${arg.slice(1)} is out of range (1-${sel.max}). Use /model first.`);
|
|
4255
4335
|
continue;
|
|
4256
4336
|
}
|
|
4257
4337
|
// kind "none" → fall through and treat `arg` as a literal model id/alias.
|
|
4258
4338
|
} else if (arg.startsWith("#")) {
|
|
4259
|
-
console.log("Use /model
|
|
4339
|
+
console.log("Use /model first to build the numbered list.");
|
|
4260
4340
|
continue;
|
|
4261
4341
|
}
|
|
4262
4342
|
const label = arg || (sessionModel || defaultModel);
|
|
@@ -4282,7 +4362,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4282
4362
|
}
|
|
4283
4363
|
}
|
|
4284
4364
|
if (arg && liveModelsCache && resolved === label && !liveModelKnown(liveModelsCache, resolved)) {
|
|
4285
|
-
console.log(` (note: '${resolved}' is not in the live ${provider} catalog — use /model
|
|
4365
|
+
console.log(` (note: '${resolved}' is not in the live ${provider} catalog — use /model to pick a valid id)`);
|
|
4286
4366
|
}
|
|
4287
4367
|
const meta = catalogMetadata(resolved);
|
|
4288
4368
|
if (meta) console.log(` ${formatCapabilityLine(meta)}`);
|
|
@@ -4450,7 +4530,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4450
4530
|
if (m.length) {
|
|
4451
4531
|
for (const line of formatSlashCommandList(input, skillSlashDetails)) console.log(line);
|
|
4452
4532
|
} else {
|
|
4453
|
-
|
|
4533
|
+
const cmds = [...completionContext().slashCommands];
|
|
4534
|
+
const near = suggestSlashCommands(input, cmds);
|
|
4535
|
+
console.log(near.length ? `Unknown command '${input}'. Did you mean ${near.join(", ")}?` : `Unknown command '${input}'. Try /help.`);
|
|
4454
4536
|
}
|
|
4455
4537
|
continue;
|
|
4456
4538
|
}
|