gearbox-code 0.1.38 → 0.2.1
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/dist/cli.mjs +1543 -580
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -105916,6 +105916,7 @@ var init_dist20 = __esm(() => {
|
|
|
105916
105916
|
// src/accounts/store.ts
|
|
105917
105917
|
var exports_store = {};
|
|
105918
105918
|
__export(exports_store, {
|
|
105919
|
+
uniqueSlug: () => uniqueSlug,
|
|
105919
105920
|
setSecret: () => setSecret,
|
|
105920
105921
|
setDefaultAccount: () => setDefaultAccount,
|
|
105921
105922
|
secretRefs: () => secretRefs,
|
|
@@ -106061,9 +106062,26 @@ function accountsForProvider(provider) {
|
|
|
106061
106062
|
function getAccount(id) {
|
|
106062
106063
|
return listAccounts().find((a) => a.id === id);
|
|
106063
106064
|
}
|
|
106065
|
+
function uniqueSlug(base2, taken) {
|
|
106066
|
+
const norm = base2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "account";
|
|
106067
|
+
if (!taken.includes(norm))
|
|
106068
|
+
return norm;
|
|
106069
|
+
for (let n = 2;; n++) {
|
|
106070
|
+
const cand = `${norm}-${n}`;
|
|
106071
|
+
if (!taken.includes(cand))
|
|
106072
|
+
return cand;
|
|
106073
|
+
}
|
|
106074
|
+
}
|
|
106075
|
+
function deriveSlugBase(a) {
|
|
106076
|
+
return a.label;
|
|
106077
|
+
}
|
|
106064
106078
|
function putAccount(account) {
|
|
106065
106079
|
const f3 = loadAccounts();
|
|
106066
106080
|
const i2 = f3.accounts.findIndex((a) => a.id === account.id);
|
|
106081
|
+
if (!account.slug) {
|
|
106082
|
+
const taken = f3.accounts.filter((a) => a.id !== account.id).map((a) => a.slug ?? "").filter(Boolean);
|
|
106083
|
+
account.slug = i2 >= 0 && f3.accounts[i2].slug || uniqueSlug(deriveSlugBase(account), taken);
|
|
106084
|
+
}
|
|
106067
106085
|
if (i2 >= 0)
|
|
106068
106086
|
f3.accounts[i2] = account;
|
|
106069
106087
|
else
|
|
@@ -106251,6 +106269,32 @@ function modelRegistry() {
|
|
|
106251
106269
|
}
|
|
106252
106270
|
return out;
|
|
106253
106271
|
}
|
|
106272
|
+
function subscriptionSeats() {
|
|
106273
|
+
const out = [];
|
|
106274
|
+
for (const a of listAccounts()) {
|
|
106275
|
+
if (!a.enabled || a.exec !== "cli")
|
|
106276
|
+
continue;
|
|
106277
|
+
const binary = (a.auth.kind === "cli" ? a.auth.binary : undefined) ?? catalogProvider(a.provider)?.binary;
|
|
106278
|
+
if (!binary)
|
|
106279
|
+
continue;
|
|
106280
|
+
const profile = a.auth.kind === "cli" ? a.auth.loginProfile : undefined;
|
|
106281
|
+
const sdkIds = a.models ?? catalogProvider(a.provider)?.defaultModels ?? [];
|
|
106282
|
+
for (const sdkId of sdkIds) {
|
|
106283
|
+
if (!sdkId)
|
|
106284
|
+
continue;
|
|
106285
|
+
const canon = CURATED.find((c) => c.sdkId === sdkId && NATIVE.has(c.provider));
|
|
106286
|
+
const spec6 = {
|
|
106287
|
+
...canon ?? { contextWindow: 200000 },
|
|
106288
|
+
id: `cli:${a.id}:${sdkId}`,
|
|
106289
|
+
provider: `cli:${binary}`,
|
|
106290
|
+
sdkId,
|
|
106291
|
+
label: canon?.label ?? sdkId
|
|
106292
|
+
};
|
|
106293
|
+
out.push({ spec: spec6, canonicalId: canon?.id, account: a, binary, profile });
|
|
106294
|
+
}
|
|
106295
|
+
}
|
|
106296
|
+
return out;
|
|
106297
|
+
}
|
|
106254
106298
|
function envVarFor(provider) {
|
|
106255
106299
|
return ENV_KEY[provider] ?? catalogProvider(provider)?.envVars[0];
|
|
106256
106300
|
}
|
|
@@ -111932,8 +111976,8 @@ function isIPv4(hostname2) {
|
|
|
111932
111976
|
if (parts.length !== 4)
|
|
111933
111977
|
return false;
|
|
111934
111978
|
return parts.every((part) => {
|
|
111935
|
-
const
|
|
111936
|
-
return Number.isInteger(
|
|
111979
|
+
const num2 = Number(part);
|
|
111980
|
+
return Number.isInteger(num2) && num2 >= 0 && num2 <= 255 && String(num2) === part;
|
|
111937
111981
|
});
|
|
111938
111982
|
}
|
|
111939
111983
|
function isPrivateIPv4(ip) {
|
|
@@ -129271,6 +129315,11 @@ var init_mcp = __esm(() => {
|
|
|
129271
129315
|
init_permission();
|
|
129272
129316
|
});
|
|
129273
129317
|
|
|
129318
|
+
// src/model/family.ts
|
|
129319
|
+
var init_family = __esm(() => {
|
|
129320
|
+
init_providers();
|
|
129321
|
+
});
|
|
129322
|
+
|
|
129274
129323
|
// src/accounts/resolve.ts
|
|
129275
129324
|
async function resolveCreds(account) {
|
|
129276
129325
|
const auth = account.auth;
|
|
@@ -129310,15 +129359,10 @@ async function resolveCreds(account) {
|
|
|
129310
129359
|
}
|
|
129311
129360
|
return {};
|
|
129312
129361
|
}
|
|
129313
|
-
|
|
129314
|
-
class AccountResolver {
|
|
129315
|
-
pick(provider) {
|
|
129316
|
-
return defaultAccount(provider);
|
|
129317
|
-
}
|
|
129318
|
-
}
|
|
129319
129362
|
var init_resolve = __esm(() => {
|
|
129320
129363
|
init_store();
|
|
129321
129364
|
init_catalog();
|
|
129365
|
+
init_family();
|
|
129322
129366
|
});
|
|
129323
129367
|
|
|
129324
129368
|
// src/accounts/detect.ts
|
|
@@ -129537,6 +129581,89 @@ var init_onboarding = __esm(() => {
|
|
|
129537
129581
|
];
|
|
129538
129582
|
});
|
|
129539
129583
|
|
|
129584
|
+
// src/accounts/sniff.ts
|
|
129585
|
+
function sniffCredential(text2) {
|
|
129586
|
+
const t2 = text2.trim();
|
|
129587
|
+
if (/^\s*\{/.test(t2) && /"type"\s*:\s*"service_account"/.test(t2)) {
|
|
129588
|
+
try {
|
|
129589
|
+
const j = JSON.parse(t2);
|
|
129590
|
+
return {
|
|
129591
|
+
kind: "vertex",
|
|
129592
|
+
provider: "vertex",
|
|
129593
|
+
fields: { project: j.project_id ?? "", serviceAccountJson: t2 },
|
|
129594
|
+
missing: j.project_id ? ["location"] : ["project", "location"],
|
|
129595
|
+
confidence: "high"
|
|
129596
|
+
};
|
|
129597
|
+
} catch {
|
|
129598
|
+
return {
|
|
129599
|
+
kind: "vertex",
|
|
129600
|
+
provider: "vertex",
|
|
129601
|
+
fields: { serviceAccountJson: t2 },
|
|
129602
|
+
missing: ["project", "location"],
|
|
129603
|
+
confidence: "low"
|
|
129604
|
+
};
|
|
129605
|
+
}
|
|
129606
|
+
}
|
|
129607
|
+
const azure2 = t2.match(/https?:\/\/([a-z0-9-]+)\.(?:openai\.azure\.com|cognitiveservices\.azure\.com|services\.ai\.azure\.com)/i);
|
|
129608
|
+
if (azure2) {
|
|
129609
|
+
return {
|
|
129610
|
+
kind: "azure",
|
|
129611
|
+
provider: "azure",
|
|
129612
|
+
fields: { resourceName: azure2[1], endpoint: t2 },
|
|
129613
|
+
missing: ["apiKey"],
|
|
129614
|
+
confidence: "high"
|
|
129615
|
+
};
|
|
129616
|
+
}
|
|
129617
|
+
if (/aws_access_key_id\s*=/.test(t2) || AWS_KEY_RE.test(t2) && /aws_secret_access_key|secret/i.test(t2)) {
|
|
129618
|
+
const id = t2.match(AWS_KEY_RE)?.[1] ?? "";
|
|
129619
|
+
const secret = t2.match(/aws_secret_access_key\s*=\s*([A-Za-z0-9/+=]+)/i)?.[1] ?? "";
|
|
129620
|
+
const region = t2.match(/(?:aws_)?region\s*=\s*([a-z0-9-]+)/i)?.[1] ?? "";
|
|
129621
|
+
const missing = [];
|
|
129622
|
+
if (!secret)
|
|
129623
|
+
missing.push("secretAccessKey");
|
|
129624
|
+
if (!region)
|
|
129625
|
+
missing.push("region");
|
|
129626
|
+
return {
|
|
129627
|
+
kind: "aws",
|
|
129628
|
+
provider: "bedrock",
|
|
129629
|
+
fields: { accessKeyId: id, secretAccessKey: secret, region },
|
|
129630
|
+
missing,
|
|
129631
|
+
confidence: "high"
|
|
129632
|
+
};
|
|
129633
|
+
}
|
|
129634
|
+
const awsId = t2.match(/^((?:AKIA|ASIA)[A-Z0-9]{16})$/)?.[1];
|
|
129635
|
+
if (awsId) {
|
|
129636
|
+
return {
|
|
129637
|
+
kind: "aws",
|
|
129638
|
+
provider: "bedrock",
|
|
129639
|
+
fields: { accessKeyId: awsId },
|
|
129640
|
+
missing: ["secretAccessKey", "region"],
|
|
129641
|
+
confidence: "high"
|
|
129642
|
+
};
|
|
129643
|
+
}
|
|
129644
|
+
if (/^vck_/.test(t2)) {
|
|
129645
|
+
return {
|
|
129646
|
+
kind: "openai-compat",
|
|
129647
|
+
provider: "vercel-gateway",
|
|
129648
|
+
fields: { apiKey: t2 },
|
|
129649
|
+
missing: [],
|
|
129650
|
+
confidence: "high"
|
|
129651
|
+
};
|
|
129652
|
+
}
|
|
129653
|
+
const provider = detectProviderByKey(t2);
|
|
129654
|
+
if (provider) {
|
|
129655
|
+
const cat = catalogProvider(provider);
|
|
129656
|
+
const kind = cat?.authKind === "openai-compat" ? "openai-compat" : "api-key";
|
|
129657
|
+
return { kind, provider, fields: { apiKey: t2 }, missing: [], confidence: "high" };
|
|
129658
|
+
}
|
|
129659
|
+
return { kind: "unknown", fields: { apiKey: t2 }, missing: ["provider"], confidence: "low" };
|
|
129660
|
+
}
|
|
129661
|
+
var AWS_KEY_RE;
|
|
129662
|
+
var init_sniff = __esm(() => {
|
|
129663
|
+
init_catalog();
|
|
129664
|
+
AWS_KEY_RE = /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/;
|
|
129665
|
+
});
|
|
129666
|
+
|
|
129540
129667
|
// src/agent/cli-backend.ts
|
|
129541
129668
|
var exports_cli_backend = {};
|
|
129542
129669
|
__export(exports_cli_backend, {
|
|
@@ -129725,13 +129852,22 @@ async function runCliTask(opts) {
|
|
|
129725
129852
|
const { binary, prompt, messages, onEvent, signal } = opts;
|
|
129726
129853
|
const args = buildCliArgs(binary, prompt, { sessionId: opts.sessionId, autoApprove: opts.autoApprove, modelId: opts.modelId, effort: opts.effort });
|
|
129727
129854
|
const state = newState();
|
|
129855
|
+
let failureMessage;
|
|
129856
|
+
const fail = (message) => {
|
|
129857
|
+
if (failureMessage)
|
|
129858
|
+
return;
|
|
129859
|
+
failureMessage = message;
|
|
129860
|
+
if (!opts.deferTerminal)
|
|
129861
|
+
onEvent({ type: "error", message });
|
|
129862
|
+
};
|
|
129728
129863
|
let proc;
|
|
129729
129864
|
try {
|
|
129730
129865
|
proc = spawnProc([binary, ...args], { stdin: "ignore", stdout: "pipe", stderr: "pipe", cwd: opts.cwd ?? process.cwd(), env: subscriptionEnv(binary, opts.profile) });
|
|
129731
129866
|
} catch (e2) {
|
|
129732
|
-
|
|
129733
|
-
|
|
129734
|
-
|
|
129867
|
+
fail(`couldn't start ${binary}: ${e2?.message ?? e2}`);
|
|
129868
|
+
if (!opts.deferTerminal)
|
|
129869
|
+
onEvent({ type: "done", usage: state.usage });
|
|
129870
|
+
return { ...finalize(state), failure: { message: failureMessage } };
|
|
129735
129871
|
}
|
|
129736
129872
|
const onAbort = () => proc.kill();
|
|
129737
129873
|
signal?.addEventListener("abort", onAbort);
|
|
@@ -129775,25 +129911,26 @@ async function runCliTask(opts) {
|
|
|
129775
129911
|
await Promise.all([readStdout(), readStderr(), proc.exited]);
|
|
129776
129912
|
} catch (e2) {
|
|
129777
129913
|
if (!signal?.aborted)
|
|
129778
|
-
|
|
129914
|
+
fail(e2?.message ?? String(e2));
|
|
129779
129915
|
} finally {
|
|
129780
129916
|
signal?.removeEventListener("abort", onAbort);
|
|
129781
129917
|
}
|
|
129782
129918
|
if (!signal?.aborted) {
|
|
129783
129919
|
const err = cleanCliStderr(stderr);
|
|
129784
129920
|
if ((proc.exitCode ?? 0) !== 0) {
|
|
129785
|
-
|
|
129921
|
+
fail(cliFailureMessage(binary, stderr, { accountLabel: opts.accountLabel, reloginCommand: opts.reloginCommand }));
|
|
129786
129922
|
} else if (!state.text && !sawEvent && err) {
|
|
129787
|
-
|
|
129923
|
+
fail(`${binary} produced no JSON output: ${err}`);
|
|
129788
129924
|
} else if (!state.text && !sawEvent) {
|
|
129789
|
-
|
|
129925
|
+
fail(`${binary} finished without an assistant message`);
|
|
129790
129926
|
}
|
|
129791
129927
|
}
|
|
129792
129928
|
const next = [...messages, { role: "user", content: prompt }];
|
|
129793
129929
|
if (state.text)
|
|
129794
129930
|
next.push({ role: "assistant", content: state.text });
|
|
129795
|
-
|
|
129796
|
-
|
|
129931
|
+
if (!opts.deferTerminal)
|
|
129932
|
+
onEvent({ type: "done", usage: state.usage });
|
|
129933
|
+
return { messages: next, usage: state.usage, sessionId: state.sessionId, costUSD: state.costUSD, rates: [...state.rates.values()], failure: failureMessage ? { message: failureMessage } : undefined };
|
|
129797
129934
|
}
|
|
129798
129935
|
var KEYS_TO_STRIP, CONFIG_DIR_VAR;
|
|
129799
129936
|
var init_cli_backend = __esm(() => {
|
|
@@ -129811,10 +129948,12 @@ __export(exports_onboard, {
|
|
|
129811
129948
|
testAccount: () => testAccount,
|
|
129812
129949
|
cliLoginArgs: () => cliLoginArgs,
|
|
129813
129950
|
cliAuthStatus: () => cliAuthStatus,
|
|
129951
|
+
bedrockListUrl: () => bedrockListUrl,
|
|
129814
129952
|
addableProviders: () => addableProviders,
|
|
129815
129953
|
addOpenAICompatAccount: () => addOpenAICompatAccount,
|
|
129816
129954
|
addCliAccount: () => addCliAccount,
|
|
129817
129955
|
addByPastedKey: () => addByPastedKey,
|
|
129956
|
+
addBedrockAccount: () => addBedrockAccount,
|
|
129818
129957
|
addAzureFoundryAccount: () => addAzureFoundryAccount,
|
|
129819
129958
|
addAzureAccount: () => addAzureAccount,
|
|
129820
129959
|
addApiKeyAccount: () => addApiKeyAccount
|
|
@@ -129931,6 +130070,27 @@ async function addAzureAccount(resourceOrEndpoint, key, opts = {}) {
|
|
|
129931
130070
|
putAccount(account);
|
|
129932
130071
|
return { ok: true, account, message: `added ${account.label} (${id})` };
|
|
129933
130072
|
}
|
|
130073
|
+
async function addBedrockAccount(accessKeyId, secretAccessKey, region, opts = {}) {
|
|
130074
|
+
if (!accessKeyId.trim() || !secretAccessKey.trim() || !region.trim()) {
|
|
130075
|
+
return { ok: false, message: "usage: /account add bedrock <access-key-id> <secret-access-key> <region>" };
|
|
130076
|
+
}
|
|
130077
|
+
const id = opts.id ?? `bedrock-${shortId()}`;
|
|
130078
|
+
const accessKeyIdRef = `${id}:aws-access-key-id`;
|
|
130079
|
+
const secretKeyRef = `${id}:aws-secret-access-key`;
|
|
130080
|
+
await setSecret(accessKeyIdRef, accessKeyId.trim());
|
|
130081
|
+
await setSecret(secretKeyRef, secretAccessKey.trim());
|
|
130082
|
+
const account = {
|
|
130083
|
+
id,
|
|
130084
|
+
label: opts.label ?? `Amazon Bedrock (${region})`,
|
|
130085
|
+
provider: "bedrock",
|
|
130086
|
+
exec: "in-loop",
|
|
130087
|
+
auth: { kind: "aws", accessKeyIdRef, secretKeyRef, region: region.trim() },
|
|
130088
|
+
enabled: true,
|
|
130089
|
+
addedAt: Date.now()
|
|
130090
|
+
};
|
|
130091
|
+
putAccount(account);
|
|
130092
|
+
return { ok: true, account, message: `added ${account.label} (${id})` };
|
|
130093
|
+
}
|
|
129934
130094
|
function cliProfileDir(id) {
|
|
129935
130095
|
const home5 = process.env.GEARBOX_HOME || join11(homedir9(), ".gearbox");
|
|
129936
130096
|
return join11(home5, "cli", id);
|
|
@@ -130016,10 +130176,23 @@ function cliLoginArgs(binary) {
|
|
|
130016
130176
|
return binary === "codex" ? ["login"] : ["auth", "login"];
|
|
130017
130177
|
}
|
|
130018
130178
|
async function addByPastedKey(key) {
|
|
130019
|
-
const
|
|
130020
|
-
if (
|
|
130021
|
-
return
|
|
130022
|
-
|
|
130179
|
+
const g = sniffCredential(key);
|
|
130180
|
+
if ((g.kind === "api-key" || g.kind === "openai-compat") && g.provider) {
|
|
130181
|
+
return addApiKeyAccount(g.provider, g.fields.apiKey ?? key);
|
|
130182
|
+
}
|
|
130183
|
+
if (g.kind === "aws" && !g.missing.length) {
|
|
130184
|
+
return addBedrockAccount(g.fields.accessKeyId, g.fields.secretAccessKey, g.fields.region);
|
|
130185
|
+
}
|
|
130186
|
+
return { ok: false, message: guidedMessageFor(g) };
|
|
130187
|
+
}
|
|
130188
|
+
function guidedMessageFor(g) {
|
|
130189
|
+
if (g.kind === "aws")
|
|
130190
|
+
return `looks like AWS/Bedrock — provide all three: /account add bedrock <access-key-id> <secret> <region>`;
|
|
130191
|
+
if (g.kind === "azure")
|
|
130192
|
+
return `looks like Azure (${g.fields.resourceName ?? "resource"}) — add the key: /account add azure ${g.fields.endpoint ?? "<endpoint>"} <api-key>`;
|
|
130193
|
+
if (g.kind === "vertex")
|
|
130194
|
+
return `looks like a Vertex service account — use: /account add vertex (guided), project ${g.fields.project || "<project>"}`;
|
|
130195
|
+
return `couldn't identify that credential — use /account add <provider> <key>, or /onboard for options`;
|
|
130023
130196
|
}
|
|
130024
130197
|
async function testAccount(a) {
|
|
130025
130198
|
const creds = await resolveCreds(a);
|
|
@@ -130055,7 +130228,7 @@ async function testAccount(a) {
|
|
|
130055
130228
|
const keyOk = /^(AKIA|ASIA)[A-Z0-9]{16}$/.test(accessKeyId);
|
|
130056
130229
|
if (!keyOk)
|
|
130057
130230
|
return { ok: false, message: `bedrock: access key ID looks malformed (expected AKIA… or ASIA…, got ${accessKeyId.slice(0, 8)}…)` };
|
|
130058
|
-
return { ok: true, message:
|
|
130231
|
+
return { ok: true, message: `credential fields present — Bedrock at ${bedrockListUrl(creds.aws.region)} (connectivity verified on first use)` };
|
|
130059
130232
|
}
|
|
130060
130233
|
if (a.provider === "vertex" && creds.vertex) {
|
|
130061
130234
|
const { project, location } = creds.vertex;
|
|
@@ -130074,6 +130247,9 @@ async function testAccount(a) {
|
|
|
130074
130247
|
return { ok: false, message: e2?.message ?? "request failed" };
|
|
130075
130248
|
}
|
|
130076
130249
|
}
|
|
130250
|
+
function bedrockListUrl(region) {
|
|
130251
|
+
return `https://bedrock.${region}.amazonaws.com/foundation-models`;
|
|
130252
|
+
}
|
|
130077
130253
|
function addableProviders() {
|
|
130078
130254
|
return CATALOG.filter((p) => p.authKind === "api-key" || p.authKind === "openai-compat").map((p) => ({ id: p.id, label: p.label, group: p.group }));
|
|
130079
130255
|
}
|
|
@@ -130093,6 +130269,7 @@ var init_onboard = __esm(() => {
|
|
|
130093
130269
|
init_store();
|
|
130094
130270
|
init_catalog();
|
|
130095
130271
|
init_onboarding();
|
|
130272
|
+
init_sniff();
|
|
130096
130273
|
init_resolve();
|
|
130097
130274
|
init_cli_backend();
|
|
130098
130275
|
init_proc();
|
|
@@ -138374,9 +138551,23 @@ function recordUsage(opts) {
|
|
|
138374
138551
|
u.turns += 1;
|
|
138375
138552
|
u.estimated = u.estimated || opts.estimated;
|
|
138376
138553
|
u.lastAt = now2;
|
|
138554
|
+
const mk = monthKeyOf(now2);
|
|
138555
|
+
if (u.monthKey !== mk) {
|
|
138556
|
+
u.monthKey = mk;
|
|
138557
|
+
u.monthSpentUSD = 0;
|
|
138558
|
+
}
|
|
138559
|
+
u.monthSpentUSD = (u.monthSpentUSD ?? 0) + opts.costUSD;
|
|
138377
138560
|
f.accounts[opts.accountId] = u;
|
|
138378
138561
|
save(f);
|
|
138379
138562
|
}
|
|
138563
|
+
function monthKeyOf(now2) {
|
|
138564
|
+
return new Date(now2).toISOString().slice(0, 7);
|
|
138565
|
+
}
|
|
138566
|
+
function spentInPeriod(u, period, now2) {
|
|
138567
|
+
if (period === "total")
|
|
138568
|
+
return u.spentUSD;
|
|
138569
|
+
return u.monthKey === monthKeyOf(now2) ? u.monthSpentUSD ?? 0 : 0;
|
|
138570
|
+
}
|
|
138380
138571
|
function recordRateLimits(accountId, rates) {
|
|
138381
138572
|
if (!rates.length)
|
|
138382
138573
|
return;
|
|
@@ -138487,6 +138678,307 @@ function buildUsageView(sessionUSD, resolve, now2 = Date.now(), accountIds = [])
|
|
|
138487
138678
|
};
|
|
138488
138679
|
}
|
|
138489
138680
|
|
|
138681
|
+
// src/commands.ts
|
|
138682
|
+
init_providers();
|
|
138683
|
+
init_catalog();
|
|
138684
|
+
|
|
138685
|
+
// src/ui/fuzzy.ts
|
|
138686
|
+
var BOUNDARY = /[/_\-. ]/;
|
|
138687
|
+
function fuzzyScore(query, target) {
|
|
138688
|
+
const q = query.toLowerCase();
|
|
138689
|
+
const t2 = target.toLowerCase();
|
|
138690
|
+
if (!q)
|
|
138691
|
+
return 0;
|
|
138692
|
+
let qi = 0;
|
|
138693
|
+
let score = 0;
|
|
138694
|
+
let last2 = -1;
|
|
138695
|
+
let run = 0;
|
|
138696
|
+
for (let ti = 0;ti < t2.length && qi < q.length; ti++) {
|
|
138697
|
+
if (t2[ti] !== q[qi])
|
|
138698
|
+
continue;
|
|
138699
|
+
if (last2 >= 0)
|
|
138700
|
+
score += ti - last2 - 1;
|
|
138701
|
+
const prev = ti > 0 ? t2[ti - 1] : "/";
|
|
138702
|
+
if (ti === 0 || BOUNDARY.test(prev))
|
|
138703
|
+
score -= 2;
|
|
138704
|
+
run = last2 === ti - 1 ? run + 1 : 0;
|
|
138705
|
+
score -= run;
|
|
138706
|
+
last2 = ti;
|
|
138707
|
+
qi++;
|
|
138708
|
+
}
|
|
138709
|
+
if (qi < q.length)
|
|
138710
|
+
return null;
|
|
138711
|
+
return score + (t2.length - last2) * 0.1 + t2.length * 0.01;
|
|
138712
|
+
}
|
|
138713
|
+
function fuzzyRank(items, query, key, limit = 8) {
|
|
138714
|
+
if (!query)
|
|
138715
|
+
return items.slice(0, limit);
|
|
138716
|
+
const scored = [];
|
|
138717
|
+
for (const it of items) {
|
|
138718
|
+
const s2 = fuzzyScore(query, key(it));
|
|
138719
|
+
if (s2 != null)
|
|
138720
|
+
scored.push({ item: it, s: s2 });
|
|
138721
|
+
}
|
|
138722
|
+
scored.sort((a, b) => a.s - b.s);
|
|
138723
|
+
return scored.slice(0, limit).map((x2) => x2.item);
|
|
138724
|
+
}
|
|
138725
|
+
|
|
138726
|
+
// src/commands.ts
|
|
138727
|
+
var envHint = (p) => ENV_LABEL[p] ?? catalogProvider(p)?.envVars[0] ?? "an API key";
|
|
138728
|
+
function badgeFor(s2) {
|
|
138729
|
+
switch (s2) {
|
|
138730
|
+
case "ok":
|
|
138731
|
+
return "✓ ready";
|
|
138732
|
+
case "expired":
|
|
138733
|
+
return "⚠ expired";
|
|
138734
|
+
case "invalid":
|
|
138735
|
+
return "✗ invalid";
|
|
138736
|
+
case "rate-limited":
|
|
138737
|
+
return "⏳ limited";
|
|
138738
|
+
case "no-credit":
|
|
138739
|
+
return "✗ no credit";
|
|
138740
|
+
default:
|
|
138741
|
+
return "— unknown";
|
|
138742
|
+
}
|
|
138743
|
+
}
|
|
138744
|
+
var COMMANDS = [
|
|
138745
|
+
{ name: "/model", usage: "/model [name]", desc: "list models · /model <name> pins one · /model auto routes per task", group: "models" },
|
|
138746
|
+
{ name: "/effort", usage: "/effort [level]", desc: "set the active model's reasoning level, e.g. low · high · xhigh · max", group: "models" },
|
|
138747
|
+
{ name: "/prefer", usage: "/prefer kind model", desc: "remember a confirmed model preference for a task type", group: "models" },
|
|
138748
|
+
{ name: "/why", usage: "/why", desc: "show the routing scorecard: every candidate scored, and why this one won", group: "models" },
|
|
138749
|
+
{ name: "/clear", usage: "/clear", desc: "start a fresh conversation", group: "chat" },
|
|
138750
|
+
{ name: "/resume", usage: "/resume [n]", desc: "reopen a past conversation", group: "chat" },
|
|
138751
|
+
{ name: "/retry", usage: "/retry", desc: "send your last message again", group: "chat" },
|
|
138752
|
+
{ name: "/compact", usage: "/compact", desc: "shrink the conversation to free up room", group: "chat" },
|
|
138753
|
+
{ name: "/context", usage: "/context", desc: "see what's loaded and how many tokens it uses", group: "chat" },
|
|
138754
|
+
{ name: "/ask", usage: "/ask <q>", desc: "ask about Gearbox itself — answered from its own docs", group: "chat" },
|
|
138755
|
+
{ name: "/memory", usage: "/memory [note]", desc: "show or add facts to remember (or start a line with #)", group: "chat" },
|
|
138756
|
+
{ name: "/account", usage: "/account", desc: "list accounts; /account <name> switches, /account login <name> re-auths, /account add adds one", group: "accounts" },
|
|
138757
|
+
{ name: "/onboard", usage: "/onboard", desc: "first-run setup; provider list and import/add commands", group: "accounts" },
|
|
138758
|
+
{ name: "/mcp", usage: "/mcp", desc: "list or connect MCP servers: /mcp add <name> <command> [args]", group: "accounts" },
|
|
138759
|
+
{ name: "/cost", usage: "/cost", desc: "see what you've spent per account", group: "accounts" },
|
|
138760
|
+
{ name: "/budget", usage: "/budget <provider> <amount> [monthly|total]", desc: "set a spend budget so routing can estimate remaining credit and preserve it", group: "accounts" },
|
|
138761
|
+
{ name: "/copy", usage: "/copy", desc: "copy the last reply to the clipboard", group: "output" },
|
|
138762
|
+
{ name: "/export", usage: "/export [file]", desc: "save the conversation to a file", group: "output" },
|
|
138763
|
+
{ name: "/plan", usage: "/plan", desc: "plan mode: read-only, no edits (also shift+tab)", group: "modes" },
|
|
138764
|
+
{ name: "/yolo", usage: "/yolo", desc: "run edits and commands without asking", group: "modes" },
|
|
138765
|
+
{ name: "/config", usage: "/config", desc: "view or change saved settings", group: "settings" },
|
|
138766
|
+
{ name: "/init", usage: "/init", desc: "scan this repo and write a GEARBOX.md guide", group: "other" },
|
|
138767
|
+
{ name: "/keys", usage: "/keys", desc: "keyboard shortcuts", group: "other" },
|
|
138768
|
+
{ name: "/help", usage: "/help", desc: "this list", group: "other" },
|
|
138769
|
+
{ name: "/exit", usage: "/exit", desc: "quit gearbox", group: "other" }
|
|
138770
|
+
];
|
|
138771
|
+
var HIDDEN = new Set(["/accounts", "/login", "/vim", "/ghost", "/cwd"]);
|
|
138772
|
+
function matchCommands(draft) {
|
|
138773
|
+
const q = draft.trim().toLowerCase();
|
|
138774
|
+
if (!q.startsWith("/"))
|
|
138775
|
+
return [];
|
|
138776
|
+
const head2 = q.split(/\s+/)[0] ?? q;
|
|
138777
|
+
if (head2 === "/")
|
|
138778
|
+
return COMMANDS;
|
|
138779
|
+
const prefix = COMMANDS.filter((c) => c.name.startsWith(head2));
|
|
138780
|
+
if (prefix.length)
|
|
138781
|
+
return prefix;
|
|
138782
|
+
return fuzzyRank(COMMANDS, head2.slice(1), (c) => c.name.slice(1), 12);
|
|
138783
|
+
}
|
|
138784
|
+
var GROUP_TITLES = [
|
|
138785
|
+
{ id: "models", title: "models & routing" },
|
|
138786
|
+
{ id: "chat", title: "conversation" },
|
|
138787
|
+
{ id: "accounts", title: "accounts & cost" },
|
|
138788
|
+
{ id: "output", title: "save & copy" },
|
|
138789
|
+
{ id: "modes", title: "modes" },
|
|
138790
|
+
{ id: "settings", title: "settings" },
|
|
138791
|
+
{ id: "other", title: "other" }
|
|
138792
|
+
];
|
|
138793
|
+
var ACCOUNT_ADD_HELP = `add an account:
|
|
138794
|
+
` + ` /account add claude Claude subscription (Pro/Max)
|
|
138795
|
+
` + ` /account add claude <name> a 2nd Claude account, e.g. /account add claude work
|
|
138796
|
+
` + ` /account add codex ChatGPT subscription (Plus/Pro)
|
|
138797
|
+
` + ` /account add codex <name> a 2nd ChatGPT account, e.g. /account add codex work
|
|
138798
|
+
` + ` /account add azure <foundry-endpoint> <api-key> Azure AI Foundry (pass the full https:// endpoint)
|
|
138799
|
+
` + ` /account add azure <resource-name> <api-key> [api-version] Azure OpenAI (pass the bare resource name)
|
|
138800
|
+
` + ` /account add bedrock <access-key-id> <secret> <region> Amazon Bedrock
|
|
138801
|
+
` + ` /account add openai-compat <name> <base-url> <api-key> <model> [model...]
|
|
138802
|
+
` + ` /account add <paste> paste any key / AWS block / service-account JSON / endpoint (auto-detected)
|
|
138803
|
+
` + ` /account add <provider> <api-key> e.g. anthropic, openai, openrouter
|
|
138804
|
+
` + "After adding, /account refresh discovers the models the account can actually serve.";
|
|
138805
|
+
function helpText() {
|
|
138806
|
+
const visible = COMMANDS.filter((c) => !HIDDEN.has(c.name));
|
|
138807
|
+
const pad2 = Math.max(...visible.map((c) => c.name.length)) + 2;
|
|
138808
|
+
const out = ["commands · type / to filter, or just say what you want"];
|
|
138809
|
+
for (const g of GROUP_TITLES) {
|
|
138810
|
+
const items = visible.filter((c) => c.group === g.id);
|
|
138811
|
+
if (!items.length)
|
|
138812
|
+
continue;
|
|
138813
|
+
out.push("", g.title);
|
|
138814
|
+
for (const c of items)
|
|
138815
|
+
out.push(` ${c.name.padEnd(pad2)}${c.desc}`);
|
|
138816
|
+
}
|
|
138817
|
+
return out.join(`
|
|
138818
|
+
`);
|
|
138819
|
+
}
|
|
138820
|
+
function accountName(a) {
|
|
138821
|
+
if (a.exec === "cli") {
|
|
138822
|
+
const bin = a.auth?.binary;
|
|
138823
|
+
const base2 = bin === "codex" ? "ChatGPT" : "Claude";
|
|
138824
|
+
const named = a.id.match(/-cli-(.+)$/);
|
|
138825
|
+
return named ? `${base2} (${named[1]})` : base2;
|
|
138826
|
+
}
|
|
138827
|
+
return catalogProvider(a.provider)?.label ?? a.provider;
|
|
138828
|
+
}
|
|
138829
|
+
function accountSlug(a) {
|
|
138830
|
+
if (a.slug)
|
|
138831
|
+
return a.slug;
|
|
138832
|
+
return accountName(a).toLowerCase().replace(/[()]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
138833
|
+
}
|
|
138834
|
+
function accountLabel(a) {
|
|
138835
|
+
return `${accountName(a)} · ${a.exec === "cli" ? "subscription" : "API key"}`;
|
|
138836
|
+
}
|
|
138837
|
+
function formatAccounts(accounts, activeCliId, importable, statuses = {}) {
|
|
138838
|
+
const lines = ["your accounts"];
|
|
138839
|
+
if (!accounts.length) {
|
|
138840
|
+
lines.push(" (none yet)");
|
|
138841
|
+
} else {
|
|
138842
|
+
const active = activeCliId ? accounts.find((a) => a.id === activeCliId) : null;
|
|
138843
|
+
if (active)
|
|
138844
|
+
lines.push(` current: ${accountLabel(active)}`);
|
|
138845
|
+
else
|
|
138846
|
+
lines.push(" current: API routing");
|
|
138847
|
+
lines.push("");
|
|
138848
|
+
accounts.forEach((a, i2) => {
|
|
138849
|
+
const mark = a.id === activeCliId ? glyph.on : " ";
|
|
138850
|
+
const st = statuses[a.id];
|
|
138851
|
+
const status = a.id === activeCliId ? "active" : st?.duplicateOf ? `same login as ${st.duplicateOf}` : st?.signedIn === false ? "not signed in" : st?.signedIn === true ? "signed in" : a.exec === "cli" ? "not checked" : "ready";
|
|
138852
|
+
const alias = accountSlug(a);
|
|
138853
|
+
lines.push(` ${mark} ${accountLabel(a).padEnd(34)} ${status}`);
|
|
138854
|
+
lines.push(` use /account ${alias}`);
|
|
138855
|
+
if (st?.detail && st.signedIn)
|
|
138856
|
+
lines.push(` ${st.detail}`);
|
|
138857
|
+
});
|
|
138858
|
+
if (!activeCliId)
|
|
138859
|
+
lines.push("", " no subscription active — your API keys auto-route per task");
|
|
138860
|
+
}
|
|
138861
|
+
if (importable.length) {
|
|
138862
|
+
lines.push("", "found in your environment — /account import to add:");
|
|
138863
|
+
for (const c of importable)
|
|
138864
|
+
lines.push(` + ${c.label} (${c.envVar})`);
|
|
138865
|
+
}
|
|
138866
|
+
lines.push("", " switch: /account <name>", " add: /account add codex [name] · /account add claude [name] · /account add <api-key>", accounts.length ? " remove: /account remove <name>" : "", accounts.length ? " refresh models: /account refresh" : "");
|
|
138867
|
+
return lines.filter(Boolean).join(`
|
|
138868
|
+
`);
|
|
138869
|
+
}
|
|
138870
|
+
function buildContextView(sections, contextWindow, cwd2 = "") {
|
|
138871
|
+
const total = sections.reduce((s2, x2) => s2 + x2.tokens, 0);
|
|
138872
|
+
const max2 = Math.max(1, ...sections.map((s2) => s2.tokens));
|
|
138873
|
+
const fmt = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
138874
|
+
const rows = sections.map((s2) => ({ label: s2.name, display: fmt(s2.tokens), frac: s2.tokens / max2 }));
|
|
138875
|
+
const labelPad = Math.max("total".length, ...rows.map((r2) => r2.label.length));
|
|
138876
|
+
const valuePad = Math.max(fmt(total).length, ...rows.map((r2) => r2.display.length));
|
|
138877
|
+
return {
|
|
138878
|
+
rows,
|
|
138879
|
+
total: fmt(total),
|
|
138880
|
+
windowPct: contextWindow ? Math.round(total / contextWindow * 100) : undefined,
|
|
138881
|
+
windowLabel: contextWindow ? fmt(contextWindow) : undefined,
|
|
138882
|
+
cwd: cwd2,
|
|
138883
|
+
labelPad,
|
|
138884
|
+
valuePad
|
|
138885
|
+
};
|
|
138886
|
+
}
|
|
138887
|
+
var ENV_LABEL = {
|
|
138888
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
138889
|
+
openai: "OPENAI_API_KEY",
|
|
138890
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
138891
|
+
deepseek: "DEEPSEEK_API_KEY"
|
|
138892
|
+
};
|
|
138893
|
+
var provAbbrev = (src) => src === "measured" ? "meas" : src === "researched" ? "rsch" : "seed";
|
|
138894
|
+
function scorecardRows(card) {
|
|
138895
|
+
const out = [];
|
|
138896
|
+
out.push({ text: `why · ${card.kind} task · quality bar ${card.bar.toFixed(2)}`, tone: "title" });
|
|
138897
|
+
if (card.prompt)
|
|
138898
|
+
out.push({ text: `"${card.prompt.length > 60 ? card.prompt.slice(0, 57) + "…" : card.prompt}"`, tone: "note" });
|
|
138899
|
+
if (!card.entries.length) {
|
|
138900
|
+
out.push({ text: card.note ?? "no candidates", tone: "note" });
|
|
138901
|
+
return out;
|
|
138902
|
+
}
|
|
138903
|
+
const entries = card.entries.slice(0, 8);
|
|
138904
|
+
const labelW = Math.min(20, Math.max(5, ...entries.map((e2) => e2.label.length)));
|
|
138905
|
+
const qcol = (e2) => `${e2.quality.toFixed(2)} ${provAbbrev(e2.qualitySrc)}`;
|
|
138906
|
+
const leftcol = (e2) => e2.headroomText ?? e2.balanceText ?? "—";
|
|
138907
|
+
const lw = Math.max(4, ...entries.map((e2) => leftcol(e2).length));
|
|
138908
|
+
const row = (label, q, cost, left, score, verdict) => `${label.padEnd(labelW).slice(0, labelW)} ${q.padEnd(9)} ${cost.padStart(7)} ${left.padEnd(lw)} ${score.padStart(5)} ${verdict}`;
|
|
138909
|
+
out.push({ text: row("model", "quality", "$/Mtok", "left", "score", "verdict"), tone: "colhead" });
|
|
138910
|
+
for (const e2 of entries) {
|
|
138911
|
+
const score = e2.verdict === "below bar" ? "—" : e2.score.toFixed(2);
|
|
138912
|
+
out.push({
|
|
138913
|
+
text: row(e2.label, qcol(e2), `$${e2.estCostPerMtok.toFixed(2)}`, leftcol(e2), score, e2.verdict + (e2.chosen ? " ◀" : "")),
|
|
138914
|
+
tone: e2.chosen ? "chosen" : e2.verdict === "below bar" ? "dim" : "row"
|
|
138915
|
+
});
|
|
138916
|
+
}
|
|
138917
|
+
if (card.entries.length > entries.length)
|
|
138918
|
+
out.push({ text: `…and ${card.entries.length - entries.length} more`, tone: "note" });
|
|
138919
|
+
return out;
|
|
138920
|
+
}
|
|
138921
|
+
function formatModelList(currentId, showAll = false) {
|
|
138922
|
+
const MODELS2 = modelRegistry();
|
|
138923
|
+
const line = (m2) => ` ${m2.id === currentId ? glyph.on : glyph.off} ${m2.label.padEnd(18)} ${m2.provider}`;
|
|
138924
|
+
const usable = MODELS2.filter((m2) => providerAvailable(m2.provider));
|
|
138925
|
+
const rest2 = MODELS2.filter((m2) => !providerAvailable(m2.provider));
|
|
138926
|
+
const rows = ["models · /model <name> pins one · /model auto routes per task"];
|
|
138927
|
+
if (usable.length) {
|
|
138928
|
+
rows.push("", "ready to use");
|
|
138929
|
+
const CAP = 8;
|
|
138930
|
+
const shown = new Map;
|
|
138931
|
+
let hidden = 0;
|
|
138932
|
+
for (const m2 of usable) {
|
|
138933
|
+
const n = shown.get(m2.provider) ?? 0;
|
|
138934
|
+
if (!showAll && n >= CAP) {
|
|
138935
|
+
hidden++;
|
|
138936
|
+
continue;
|
|
138937
|
+
}
|
|
138938
|
+
shown.set(m2.provider, n + 1);
|
|
138939
|
+
rows.push(line(m2));
|
|
138940
|
+
}
|
|
138941
|
+
if (hidden)
|
|
138942
|
+
rows.push(` + ${hidden} more on your accounts — /model all to list · /model <name> to pick`);
|
|
138943
|
+
} else {
|
|
138944
|
+
rows.push("", "no accounts yet — /account to add one");
|
|
138945
|
+
}
|
|
138946
|
+
if (showAll && rest2.length) {
|
|
138947
|
+
rows.push("", "needs an account");
|
|
138948
|
+
for (const m2 of rest2)
|
|
138949
|
+
rows.push(` ${glyph.off} ${m2.label.padEnd(18)} ${m2.provider}`);
|
|
138950
|
+
} else if (rest2.length) {
|
|
138951
|
+
rows.push("", ` + ${rest2.length} more once you add a key — /model all to list · /account to add one`);
|
|
138952
|
+
}
|
|
138953
|
+
return rows.join(`
|
|
138954
|
+
`);
|
|
138955
|
+
}
|
|
138956
|
+
function resolveModelSwitch(query) {
|
|
138957
|
+
const q = query.trim().toLowerCase();
|
|
138958
|
+
if (!q)
|
|
138959
|
+
return { ok: false, message: "usage: /model <name>" };
|
|
138960
|
+
const MODELS2 = modelRegistry();
|
|
138961
|
+
const matches2 = MODELS2.filter((m3) => m3.label.toLowerCase().includes(q) || m3.id.toLowerCase().includes(q));
|
|
138962
|
+
if (matches2.length === 0)
|
|
138963
|
+
return { ok: false, message: `no model matching “${query}” — /model to list` };
|
|
138964
|
+
const exact = matches2.find((m3) => m3.label.toLowerCase() === q || m3.id.toLowerCase() === q);
|
|
138965
|
+
const available = matches2.filter((m3) => providerAvailable(m3.provider));
|
|
138966
|
+
if (exact) {
|
|
138967
|
+
if (!providerAvailable(exact.provider))
|
|
138968
|
+
return { ok: false, message: `${exact.label}: no ${exact.provider} account yet — /account add ${exact.provider} <key> or set ${envHint(exact.provider)}` };
|
|
138969
|
+
return { ok: true, modelId: exact.id, message: `model → ${exact.label}` };
|
|
138970
|
+
}
|
|
138971
|
+
if (available.length === 0) {
|
|
138972
|
+
const m3 = matches2[0];
|
|
138973
|
+
return { ok: false, message: `“${query}” matches ${m3.label} but no account for ${m3.provider} — /accounts add ${m3.provider} <key> or set ${envHint(m3.provider)}` };
|
|
138974
|
+
}
|
|
138975
|
+
if (available.length > 1) {
|
|
138976
|
+
return { ok: false, message: `“${query}” matches ${available.map((m3) => m3.label).join(", ")} — be more specific` };
|
|
138977
|
+
}
|
|
138978
|
+
const m2 = available[0];
|
|
138979
|
+
return { ok: true, modelId: m2.id, message: `model → ${m2.label}` };
|
|
138980
|
+
}
|
|
138981
|
+
|
|
138490
138982
|
// src/ui/components/Transcript.tsx
|
|
138491
138983
|
var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
|
|
138492
138984
|
var limitColor = (pct) => pct >= 85 ? color.err : pct >= 60 ? color.accent : color.ok;
|
|
@@ -138539,7 +139031,7 @@ function UsageCard({ view }) {
|
|
|
138539
139031
|
color: color.faint,
|
|
138540
139032
|
children: " subscriptions"
|
|
138541
139033
|
}, undefined, false, undefined, this),
|
|
138542
|
-
view.subscriptions.map((a,
|
|
139034
|
+
view.subscriptions.map((a, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
138543
139035
|
children: [
|
|
138544
139036
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138545
139037
|
color: color.text,
|
|
@@ -138577,7 +139069,7 @@ function UsageCard({ view }) {
|
|
|
138577
139069
|
children: " " + (a.limitNote ?? "limits not observed yet")
|
|
138578
139070
|
}, undefined, false, undefined, this)
|
|
138579
139071
|
]
|
|
138580
|
-
},
|
|
139072
|
+
}, i2, true, undefined, this))
|
|
138581
139073
|
]
|
|
138582
139074
|
}, undefined, true, undefined, this) : null,
|
|
138583
139075
|
view.apiKeys.length ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
@@ -138588,7 +139080,7 @@ function UsageCard({ view }) {
|
|
|
138588
139080
|
color: color.faint,
|
|
138589
139081
|
children: " api keys"
|
|
138590
139082
|
}, undefined, false, undefined, this),
|
|
138591
|
-
view.apiKeys.map((a,
|
|
139083
|
+
view.apiKeys.map((a, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
138592
139084
|
children: [
|
|
138593
139085
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138594
139086
|
color: color.text,
|
|
@@ -138611,7 +139103,7 @@ function UsageCard({ view }) {
|
|
|
138611
139103
|
children: " · " + a.balanceNote
|
|
138612
139104
|
}, undefined, false, undefined, this) : null
|
|
138613
139105
|
]
|
|
138614
|
-
},
|
|
139106
|
+
}, i2, true, undefined, this))
|
|
138615
139107
|
]
|
|
138616
139108
|
}, undefined, true, undefined, this) : null,
|
|
138617
139109
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
@@ -138638,32 +139130,32 @@ function UsageCard({ view }) {
|
|
|
138638
139130
|
]
|
|
138639
139131
|
}, undefined, true, undefined, this);
|
|
138640
139132
|
}
|
|
138641
|
-
var accountStateColor = (status) => status === "active" || status === "signed in" || status === "ready" ? color.ok : status === "duplicate" ? color.accent : status === "not signed in" ? color.run : color.faint;
|
|
139133
|
+
var accountStateColor = (status) => status === "active" || status === "signed in" || status === "ready" || status.startsWith("✓") ? color.ok : status === "duplicate" ? color.accent : status === "not signed in" || status.startsWith("✗") ? color.run : status.startsWith("⚠") || status.startsWith("⏳") ? color.accent : color.faint;
|
|
138642
139134
|
function AccountCard({ view }) {
|
|
138643
|
-
const subs = view.rows.filter((
|
|
138644
|
-
const keys2 = view.rows.filter((
|
|
138645
|
-
const commandWidth = Math.max(18, ...view.rows.map((
|
|
138646
|
-
const Row = ({ r }) => {
|
|
138647
|
-
const cmd = `/account ${
|
|
139135
|
+
const subs = view.rows.filter((r2) => r2.type === "subscription");
|
|
139136
|
+
const keys2 = view.rows.filter((r2) => r2.type === "API key");
|
|
139137
|
+
const commandWidth = Math.max(18, ...view.rows.map((r2) => `/account ${r2.alias}`.length));
|
|
139138
|
+
const Row = ({ r: r2 }) => {
|
|
139139
|
+
const cmd = `/account ${r2.alias}`;
|
|
138648
139140
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
138649
139141
|
flexDirection: "column",
|
|
138650
139142
|
children: [
|
|
138651
139143
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138652
139144
|
children: [
|
|
138653
139145
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138654
|
-
color:
|
|
138655
|
-
children:
|
|
139146
|
+
color: r2.active ? color.ok : color.faint,
|
|
139147
|
+
children: r2.active ? " ● " : " "
|
|
138656
139148
|
}, undefined, false, undefined, this),
|
|
138657
139149
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138658
139150
|
color: color.text,
|
|
138659
|
-
bold:
|
|
138660
|
-
children:
|
|
139151
|
+
bold: r2.active,
|
|
139152
|
+
children: r2.name.padEnd(view.labelPad)
|
|
138661
139153
|
}, undefined, false, undefined, this),
|
|
138662
139154
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138663
|
-
color: accountStateColor(
|
|
139155
|
+
color: accountStateColor(r2.status),
|
|
138664
139156
|
children: [
|
|
138665
139157
|
" ",
|
|
138666
|
-
|
|
139158
|
+
r2.status.padEnd(view.statusPad)
|
|
138667
139159
|
]
|
|
138668
139160
|
}, undefined, true, undefined, this),
|
|
138669
139161
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
@@ -138675,28 +139167,21 @@ function AccountCard({ view }) {
|
|
|
138675
139167
|
bold: true,
|
|
138676
139168
|
backgroundColor: color.accentBg,
|
|
138677
139169
|
children: cmd.padEnd(commandWidth)
|
|
138678
|
-
}, undefined, false, undefined, this)
|
|
138679
|
-
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138680
|
-
color: color.faint,
|
|
138681
|
-
children: [
|
|
138682
|
-
" or ",
|
|
138683
|
-
r.number
|
|
138684
|
-
]
|
|
138685
|
-
}, undefined, true, undefined, this)
|
|
139170
|
+
}, undefined, false, undefined, this)
|
|
138686
139171
|
]
|
|
138687
139172
|
}, undefined, true, undefined, this),
|
|
138688
|
-
|
|
139173
|
+
r2.duplicateOf ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138689
139174
|
color: color.faint,
|
|
138690
139175
|
children: [
|
|
138691
139176
|
" same login as ",
|
|
138692
139177
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138693
139178
|
color: color.text,
|
|
138694
|
-
children:
|
|
139179
|
+
children: r2.duplicateOf
|
|
138695
139180
|
}, undefined, false, undefined, this)
|
|
138696
139181
|
]
|
|
138697
|
-
}, undefined, true, undefined, this) :
|
|
139182
|
+
}, undefined, true, undefined, this) : r2.detail ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138698
139183
|
color: color.faint,
|
|
138699
|
-
children: " " +
|
|
139184
|
+
children: " " + r2.detail
|
|
138700
139185
|
}, undefined, false, undefined, this) : null
|
|
138701
139186
|
]
|
|
138702
139187
|
}, undefined, true, undefined, this);
|
|
@@ -138709,9 +139194,9 @@ function AccountCard({ view }) {
|
|
|
138709
139194
|
color: color.faint,
|
|
138710
139195
|
children: " " + title
|
|
138711
139196
|
}, undefined, false, undefined, this),
|
|
138712
|
-
rows.map((
|
|
138713
|
-
r
|
|
138714
|
-
},
|
|
139197
|
+
rows.map((r2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Row, {
|
|
139198
|
+
r: r2
|
|
139199
|
+
}, r2.alias, false, undefined, this))
|
|
138715
139200
|
]
|
|
138716
139201
|
}, undefined, true, undefined, this) : null;
|
|
138717
139202
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
@@ -138821,7 +139306,7 @@ function AccountCard({ view }) {
|
|
|
138821
139306
|
}, undefined, false, undefined, this),
|
|
138822
139307
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138823
139308
|
color: color.accent,
|
|
138824
|
-
children: "/account remove <name
|
|
139309
|
+
children: "/account remove <name>"
|
|
138825
139310
|
}, undefined, false, undefined, this)
|
|
138826
139311
|
]
|
|
138827
139312
|
}, undefined, true, undefined, this)
|
|
@@ -138862,9 +139347,9 @@ function guessLang(text) {
|
|
|
138862
139347
|
function CodeRows({ lines, lang, width, start = 1, rowBg }) {
|
|
138863
139348
|
const lineNoWidth = Math.max(2, String(start + lines.length - 1).length);
|
|
138864
139349
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(jsx_dev_runtime3.Fragment, {
|
|
138865
|
-
children: lines.map((line,
|
|
138866
|
-
const bg = rowBg?.(line,
|
|
138867
|
-
const prefix = `${String(start +
|
|
139350
|
+
children: lines.map((line, i2) => {
|
|
139351
|
+
const bg = rowBg?.(line, i2) ?? color.codeBg;
|
|
139352
|
+
const prefix = `${String(start + i2).padStart(lineNoWidth)} │ `;
|
|
138868
139353
|
const spans = highlightLine(line, lang);
|
|
138869
139354
|
const used = prefix.length + spanLen(spans);
|
|
138870
139355
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
@@ -138874,19 +139359,19 @@ function CodeRows({ lines, lang, width, start = 1, rowBg }) {
|
|
|
138874
139359
|
backgroundColor: bg,
|
|
138875
139360
|
children: prefix
|
|
138876
139361
|
}, undefined, false, undefined, this),
|
|
138877
|
-
spans.map((
|
|
138878
|
-
color:
|
|
138879
|
-
bold:
|
|
138880
|
-
dimColor:
|
|
139362
|
+
spans.map((s2, j) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139363
|
+
color: s2.color,
|
|
139364
|
+
bold: s2.bold,
|
|
139365
|
+
dimColor: s2.dim,
|
|
138881
139366
|
backgroundColor: bg,
|
|
138882
|
-
children:
|
|
139367
|
+
children: s2.text
|
|
138883
139368
|
}, j, false, undefined, this)),
|
|
138884
139369
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138885
139370
|
backgroundColor: bg,
|
|
138886
139371
|
children: pad2(used, width)
|
|
138887
139372
|
}, undefined, false, undefined, this)
|
|
138888
139373
|
]
|
|
138889
|
-
},
|
|
139374
|
+
}, i2, true, undefined, this);
|
|
138890
139375
|
})
|
|
138891
139376
|
}, undefined, false, undefined, this);
|
|
138892
139377
|
}
|
|
@@ -138898,7 +139383,7 @@ function DiffView({ lines, width }) {
|
|
|
138898
139383
|
marginLeft: 5,
|
|
138899
139384
|
marginTop: 1,
|
|
138900
139385
|
children: [
|
|
138901
|
-
shown.map((l,
|
|
139386
|
+
shown.map((l, i2) => {
|
|
138902
139387
|
const bg = l.sign === "+" ? color.diffAddBg : color.diffDelBg;
|
|
138903
139388
|
const sign = l.sign === "+" ? "+ " : "− ";
|
|
138904
139389
|
const spans = highlightLine(l.text);
|
|
@@ -138911,19 +139396,19 @@ function DiffView({ lines, width }) {
|
|
|
138911
139396
|
backgroundColor: bg,
|
|
138912
139397
|
children: sign
|
|
138913
139398
|
}, undefined, false, undefined, this),
|
|
138914
|
-
spans.map((
|
|
138915
|
-
color:
|
|
138916
|
-
bold:
|
|
138917
|
-
dimColor:
|
|
139399
|
+
spans.map((s2, j) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139400
|
+
color: s2.color,
|
|
139401
|
+
bold: s2.bold,
|
|
139402
|
+
dimColor: s2.dim,
|
|
138918
139403
|
backgroundColor: bg,
|
|
138919
|
-
children:
|
|
139404
|
+
children: s2.text
|
|
138920
139405
|
}, j, false, undefined, this)),
|
|
138921
139406
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138922
139407
|
backgroundColor: bg,
|
|
138923
139408
|
children: pad2(used, width)
|
|
138924
139409
|
}, undefined, false, undefined, this)
|
|
138925
139410
|
]
|
|
138926
|
-
},
|
|
139411
|
+
}, i2, true, undefined, this);
|
|
138927
139412
|
}),
|
|
138928
139413
|
extra > 0 ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138929
139414
|
color: color.faint,
|
|
@@ -138936,20 +139421,20 @@ function DiffView({ lines, width }) {
|
|
|
138936
139421
|
]
|
|
138937
139422
|
}, undefined, true, undefined, this);
|
|
138938
139423
|
}
|
|
138939
|
-
var friendlyTool = (
|
|
139424
|
+
var friendlyTool = (name15) => name15 === "AskUserQuestion" ? "question" : name15 === "Write" ? "write" : name15 === "Edit" ? "edit" : name15 === "Read" ? "read" : name15 === "Bash" ? "shell" : name15 === "read_file" ? "read" : name15 === "write_file" ? "write" : name15 === "edit_file" ? "edit" : name15 === "run_shell" ? "shell" : name15 === "command_execution" ? "shell" : name15 === "file_change" ? "write" : name15 === "list_dir" ? "list" : name15 === "glob" ? "glob" : name15 === "search" ? "search" : name15;
|
|
138940
139425
|
var fmtMs = (ms) => ms == null ? "" : ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
138941
139426
|
var frame = () => Math.floor(Date.now() / 360);
|
|
138942
139427
|
var TOOL_SPIN = ["◐", "◓", "◑", "◒"];
|
|
138943
139428
|
var spin = () => TOOL_SPIN[frame() % TOOL_SPIN.length];
|
|
138944
139429
|
var activePhrase = (label) => `${label}${["", ".", "..", "..."][frame() % 4]}`;
|
|
138945
139430
|
var toolColor = (item) => item.name === "AskUserQuestion" ? color.accent : item.status === "err" ? color.err : item.status === "running" ? color.run : item.name === "run_shell" || item.name === "command_execution" ? color.accent : item.name.toLowerCase().includes("write") || item.name.toLowerCase().includes("edit") || item.name === "file_change" ? color.ok : color.accentDim;
|
|
138946
|
-
function previewHighlight(line, lang,
|
|
139431
|
+
function previewHighlight(line, lang, doc2) {
|
|
138947
139432
|
const isPy = /^(py|python)$/i.test(lang ?? "");
|
|
138948
139433
|
const tripleCount = isPy ? (line.match(/("""|''')/g) ?? []).length : 0;
|
|
138949
|
-
if (isPy && (
|
|
139434
|
+
if (isPy && (doc2.open || tripleCount > 0)) {
|
|
138950
139435
|
const spans = [{ text: line, color: color.codeString }];
|
|
138951
139436
|
if (tripleCount % 2 === 1)
|
|
138952
|
-
|
|
139437
|
+
doc2.open = !doc2.open;
|
|
138953
139438
|
return spans;
|
|
138954
139439
|
}
|
|
138955
139440
|
return highlightLine(line, lang);
|
|
@@ -138958,9 +139443,9 @@ function noticeParts(text) {
|
|
|
138958
139443
|
const out = [];
|
|
138959
139444
|
const re = /(\/account\s+\d+|\/[a-z][\w-]*(?:\s+[^\s]+)?|`[^`]+`|\b\d+\.\b|\b(?:Claude|ChatGPT|Anthropic|OpenAI|OpenRouter|subscription|API key|active|current|switch|add|remove|use)\b)/gi;
|
|
138960
139445
|
let last2 = 0;
|
|
138961
|
-
for (const
|
|
138962
|
-
const idx =
|
|
138963
|
-
const token =
|
|
139446
|
+
for (const m2 of text.matchAll(re)) {
|
|
139447
|
+
const idx = m2.index ?? 0;
|
|
139448
|
+
const token = m2[0];
|
|
138964
139449
|
if (idx > last2)
|
|
138965
139450
|
out.push({ text: text.slice(last2, idx), color: color.dim });
|
|
138966
139451
|
const low = token.toLowerCase();
|
|
@@ -138974,12 +139459,12 @@ function noticeParts(text) {
|
|
|
138974
139459
|
}
|
|
138975
139460
|
function NoticeText({ text }) {
|
|
138976
139461
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138977
|
-
children: noticeParts(text).map((
|
|
138978
|
-
color:
|
|
138979
|
-
bold:
|
|
138980
|
-
backgroundColor:
|
|
138981
|
-
children:
|
|
138982
|
-
},
|
|
139462
|
+
children: noticeParts(text).map((s2, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139463
|
+
color: s2.color,
|
|
139464
|
+
bold: s2.bold,
|
|
139465
|
+
backgroundColor: s2.bg,
|
|
139466
|
+
children: s2.text
|
|
139467
|
+
}, i2, false, undefined, this))
|
|
138983
139468
|
}, undefined, false, undefined, this);
|
|
138984
139469
|
}
|
|
138985
139470
|
function UserLine({ text, width }) {
|
|
@@ -138988,8 +139473,8 @@ function UserLine({ text, width }) {
|
|
|
138988
139473
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
138989
139474
|
marginTop: 1,
|
|
138990
139475
|
flexDirection: "column",
|
|
138991
|
-
children: lines.map((line,
|
|
138992
|
-
const prefix =
|
|
139476
|
+
children: lines.map((line, i2) => {
|
|
139477
|
+
const prefix = i2 === 0 ? "▌ " : " ";
|
|
138993
139478
|
const used = prefix.length + line.length;
|
|
138994
139479
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
138995
139480
|
children: [
|
|
@@ -139010,7 +139495,7 @@ function UserLine({ text, width }) {
|
|
|
139010
139495
|
children: " ".repeat(Math.max(0, width - used - 2))
|
|
139011
139496
|
}, undefined, false, undefined, this)
|
|
139012
139497
|
]
|
|
139013
|
-
},
|
|
139498
|
+
}, i2, true, undefined, this);
|
|
139014
139499
|
})
|
|
139015
139500
|
}, undefined, false, undefined, this);
|
|
139016
139501
|
}
|
|
@@ -139028,7 +139513,7 @@ function AssistantLine({ text, width }) {
|
|
|
139028
139513
|
}, undefined, false, undefined, this)
|
|
139029
139514
|
}, undefined, false, undefined, this);
|
|
139030
139515
|
}
|
|
139031
|
-
var spanLen = (spans) => spans.reduce((n,
|
|
139516
|
+
var spanLen = (spans) => spans.reduce((n, s2) => n + s2.text.length, 0);
|
|
139032
139517
|
var pad2 = (used, width) => " ".repeat(Math.max(0, width - used));
|
|
139033
139518
|
function ToolLine({ item, width, expandAll = false }) {
|
|
139034
139519
|
const dotColor = toolColor(item);
|
|
@@ -139138,7 +139623,7 @@ function ToolLine({ item, width, expandAll = false }) {
|
|
|
139138
139623
|
}, undefined, true, undefined, this);
|
|
139139
139624
|
})()
|
|
139140
139625
|
}, undefined, false, undefined, this),
|
|
139141
|
-
previewShown.map((l,
|
|
139626
|
+
previewShown.map((l, i2) => {
|
|
139142
139627
|
const spans = previewHighlight(l, item.previewLang, docState);
|
|
139143
139628
|
const used = 2 + 3 + 2 + spanLen(spans);
|
|
139144
139629
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
@@ -139152,7 +139637,7 @@ function ToolLine({ item, width, expandAll = false }) {
|
|
|
139152
139637
|
color: color.faint,
|
|
139153
139638
|
backgroundColor: color.codeBg,
|
|
139154
139639
|
children: [
|
|
139155
|
-
String(
|
|
139640
|
+
String(i2 + 1).padStart(2),
|
|
139156
139641
|
" "
|
|
139157
139642
|
]
|
|
139158
139643
|
}, undefined, true, undefined, this),
|
|
@@ -139161,18 +139646,18 @@ function ToolLine({ item, width, expandAll = false }) {
|
|
|
139161
139646
|
backgroundColor: color.codeBg,
|
|
139162
139647
|
children: "│ "
|
|
139163
139648
|
}, undefined, false, undefined, this),
|
|
139164
|
-
spans.map((
|
|
139165
|
-
color:
|
|
139166
|
-
bold:
|
|
139649
|
+
spans.map((s2, j) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139650
|
+
color: s2.color,
|
|
139651
|
+
bold: s2.bold,
|
|
139167
139652
|
backgroundColor: color.codeBg,
|
|
139168
|
-
children:
|
|
139653
|
+
children: s2.text
|
|
139169
139654
|
}, j, false, undefined, this)),
|
|
139170
139655
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139171
139656
|
backgroundColor: color.codeBg,
|
|
139172
139657
|
children: pad2(used, codeWidth)
|
|
139173
139658
|
}, undefined, false, undefined, this)
|
|
139174
139659
|
]
|
|
139175
|
-
},
|
|
139660
|
+
}, i2, true, undefined, this);
|
|
139176
139661
|
}),
|
|
139177
139662
|
(item.previewLines ?? 0) > previewShown.length ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139178
139663
|
children: [
|
|
@@ -139256,10 +139741,10 @@ function ToolLine({ item, width, expandAll = false }) {
|
|
|
139256
139741
|
color: color.faint,
|
|
139257
139742
|
children: `… ${outLines} lines · ⌃O to expand`
|
|
139258
139743
|
}, undefined, false, undefined, this) : null,
|
|
139259
|
-
shown.map((l,
|
|
139744
|
+
shown.map((l, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139260
139745
|
color: color.dim,
|
|
139261
139746
|
children: `│ ${l}`
|
|
139262
|
-
},
|
|
139747
|
+
}, i2, false, undefined, this))
|
|
139263
139748
|
]
|
|
139264
139749
|
}, undefined, true, undefined, this);
|
|
139265
139750
|
})()
|
|
@@ -139455,23 +139940,23 @@ function ContextCard({ view }) {
|
|
|
139455
139940
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139456
139941
|
marginTop: 1,
|
|
139457
139942
|
flexDirection: "column",
|
|
139458
|
-
children: view.rows.map((
|
|
139943
|
+
children: view.rows.map((r2, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139459
139944
|
children: [
|
|
139460
139945
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139461
139946
|
color: color.dim,
|
|
139462
|
-
children: " " +
|
|
139947
|
+
children: " " + r2.label.padEnd(view.labelPad)
|
|
139463
139948
|
}, undefined, false, undefined, this),
|
|
139464
139949
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139465
139950
|
color: color.text,
|
|
139466
|
-
children: " " +
|
|
139951
|
+
children: " " + r2.display.padStart(view.valuePad) + " "
|
|
139467
139952
|
}, undefined, false, undefined, this),
|
|
139468
139953
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Bar, {
|
|
139469
|
-
frac:
|
|
139954
|
+
frac: r2.frac,
|
|
139470
139955
|
width: 18,
|
|
139471
139956
|
on: color.accent
|
|
139472
139957
|
}, undefined, false, undefined, this)
|
|
139473
139958
|
]
|
|
139474
|
-
},
|
|
139959
|
+
}, i2, true, undefined, this))
|
|
139475
139960
|
}, undefined, false, undefined, this),
|
|
139476
139961
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139477
139962
|
marginTop: 1,
|
|
@@ -139502,6 +139987,27 @@ function ContextCard({ view }) {
|
|
|
139502
139987
|
]
|
|
139503
139988
|
}, undefined, true, undefined, this);
|
|
139504
139989
|
}
|
|
139990
|
+
function ScorecardCard({ card }) {
|
|
139991
|
+
const toneColor = { title: color.text, colhead: color.faint, chosen: color.accent, row: color.dim, dim: color.faint, note: color.faint };
|
|
139992
|
+
const rows = scorecardRows(card);
|
|
139993
|
+
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139994
|
+
flexDirection: "column",
|
|
139995
|
+
marginTop: 1,
|
|
139996
|
+
marginLeft: 2,
|
|
139997
|
+
children: rows.map((r2, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139998
|
+
children: [
|
|
139999
|
+
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
140000
|
+
color: color.accentDim,
|
|
140001
|
+
children: i2 === 0 ? glyph.notice + " " : " "
|
|
140002
|
+
}, undefined, false, undefined, this),
|
|
140003
|
+
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
140004
|
+
color: toneColor[r2.tone] ?? color.text,
|
|
140005
|
+
children: r2.text
|
|
140006
|
+
}, undefined, false, undefined, this)
|
|
140007
|
+
]
|
|
140008
|
+
}, i2, true, undefined, this))
|
|
140009
|
+
}, undefined, false, undefined, this);
|
|
140010
|
+
}
|
|
139505
140011
|
function Row({ item, width, expandAll = false }) {
|
|
139506
140012
|
switch (item.kind) {
|
|
139507
140013
|
case "user":
|
|
@@ -139552,23 +140058,27 @@ function Row({ item, width, expandAll = false }) {
|
|
|
139552
140058
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(ContextCard, {
|
|
139553
140059
|
view: item.view
|
|
139554
140060
|
}, undefined, false, undefined, this);
|
|
140061
|
+
case "scorecard":
|
|
140062
|
+
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(ScorecardCard, {
|
|
140063
|
+
card: item.card
|
|
140064
|
+
}, undefined, false, undefined, this);
|
|
139555
140065
|
case "notice":
|
|
139556
140066
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139557
140067
|
marginTop: 1,
|
|
139558
140068
|
marginLeft: 2,
|
|
139559
140069
|
flexDirection: "column",
|
|
139560
140070
|
children: item.text.split(`
|
|
139561
|
-
`).map((line,
|
|
140071
|
+
`).map((line, i2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139562
140072
|
children: [
|
|
139563
140073
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Text, {
|
|
139564
140074
|
color: color.accentDim,
|
|
139565
|
-
children:
|
|
140075
|
+
children: i2 === 0 ? glyph.notice + " " : " "
|
|
139566
140076
|
}, undefined, false, undefined, this),
|
|
139567
140077
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(NoticeText, {
|
|
139568
140078
|
text: line
|
|
139569
140079
|
}, undefined, false, undefined, this)
|
|
139570
140080
|
]
|
|
139571
|
-
},
|
|
140081
|
+
}, i2, true, undefined, this))
|
|
139572
140082
|
}, undefined, false, undefined, this);
|
|
139573
140083
|
case "error":
|
|
139574
140084
|
return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
@@ -139618,9 +140128,9 @@ function Transcript({ items, width = 80, header, expandAll = false }) {
|
|
|
139618
140128
|
}, undefined, true, undefined, this);
|
|
139619
140129
|
}
|
|
139620
140130
|
let cut = items.length;
|
|
139621
|
-
for (let
|
|
139622
|
-
if (!isFinal(items[
|
|
139623
|
-
cut =
|
|
140131
|
+
for (let i2 = 0;i2 < items.length; i2++) {
|
|
140132
|
+
if (!isFinal(items[i2])) {
|
|
140133
|
+
cut = i2;
|
|
139624
140134
|
break;
|
|
139625
140135
|
}
|
|
139626
140136
|
}
|
|
@@ -139635,14 +140145,14 @@ function Transcript({ items, width = 80, header, expandAll = false }) {
|
|
|
139635
140145
|
children: [
|
|
139636
140146
|
/* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Static, {
|
|
139637
140147
|
items: entries,
|
|
139638
|
-
children: (
|
|
139639
|
-
paddingX:
|
|
139640
|
-
children:
|
|
139641
|
-
item:
|
|
140148
|
+
children: (e2) => /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
140149
|
+
paddingX: e2.kind === "item" ? 1 : 0,
|
|
140150
|
+
children: e2.kind === "header" ? header : /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Row, {
|
|
140151
|
+
item: e2.item,
|
|
139642
140152
|
width,
|
|
139643
140153
|
expandAll
|
|
139644
140154
|
}, undefined, false, undefined, this)
|
|
139645
|
-
},
|
|
140155
|
+
}, e2.key, false, undefined, this)
|
|
139646
140156
|
}, undefined, false, undefined, this),
|
|
139647
140157
|
live.length ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
|
|
139648
140158
|
flexDirection: "column",
|
|
@@ -139668,10 +140178,10 @@ var SEP2 = ` ${glyph.bullet} `;
|
|
|
139668
140178
|
function statusBarLayout({
|
|
139669
140179
|
model,
|
|
139670
140180
|
effort,
|
|
139671
|
-
mode = "normal"
|
|
140181
|
+
mode: mode2 = "normal"
|
|
139672
140182
|
}) {
|
|
139673
|
-
const modeLabel =
|
|
139674
|
-
const modelStart = 1 + (
|
|
140183
|
+
const modeLabel = mode2 === "auto-accept" ? "auto-accept" : mode2;
|
|
140184
|
+
const modelStart = 1 + (mode2 !== "normal" ? modeLabel.length + SEP2.length : 0);
|
|
139675
140185
|
const modelZone = [modelStart, modelStart + model.length];
|
|
139676
140186
|
if (!effort)
|
|
139677
140187
|
return { modelZone, effortZone: null };
|
|
@@ -139700,13 +140210,13 @@ function StatusBar({
|
|
|
139700
140210
|
tokens,
|
|
139701
140211
|
cost = 0,
|
|
139702
140212
|
width,
|
|
139703
|
-
mode = "normal",
|
|
140213
|
+
mode: mode2 = "normal",
|
|
139704
140214
|
effort,
|
|
139705
140215
|
subscription = null,
|
|
139706
140216
|
online = true
|
|
139707
140217
|
}) {
|
|
139708
140218
|
const sep = SEP2;
|
|
139709
|
-
const modeLabel =
|
|
140219
|
+
const modeLabel = mode2 === "auto-accept" ? "auto-accept" : mode2;
|
|
139710
140220
|
const left = [
|
|
139711
140221
|
model,
|
|
139712
140222
|
effort ? `effort ${effort}` : null,
|
|
@@ -139725,7 +140235,7 @@ function StatusBar({
|
|
|
139725
140235
|
color: color.dim,
|
|
139726
140236
|
wrap: "truncate-end",
|
|
139727
140237
|
children: [
|
|
139728
|
-
|
|
140238
|
+
mode2 !== "normal" ? /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
139729
140239
|
color: color.accent,
|
|
139730
140240
|
children: [
|
|
139731
140241
|
modeLabel,
|
|
@@ -139736,13 +140246,13 @@ function StatusBar({
|
|
|
139736
140246
|
color: color.dim,
|
|
139737
140247
|
children: left[0]
|
|
139738
140248
|
}, undefined, false, undefined, this) : null,
|
|
139739
|
-
left.slice(1).map((
|
|
139740
|
-
color:
|
|
140249
|
+
left.slice(1).map((x2) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
140250
|
+
color: x2.includes("tok") ? color.accentDim : color.faint,
|
|
139741
140251
|
children: [
|
|
139742
140252
|
sep,
|
|
139743
|
-
|
|
140253
|
+
x2
|
|
139744
140254
|
]
|
|
139745
|
-
},
|
|
140255
|
+
}, x2, true, undefined, this)),
|
|
139746
140256
|
ctxPct != null && ctxPct > 0 ? /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
139747
140257
|
color: ctxColor,
|
|
139748
140258
|
children: [
|
|
@@ -139802,258 +140312,6 @@ function StatusBar({
|
|
|
139802
140312
|
}, undefined, true, undefined, this);
|
|
139803
140313
|
}
|
|
139804
140314
|
|
|
139805
|
-
// src/commands.ts
|
|
139806
|
-
init_providers();
|
|
139807
|
-
init_catalog();
|
|
139808
|
-
|
|
139809
|
-
// src/ui/fuzzy.ts
|
|
139810
|
-
var BOUNDARY = /[/_\-. ]/;
|
|
139811
|
-
function fuzzyScore(query, target) {
|
|
139812
|
-
const q = query.toLowerCase();
|
|
139813
|
-
const t2 = target.toLowerCase();
|
|
139814
|
-
if (!q)
|
|
139815
|
-
return 0;
|
|
139816
|
-
let qi = 0;
|
|
139817
|
-
let score = 0;
|
|
139818
|
-
let last2 = -1;
|
|
139819
|
-
let run = 0;
|
|
139820
|
-
for (let ti = 0;ti < t2.length && qi < q.length; ti++) {
|
|
139821
|
-
if (t2[ti] !== q[qi])
|
|
139822
|
-
continue;
|
|
139823
|
-
if (last2 >= 0)
|
|
139824
|
-
score += ti - last2 - 1;
|
|
139825
|
-
const prev = ti > 0 ? t2[ti - 1] : "/";
|
|
139826
|
-
if (ti === 0 || BOUNDARY.test(prev))
|
|
139827
|
-
score -= 2;
|
|
139828
|
-
run = last2 === ti - 1 ? run + 1 : 0;
|
|
139829
|
-
score -= run;
|
|
139830
|
-
last2 = ti;
|
|
139831
|
-
qi++;
|
|
139832
|
-
}
|
|
139833
|
-
if (qi < q.length)
|
|
139834
|
-
return null;
|
|
139835
|
-
return score + (t2.length - last2) * 0.1 + t2.length * 0.01;
|
|
139836
|
-
}
|
|
139837
|
-
function fuzzyRank(items, query, key, limit = 8) {
|
|
139838
|
-
if (!query)
|
|
139839
|
-
return items.slice(0, limit);
|
|
139840
|
-
const scored = [];
|
|
139841
|
-
for (const it of items) {
|
|
139842
|
-
const s2 = fuzzyScore(query, key(it));
|
|
139843
|
-
if (s2 != null)
|
|
139844
|
-
scored.push({ item: it, s: s2 });
|
|
139845
|
-
}
|
|
139846
|
-
scored.sort((a, b) => a.s - b.s);
|
|
139847
|
-
return scored.slice(0, limit).map((x2) => x2.item);
|
|
139848
|
-
}
|
|
139849
|
-
|
|
139850
|
-
// src/commands.ts
|
|
139851
|
-
var envHint = (p) => ENV_LABEL[p] ?? catalogProvider(p)?.envVars[0] ?? "an API key";
|
|
139852
|
-
var COMMANDS = [
|
|
139853
|
-
{ name: "/model", usage: "/model [name]", desc: "list models · /model <name> pins one · /model auto routes per task", group: "models" },
|
|
139854
|
-
{ name: "/effort", usage: "/effort [level]", desc: "set the active model's reasoning level, e.g. low · high · xhigh · max", group: "models" },
|
|
139855
|
-
{ name: "/prefer", usage: "/prefer kind model", desc: "remember a confirmed model preference for a task type", group: "models" },
|
|
139856
|
-
{ name: "/clear", usage: "/clear", desc: "start a fresh conversation", group: "chat" },
|
|
139857
|
-
{ name: "/resume", usage: "/resume [n]", desc: "reopen a past conversation", group: "chat" },
|
|
139858
|
-
{ name: "/retry", usage: "/retry", desc: "send your last message again", group: "chat" },
|
|
139859
|
-
{ name: "/compact", usage: "/compact", desc: "shrink the conversation to free up room", group: "chat" },
|
|
139860
|
-
{ name: "/context", usage: "/context", desc: "see what's loaded and how many tokens it uses", group: "chat" },
|
|
139861
|
-
{ name: "/ask", usage: "/ask <q>", desc: "ask about Gearbox itself — answered from its own docs", group: "chat" },
|
|
139862
|
-
{ name: "/memory", usage: "/memory [note]", desc: "show or add facts to remember (or start a line with #)", group: "chat" },
|
|
139863
|
-
{ name: "/account", usage: "/account", desc: "list accounts; /account <number> to switch, /account add to add one", group: "accounts" },
|
|
139864
|
-
{ name: "/onboard", usage: "/onboard", desc: "first-run setup; provider list and import/add commands", group: "accounts" },
|
|
139865
|
-
{ name: "/mcp", usage: "/mcp", desc: "list or connect MCP servers: /mcp add <name> <command> [args]", group: "accounts" },
|
|
139866
|
-
{ name: "/cost", usage: "/cost", desc: "see what you've spent per account", group: "accounts" },
|
|
139867
|
-
{ name: "/copy", usage: "/copy", desc: "copy the last reply to the clipboard", group: "output" },
|
|
139868
|
-
{ name: "/export", usage: "/export [file]", desc: "save the conversation to a file", group: "output" },
|
|
139869
|
-
{ name: "/plan", usage: "/plan", desc: "plan mode: read-only, no edits (also shift+tab)", group: "modes" },
|
|
139870
|
-
{ name: "/yolo", usage: "/yolo", desc: "run edits and commands without asking", group: "modes" },
|
|
139871
|
-
{ name: "/config", usage: "/config", desc: "view or change saved settings", group: "settings" },
|
|
139872
|
-
{ name: "/init", usage: "/init", desc: "scan this repo and write a GEARBOX.md guide", group: "other" },
|
|
139873
|
-
{ name: "/keys", usage: "/keys", desc: "keyboard shortcuts", group: "other" },
|
|
139874
|
-
{ name: "/help", usage: "/help", desc: "this list", group: "other" },
|
|
139875
|
-
{ name: "/exit", usage: "/exit", desc: "quit gearbox", group: "other" }
|
|
139876
|
-
];
|
|
139877
|
-
var HIDDEN = new Set(["/accounts", "/login", "/vim", "/ghost", "/cwd"]);
|
|
139878
|
-
function matchCommands(draft) {
|
|
139879
|
-
const q = draft.trim().toLowerCase();
|
|
139880
|
-
if (!q.startsWith("/"))
|
|
139881
|
-
return [];
|
|
139882
|
-
const head2 = q.split(/\s+/)[0] ?? q;
|
|
139883
|
-
if (head2 === "/")
|
|
139884
|
-
return COMMANDS;
|
|
139885
|
-
const prefix = COMMANDS.filter((c) => c.name.startsWith(head2));
|
|
139886
|
-
if (prefix.length)
|
|
139887
|
-
return prefix;
|
|
139888
|
-
return fuzzyRank(COMMANDS, head2.slice(1), (c) => c.name.slice(1), 12);
|
|
139889
|
-
}
|
|
139890
|
-
var GROUP_TITLES = [
|
|
139891
|
-
{ id: "models", title: "models & routing" },
|
|
139892
|
-
{ id: "chat", title: "conversation" },
|
|
139893
|
-
{ id: "accounts", title: "accounts & cost" },
|
|
139894
|
-
{ id: "output", title: "save & copy" },
|
|
139895
|
-
{ id: "modes", title: "modes" },
|
|
139896
|
-
{ id: "settings", title: "settings" },
|
|
139897
|
-
{ id: "other", title: "other" }
|
|
139898
|
-
];
|
|
139899
|
-
var ACCOUNT_ADD_HELP = `add an account:
|
|
139900
|
-
` + ` /account add claude Claude subscription (Pro/Max)
|
|
139901
|
-
` + ` /account add claude <name> a 2nd Claude account, e.g. /account add claude work
|
|
139902
|
-
` + ` /account add codex ChatGPT subscription (Plus/Pro)
|
|
139903
|
-
` + ` /account add codex <name> a 2nd ChatGPT account, e.g. /account add codex work
|
|
139904
|
-
` + ` /account add azure <foundry-endpoint> <api-key> Azure AI Foundry (pass the full https:// endpoint)
|
|
139905
|
-
` + ` /account add azure <resource-name> <api-key> [api-version] Azure OpenAI (pass the bare resource name)
|
|
139906
|
-
` + ` /account add openai-compat <name> <base-url> <api-key> <model> [model...]
|
|
139907
|
-
` + ` /account add <api-key> paste any provider key (auto-detected)
|
|
139908
|
-
` + ` /account add <provider> <api-key> e.g. anthropic, openai, openrouter
|
|
139909
|
-
` + "After adding, /account refresh discovers the models the account can actually serve.";
|
|
139910
|
-
function helpText() {
|
|
139911
|
-
const visible = COMMANDS.filter((c) => !HIDDEN.has(c.name));
|
|
139912
|
-
const pad3 = Math.max(...visible.map((c) => c.name.length)) + 2;
|
|
139913
|
-
const out = ["commands · type / to filter, or just say what you want"];
|
|
139914
|
-
for (const g of GROUP_TITLES) {
|
|
139915
|
-
const items = visible.filter((c) => c.group === g.id);
|
|
139916
|
-
if (!items.length)
|
|
139917
|
-
continue;
|
|
139918
|
-
out.push("", g.title);
|
|
139919
|
-
for (const c of items)
|
|
139920
|
-
out.push(` ${c.name.padEnd(pad3)}${c.desc}`);
|
|
139921
|
-
}
|
|
139922
|
-
return out.join(`
|
|
139923
|
-
`);
|
|
139924
|
-
}
|
|
139925
|
-
function accountName(a) {
|
|
139926
|
-
if (a.exec === "cli") {
|
|
139927
|
-
const bin = a.auth?.binary;
|
|
139928
|
-
const base2 = bin === "codex" ? "ChatGPT" : "Claude";
|
|
139929
|
-
const named = a.id.match(/-cli-(.+)$/);
|
|
139930
|
-
return named ? `${base2} (${named[1]})` : base2;
|
|
139931
|
-
}
|
|
139932
|
-
return catalogProvider(a.provider)?.label ?? a.provider;
|
|
139933
|
-
}
|
|
139934
|
-
function accountSlug(a) {
|
|
139935
|
-
return accountName(a).toLowerCase().replace(/[()]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
139936
|
-
}
|
|
139937
|
-
function accountLabel(a) {
|
|
139938
|
-
return `${accountName(a)} · ${a.exec === "cli" ? "subscription" : "API key"}`;
|
|
139939
|
-
}
|
|
139940
|
-
function formatAccounts(accounts, activeCliId, importable, statuses = {}) {
|
|
139941
|
-
const lines = ["your accounts"];
|
|
139942
|
-
if (!accounts.length) {
|
|
139943
|
-
lines.push(" (none yet)");
|
|
139944
|
-
} else {
|
|
139945
|
-
const active = activeCliId ? accounts.find((a) => a.id === activeCliId) : null;
|
|
139946
|
-
if (active)
|
|
139947
|
-
lines.push(` current: ${accountLabel(active)}`);
|
|
139948
|
-
else
|
|
139949
|
-
lines.push(" current: API routing");
|
|
139950
|
-
lines.push("");
|
|
139951
|
-
accounts.forEach((a, i2) => {
|
|
139952
|
-
const mark = a.id === activeCliId ? glyph.on : " ";
|
|
139953
|
-
const st = statuses[a.id];
|
|
139954
|
-
const status = a.id === activeCliId ? "active" : st?.duplicateOf ? `same login as ${st.duplicateOf}` : st?.signedIn === false ? "not signed in" : st?.signedIn === true ? "signed in" : a.exec === "cli" ? "not checked" : "ready";
|
|
139955
|
-
const alias = accountSlug(a);
|
|
139956
|
-
lines.push(` ${mark} ${accountLabel(a).padEnd(34)} ${status}`);
|
|
139957
|
-
lines.push(` use /account ${alias}${i2 + 1 ? ` (or ${i2 + 1})` : ""}`);
|
|
139958
|
-
if (st?.detail && st.signedIn)
|
|
139959
|
-
lines.push(` ${st.detail}`);
|
|
139960
|
-
});
|
|
139961
|
-
if (!activeCliId)
|
|
139962
|
-
lines.push("", " no subscription active — your API keys auto-route per task");
|
|
139963
|
-
}
|
|
139964
|
-
if (importable.length) {
|
|
139965
|
-
lines.push("", "found in your environment — /account import to add:");
|
|
139966
|
-
for (const c of importable)
|
|
139967
|
-
lines.push(` + ${c.label} (${c.envVar})`);
|
|
139968
|
-
}
|
|
139969
|
-
lines.push("", " switch: /account <name-or-number>", " add: /account add codex [name] · /account add claude [name] · /account add <api-key>", accounts.length ? " remove: /account remove <name-or-number>" : "", accounts.length ? " refresh models: /account refresh" : "");
|
|
139970
|
-
return lines.filter(Boolean).join(`
|
|
139971
|
-
`);
|
|
139972
|
-
}
|
|
139973
|
-
function buildContextView(sections, contextWindow, cwd2 = "") {
|
|
139974
|
-
const total = sections.reduce((s2, x2) => s2 + x2.tokens, 0);
|
|
139975
|
-
const max2 = Math.max(1, ...sections.map((s2) => s2.tokens));
|
|
139976
|
-
const fmt = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
139977
|
-
const rows = sections.map((s2) => ({ label: s2.name, display: fmt(s2.tokens), frac: s2.tokens / max2 }));
|
|
139978
|
-
const labelPad = Math.max("total".length, ...rows.map((r2) => r2.label.length));
|
|
139979
|
-
const valuePad = Math.max(fmt(total).length, ...rows.map((r2) => r2.display.length));
|
|
139980
|
-
return {
|
|
139981
|
-
rows,
|
|
139982
|
-
total: fmt(total),
|
|
139983
|
-
windowPct: contextWindow ? Math.round(total / contextWindow * 100) : undefined,
|
|
139984
|
-
windowLabel: contextWindow ? fmt(contextWindow) : undefined,
|
|
139985
|
-
cwd: cwd2,
|
|
139986
|
-
labelPad,
|
|
139987
|
-
valuePad
|
|
139988
|
-
};
|
|
139989
|
-
}
|
|
139990
|
-
var ENV_LABEL = {
|
|
139991
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
139992
|
-
openai: "OPENAI_API_KEY",
|
|
139993
|
-
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
139994
|
-
deepseek: "DEEPSEEK_API_KEY"
|
|
139995
|
-
};
|
|
139996
|
-
function formatModelList(currentId, showAll = false) {
|
|
139997
|
-
const MODELS2 = modelRegistry();
|
|
139998
|
-
const line = (m2) => ` ${m2.id === currentId ? glyph.on : glyph.off} ${m2.label.padEnd(18)} ${m2.provider}`;
|
|
139999
|
-
const usable = MODELS2.filter((m2) => providerAvailable(m2.provider));
|
|
140000
|
-
const rest2 = MODELS2.filter((m2) => !providerAvailable(m2.provider));
|
|
140001
|
-
const rows = ["models · /model <name> pins one · /model auto routes per task"];
|
|
140002
|
-
if (usable.length) {
|
|
140003
|
-
rows.push("", "ready to use");
|
|
140004
|
-
const CAP = 8;
|
|
140005
|
-
const shown = new Map;
|
|
140006
|
-
let hidden = 0;
|
|
140007
|
-
for (const m2 of usable) {
|
|
140008
|
-
const n = shown.get(m2.provider) ?? 0;
|
|
140009
|
-
if (!showAll && n >= CAP) {
|
|
140010
|
-
hidden++;
|
|
140011
|
-
continue;
|
|
140012
|
-
}
|
|
140013
|
-
shown.set(m2.provider, n + 1);
|
|
140014
|
-
rows.push(line(m2));
|
|
140015
|
-
}
|
|
140016
|
-
if (hidden)
|
|
140017
|
-
rows.push(` + ${hidden} more on your accounts — /model all to list · /model <name> to pick`);
|
|
140018
|
-
} else {
|
|
140019
|
-
rows.push("", "no accounts yet — /account to add one");
|
|
140020
|
-
}
|
|
140021
|
-
if (showAll && rest2.length) {
|
|
140022
|
-
rows.push("", "needs an account");
|
|
140023
|
-
for (const m2 of rest2)
|
|
140024
|
-
rows.push(` ${glyph.off} ${m2.label.padEnd(18)} ${m2.provider}`);
|
|
140025
|
-
} else if (rest2.length) {
|
|
140026
|
-
rows.push("", ` + ${rest2.length} more once you add a key — /model all to list · /account to add one`);
|
|
140027
|
-
}
|
|
140028
|
-
return rows.join(`
|
|
140029
|
-
`);
|
|
140030
|
-
}
|
|
140031
|
-
function resolveModelSwitch(query) {
|
|
140032
|
-
const q = query.trim().toLowerCase();
|
|
140033
|
-
if (!q)
|
|
140034
|
-
return { ok: false, message: "usage: /model <name>" };
|
|
140035
|
-
const MODELS2 = modelRegistry();
|
|
140036
|
-
const matches2 = MODELS2.filter((m3) => m3.label.toLowerCase().includes(q) || m3.id.toLowerCase().includes(q));
|
|
140037
|
-
if (matches2.length === 0)
|
|
140038
|
-
return { ok: false, message: `no model matching “${query}” — /model to list` };
|
|
140039
|
-
const exact = matches2.find((m3) => m3.label.toLowerCase() === q || m3.id.toLowerCase() === q);
|
|
140040
|
-
const available = matches2.filter((m3) => providerAvailable(m3.provider));
|
|
140041
|
-
if (exact) {
|
|
140042
|
-
if (!providerAvailable(exact.provider))
|
|
140043
|
-
return { ok: false, message: `${exact.label}: no ${exact.provider} account yet — /account add ${exact.provider} <key> or set ${envHint(exact.provider)}` };
|
|
140044
|
-
return { ok: true, modelId: exact.id, message: `model → ${exact.label}` };
|
|
140045
|
-
}
|
|
140046
|
-
if (available.length === 0) {
|
|
140047
|
-
const m3 = matches2[0];
|
|
140048
|
-
return { ok: false, message: `“${query}” matches ${m3.label} but no account for ${m3.provider} — /accounts add ${m3.provider} <key> or set ${envHint(m3.provider)}` };
|
|
140049
|
-
}
|
|
140050
|
-
if (available.length > 1) {
|
|
140051
|
-
return { ok: false, message: `“${query}” matches ${available.map((m3) => m3.label).join(", ")} — be more specific` };
|
|
140052
|
-
}
|
|
140053
|
-
const m2 = available[0];
|
|
140054
|
-
return { ok: true, modelId: m2.id, message: `model → ${m2.label}` };
|
|
140055
|
-
}
|
|
140056
|
-
|
|
140057
140315
|
// src/ui/components/CommandPalette.tsx
|
|
140058
140316
|
var jsx_dev_runtime5 = __toESM(require_jsx_dev_runtime(), 1);
|
|
140059
140317
|
function windowed(items, selected, limit) {
|
|
@@ -141873,7 +142131,7 @@ function Viewport({ lines, scrollTop, height, width, selection }) {
|
|
|
141873
142131
|
|
|
141874
142132
|
// src/ui/lines.ts
|
|
141875
142133
|
var limitColor2 = (pct) => pct >= 85 ? color.err : pct >= 60 ? color.accent : color.ok;
|
|
141876
|
-
var accountStateColor2 = (status) => status === "active" || status === "signed in" || status === "ready" ? color.ok : status === "duplicate" ? color.accent : status === "not signed in" ? color.run : color.faint;
|
|
142134
|
+
var accountStateColor2 = (status) => status === "active" || status === "signed in" || status === "ready" || status.startsWith("✓") ? color.ok : status === "duplicate" ? color.accent : status === "not signed in" || status.startsWith("✗") ? color.run : status.startsWith("⚠") || status.startsWith("⏳") ? color.accent : color.faint;
|
|
141877
142135
|
var BLANK = [];
|
|
141878
142136
|
function clipSpans(spans, width) {
|
|
141879
142137
|
const out = [];
|
|
@@ -142376,8 +142634,7 @@ function itemsToLines(items, width, expand = false) {
|
|
|
142376
142634
|
{ text: r2.name.padEnd(v.labelPad), color: color.text, bold: r2.active },
|
|
142377
142635
|
{ text: " " + r2.status.padEnd(v.statusPad), color: accountStateColor2(r2.status) },
|
|
142378
142636
|
{ text: " use ", color: color.faint },
|
|
142379
|
-
{ text: cmd.padEnd(commandWidth), color: color.accent, bold: true, bg: color.accentBg }
|
|
142380
|
-
{ text: " or " + r2.number, color: color.faint }
|
|
142637
|
+
{ text: cmd.padEnd(commandWidth), color: color.accent, bold: true, bg: color.accentBg }
|
|
142381
142638
|
], width));
|
|
142382
142639
|
if (r2.duplicateOf)
|
|
142383
142640
|
out.push(clipSpans([{ text: " same login as ", color: color.faint }, { text: r2.duplicateOf, color: color.text }], width));
|
|
@@ -142395,7 +142652,7 @@ function itemsToLines(items, width, expand = false) {
|
|
|
142395
142652
|
}
|
|
142396
142653
|
out.push(BLANK);
|
|
142397
142654
|
out.push(clipSpans([{ text: " add ", color: color.faint }, { text: "/account add codex [name]", color: color.accent }, { text: " /account add claude [name]", color: color.accent }, { text: " /account add <api-key>", color: color.accent }], width));
|
|
142398
|
-
out.push(clipSpans([{ text: " remove ", color: color.faint }, { text: "/account remove <name
|
|
142655
|
+
out.push(clipSpans([{ text: " remove ", color: color.faint }, { text: "/account remove <name>", color: color.accent }], width));
|
|
142399
142656
|
break;
|
|
142400
142657
|
}
|
|
142401
142658
|
case "usage": {
|
|
@@ -142476,6 +142733,18 @@ function itemsToLines(items, width, expand = false) {
|
|
|
142476
142733
|
out.push(clipSpans([{ text: " working directory: " + v.cwd, color: color.faint }], width));
|
|
142477
142734
|
break;
|
|
142478
142735
|
}
|
|
142736
|
+
case "scorecard": {
|
|
142737
|
+
const toneColor = { title: color.text, colhead: color.faint, chosen: color.accent, row: color.dim, dim: color.faint, note: color.faint };
|
|
142738
|
+
let first = true;
|
|
142739
|
+
for (const r2 of scorecardRows(it.card)) {
|
|
142740
|
+
const prefix = first ? " " + glyph.notice + " " : " ";
|
|
142741
|
+
out.push(clipSpans([{ text: prefix, color: color.accentDim }, { text: r2.text, color: toneColor[r2.tone] ?? color.text }], width));
|
|
142742
|
+
if (first)
|
|
142743
|
+
out.push(BLANK);
|
|
142744
|
+
first = false;
|
|
142745
|
+
}
|
|
142746
|
+
break;
|
|
142747
|
+
}
|
|
142479
142748
|
case "error": {
|
|
142480
142749
|
let first = true;
|
|
142481
142750
|
for (const para of it.text.split(`
|
|
@@ -142657,9 +142926,32 @@ class FixedSelector {
|
|
|
142657
142926
|
}
|
|
142658
142927
|
}
|
|
142659
142928
|
|
|
142929
|
+
// src/model/cooldown.ts
|
|
142930
|
+
function classifyFailure(message) {
|
|
142931
|
+
const m2 = (message || "").toLowerCase();
|
|
142932
|
+
const exhausted = /\b429\b|\b529\b|\b402\b/.test(m2) || /rate.?limit|too many requests|insufficient_quota|quota|over(loaded|capacity)|throttl|resource.?exhausted|usage.?limit|billing|payment required|out of credit|credit balance/.test(m2);
|
|
142933
|
+
return exhausted ? "exhausted" : "other";
|
|
142934
|
+
}
|
|
142935
|
+
var DEFAULT_COOLDOWN_MS = 5 * 60000;
|
|
142936
|
+
var cooldowns = new Map;
|
|
142937
|
+
function markExhausted(key, ms, reason, now2 = Date.now()) {
|
|
142938
|
+
cooldowns.set(key, { until: now2 + Math.max(0, ms), reason });
|
|
142939
|
+
}
|
|
142940
|
+
function coolingDown(key, now2 = Date.now()) {
|
|
142941
|
+
const c = cooldowns.get(key);
|
|
142942
|
+
if (!c)
|
|
142943
|
+
return false;
|
|
142944
|
+
if (c.until <= now2) {
|
|
142945
|
+
cooldowns.delete(key);
|
|
142946
|
+
return false;
|
|
142947
|
+
}
|
|
142948
|
+
return true;
|
|
142949
|
+
}
|
|
142950
|
+
|
|
142660
142951
|
// src/model/router.ts
|
|
142661
142952
|
init_providers();
|
|
142662
142953
|
init_profiles();
|
|
142954
|
+
init_store();
|
|
142663
142955
|
|
|
142664
142956
|
// src/model/preferences.ts
|
|
142665
142957
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "node:fs";
|
|
@@ -142674,10 +142966,26 @@ function loadRoutingPreferences() {
|
|
|
142674
142966
|
try {
|
|
142675
142967
|
const f3 = JSON.parse(readFileSync6(file4(), "utf8"));
|
|
142676
142968
|
if (f3?.byKind)
|
|
142677
|
-
return { version: 1, byKind: f3.byKind };
|
|
142969
|
+
return { version: 1, byKind: f3.byKind, global: f3.global, budgets: f3.budgets };
|
|
142678
142970
|
} catch {}
|
|
142679
142971
|
return empty();
|
|
142680
142972
|
}
|
|
142973
|
+
function loadBudgets() {
|
|
142974
|
+
return loadRoutingPreferences().budgets ?? {};
|
|
142975
|
+
}
|
|
142976
|
+
function setBudget(key, budget) {
|
|
142977
|
+
const prefs = loadRoutingPreferences();
|
|
142978
|
+
const budgets = { ...prefs.budgets ?? {} };
|
|
142979
|
+
if (budget)
|
|
142980
|
+
budgets[key] = budget;
|
|
142981
|
+
else
|
|
142982
|
+
delete budgets[key];
|
|
142983
|
+
prefs.budgets = budgets;
|
|
142984
|
+
save2(prefs);
|
|
142985
|
+
}
|
|
142986
|
+
function globalPreference() {
|
|
142987
|
+
return loadRoutingPreferences().global;
|
|
142988
|
+
}
|
|
142681
142989
|
function save2(prefs) {
|
|
142682
142990
|
try {
|
|
142683
142991
|
mkdirSync5(dirname2(file4()), { recursive: true });
|
|
@@ -142703,6 +143011,119 @@ function confirmRoutingPreference(pref) {
|
|
|
142703
143011
|
|
|
142704
143012
|
// src/model/router.ts
|
|
142705
143013
|
init_capabilities();
|
|
143014
|
+
|
|
143015
|
+
// src/model/routing-context.ts
|
|
143016
|
+
init_store();
|
|
143017
|
+
var clamp01 = (n) => Math.max(0, Math.min(1, n));
|
|
143018
|
+
function headroomOf(u) {
|
|
143019
|
+
const snaps = u?.rates ?? (u?.rate ? [u.rate] : []);
|
|
143020
|
+
let rateHeadroom;
|
|
143021
|
+
let bindingWindow;
|
|
143022
|
+
let apiThrottle;
|
|
143023
|
+
for (const r2 of snaps) {
|
|
143024
|
+
const h2 = 1 - clamp01(r2.utilization);
|
|
143025
|
+
if (r2.type?.startsWith("api:")) {
|
|
143026
|
+
if (apiThrottle === undefined || h2 < apiThrottle)
|
|
143027
|
+
apiThrottle = h2;
|
|
143028
|
+
continue;
|
|
143029
|
+
}
|
|
143030
|
+
if (rateHeadroom === undefined || h2 < rateHeadroom) {
|
|
143031
|
+
rateHeadroom = h2;
|
|
143032
|
+
bindingWindow = { type: r2.type, utilization: r2.utilization, resetsAt: r2.resetsAt };
|
|
143033
|
+
}
|
|
143034
|
+
}
|
|
143035
|
+
return { rateHeadroom, bindingWindow, rateAt: snaps[0]?.at, apiThrottle };
|
|
143036
|
+
}
|
|
143037
|
+
function buildRoutingContext(now2, opts) {
|
|
143038
|
+
const accounts = opts?.accounts ?? listAccounts();
|
|
143039
|
+
const usageById = new Map((opts?.usage ?? loadUsage()).map((u) => [u.accountId, u]));
|
|
143040
|
+
const budgets = opts?.budgets ?? loadBudgets();
|
|
143041
|
+
const byAccountId = new Map;
|
|
143042
|
+
for (const acct of accounts) {
|
|
143043
|
+
if (!acct.enabled)
|
|
143044
|
+
continue;
|
|
143045
|
+
const u = usageById.get(acct.id);
|
|
143046
|
+
byAccountId.set(acct.id, {
|
|
143047
|
+
accountId: acct.id,
|
|
143048
|
+
provider: acct.provider,
|
|
143049
|
+
exec: acct.exec,
|
|
143050
|
+
isSubscription: acct.exec === "cli",
|
|
143051
|
+
...balanceOf(acct, u, budgets, now2),
|
|
143052
|
+
...headroomOf(u)
|
|
143053
|
+
});
|
|
143054
|
+
}
|
|
143055
|
+
return { now: now2, byAccountId };
|
|
143056
|
+
}
|
|
143057
|
+
function balanceOf(acct, u, budgets, now2) {
|
|
143058
|
+
if (acct.exec === "cli")
|
|
143059
|
+
return {};
|
|
143060
|
+
if (u?.balance?.remainingUSD !== undefined) {
|
|
143061
|
+
return { balanceRemainingUSD: u.balance.remainingUSD, balanceTotalUSD: u.balance.totalUSD, balanceAt: u.balance.at };
|
|
143062
|
+
}
|
|
143063
|
+
const budget = budgets[acct.id] ?? budgets[acct.provider];
|
|
143064
|
+
if (!budget)
|
|
143065
|
+
return {};
|
|
143066
|
+
const spent = u ? spentInPeriod(u, budget.period, now2) : 0;
|
|
143067
|
+
return {
|
|
143068
|
+
balanceRemainingUSD: Math.max(0, budget.amountUSD - spent),
|
|
143069
|
+
balanceTotalUSD: budget.amountUSD,
|
|
143070
|
+
balanceAt: now2,
|
|
143071
|
+
balanceEstimated: true
|
|
143072
|
+
};
|
|
143073
|
+
}
|
|
143074
|
+
|
|
143075
|
+
// src/model/scoring.ts
|
|
143076
|
+
var DEFAULT_WEIGHTS = {
|
|
143077
|
+
wScarcity: 1,
|
|
143078
|
+
wSwitch: 0.15,
|
|
143079
|
+
wPlan: 1,
|
|
143080
|
+
wLimit: 2,
|
|
143081
|
+
wApiThrottle: 0.5,
|
|
143082
|
+
planHeadroomKnee: 0.2,
|
|
143083
|
+
apiThrottleKnee: 0.15,
|
|
143084
|
+
scarcityStaleMs: 15 * 60000
|
|
143085
|
+
};
|
|
143086
|
+
var clamp2 = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
143087
|
+
function scoreCandidate(c, input) {
|
|
143088
|
+
const w = input.weights ?? DEFAULT_WEIGHTS;
|
|
143089
|
+
const inTok = input.estInputTokens;
|
|
143090
|
+
const outTok = input.estOutputTokens ?? 0.2 * inTok;
|
|
143091
|
+
const costEst = inTok / 1e6 * c.inUSDPerMtok + outTok / 1e6 * c.outUSDPerMtok;
|
|
143092
|
+
const a = c.account;
|
|
143093
|
+
let planBonus = 0;
|
|
143094
|
+
const meteredEquiv = costEst;
|
|
143095
|
+
if (a.isSubscription) {
|
|
143096
|
+
const headroom = a.rateHeadroom ?? 1;
|
|
143097
|
+
const ramp = clamp2(headroom / w.planHeadroomKnee, 0, 1);
|
|
143098
|
+
planBonus = w.wPlan * meteredEquiv * ramp;
|
|
143099
|
+
}
|
|
143100
|
+
let scarcity = 0;
|
|
143101
|
+
if (!a.isSubscription && a.balanceRemainingUSD !== undefined) {
|
|
143102
|
+
const fresh = a.balanceAt === undefined || input.now - a.balanceAt <= w.scarcityStaleMs;
|
|
143103
|
+
if (fresh)
|
|
143104
|
+
scarcity = w.wScarcity * (costEst / Math.max(a.balanceRemainingUSD, 0.000001));
|
|
143105
|
+
}
|
|
143106
|
+
let limitPenalty = 0;
|
|
143107
|
+
if (a.isSubscription && a.rateHeadroom !== undefined && a.rateHeadroom < w.planHeadroomKnee) {
|
|
143108
|
+
limitPenalty = w.wLimit * ((w.planHeadroomKnee - a.rateHeadroom) / w.planHeadroomKnee);
|
|
143109
|
+
}
|
|
143110
|
+
let apiThrottlePenalty = 0;
|
|
143111
|
+
if (!a.isSubscription && a.apiThrottle !== undefined && a.apiThrottle < w.apiThrottleKnee) {
|
|
143112
|
+
apiThrottlePenalty = w.wApiThrottle * ((w.apiThrottleKnee - a.apiThrottle) / w.apiThrottleKnee);
|
|
143113
|
+
}
|
|
143114
|
+
const warm = !!input.warm && input.warm.accountId === a.accountId && input.warm.modelId === c.id;
|
|
143115
|
+
const switchPenalty = input.warm && !warm ? w.wSwitch * costEst : 0;
|
|
143116
|
+
const score = costEst + scarcity + switchPenalty + limitPenalty + apiThrottlePenalty - planBonus;
|
|
143117
|
+
return { candidate: c, score, costEst, terms: { costEst, scarcity, switchPenalty, limitPenalty, apiThrottlePenalty, planBonus, meteredEquiv } };
|
|
143118
|
+
}
|
|
143119
|
+
function pickBest(input) {
|
|
143120
|
+
const scored = input.candidates.map((c) => scoreCandidate(c, input));
|
|
143121
|
+
scored.sort((a, b) => a.score - b.score || b.candidate.tps - a.candidate.tps || b.candidate.quality - a.candidate.quality || a.candidate.id.localeCompare(b.candidate.id));
|
|
143122
|
+
return scored[0];
|
|
143123
|
+
}
|
|
143124
|
+
|
|
143125
|
+
// src/model/router.ts
|
|
143126
|
+
var NOMINAL_INPUT_TOKENS = 16000;
|
|
142706
143127
|
var BAR = {
|
|
142707
143128
|
summarize: 0,
|
|
142708
143129
|
classify: 0,
|
|
@@ -142726,8 +143147,8 @@ function classify(prompt) {
|
|
|
142726
143147
|
return "search";
|
|
142727
143148
|
return "code";
|
|
142728
143149
|
}
|
|
142729
|
-
function qualityOf(
|
|
142730
|
-
const pr = profileFor(
|
|
143150
|
+
function qualityOf(c) {
|
|
143151
|
+
const pr = profileFor(c.canonicalId ?? c.spec.id);
|
|
142731
143152
|
if (!pr)
|
|
142732
143153
|
return 0.5;
|
|
142733
143154
|
if (pr.quality.sweBenchVerified != null)
|
|
@@ -142736,14 +143157,16 @@ function qualityOf(m2) {
|
|
|
142736
143157
|
return pr.quality.intelligenceIndex / 100;
|
|
142737
143158
|
return 0.5;
|
|
142738
143159
|
}
|
|
142739
|
-
function
|
|
142740
|
-
const
|
|
142741
|
-
|
|
142742
|
-
|
|
142743
|
-
|
|
143160
|
+
function costPair(c) {
|
|
143161
|
+
const cost = profileFor(c.canonicalId ?? c.spec.id)?.cost ?? c.spec.cost;
|
|
143162
|
+
return cost ?? { inUSDPerMtok: 1e6, outUSDPerMtok: 1e6 };
|
|
143163
|
+
}
|
|
143164
|
+
function tpsOf(c) {
|
|
143165
|
+
return profileFor(c.canonicalId ?? c.spec.id)?.latency?.tps ?? 0;
|
|
142744
143166
|
}
|
|
142745
|
-
function
|
|
142746
|
-
|
|
143167
|
+
function toScoreCandidate(c) {
|
|
143168
|
+
const cost = costPair(c);
|
|
143169
|
+
return { id: c.spec.id, inUSDPerMtok: cost.inUSDPerMtok, outUSDPerMtok: cost.outUSDPerMtok, quality: qualityOf(c), tps: tpsOf(c), account: c.state };
|
|
142747
143170
|
}
|
|
142748
143171
|
|
|
142749
143172
|
class RoutingSelector {
|
|
@@ -142751,58 +143174,231 @@ class RoutingSelector {
|
|
|
142751
143174
|
constructor(fallbackId) {
|
|
142752
143175
|
this.fallbackId = fallbackId;
|
|
142753
143176
|
}
|
|
142754
|
-
|
|
143177
|
+
enumerate(ctx) {
|
|
143178
|
+
const out = [];
|
|
143179
|
+
const neutral = (id, provider) => ctx.byAccountId.get(id) ?? { accountId: id, provider, exec: "in-loop", isSubscription: false };
|
|
143180
|
+
for (const m2 of modelRegistry().filter((mm) => providerAvailable(mm.provider))) {
|
|
143181
|
+
const accts = accountsForProvider(m2.provider).filter((a) => a.enabled && a.exec !== "cli");
|
|
143182
|
+
if (accts.length === 0) {
|
|
143183
|
+
out.push({ spec: m2, canonicalId: m2.id, backend: { kind: "in-loop" }, state: neutral(`env:${m2.provider}`, m2.provider) });
|
|
143184
|
+
} else {
|
|
143185
|
+
for (const a of accts)
|
|
143186
|
+
out.push({ spec: m2, canonicalId: m2.id, backend: { kind: "in-loop", account: a }, state: neutral(a.id, m2.provider) });
|
|
143187
|
+
}
|
|
143188
|
+
}
|
|
143189
|
+
for (const seat of subscriptionSeats()) {
|
|
143190
|
+
const state = ctx.byAccountId.get(seat.account.id) ?? { accountId: seat.account.id, provider: seat.account.provider, exec: "cli", isSubscription: true };
|
|
143191
|
+
out.push({ spec: seat.spec, canonicalId: seat.canonicalId, backend: { kind: "cli", account: seat.account, binary: seat.binary, profile: seat.profile }, state });
|
|
143192
|
+
}
|
|
143193
|
+
const live = out.filter((c) => !coolingDown(c.state.accountId, ctx.now));
|
|
143194
|
+
return live.length ? live : out;
|
|
143195
|
+
}
|
|
143196
|
+
prepare(task) {
|
|
142755
143197
|
const kind = task.kind ?? classify(task.prompt);
|
|
142756
143198
|
const bar = BAR[kind];
|
|
142757
143199
|
const required2 = task.requires ?? [];
|
|
142758
|
-
const
|
|
142759
|
-
const
|
|
142760
|
-
|
|
142761
|
-
|
|
142762
|
-
throw new Error(`No configured model supports this turn (${required2.join(", ")} required). ${missing}`);
|
|
142763
|
-
}
|
|
142764
|
-
if (available.length === 0) {
|
|
143200
|
+
const ctx = buildRoutingContext(ctx_now());
|
|
143201
|
+
const estInputTokens = task.estTokens || NOMINAL_INPUT_TOKENS;
|
|
143202
|
+
const all = this.enumerate(ctx);
|
|
143203
|
+
if (all.length === 0) {
|
|
142765
143204
|
const m2 = pickDefaultModel(this.fallbackId);
|
|
142766
|
-
|
|
142767
|
-
|
|
142768
|
-
|
|
142769
|
-
|
|
143205
|
+
return { kind, bar, required: required2, ctx, pool: [], clears: [], estInputTokens, fallback: m2 ?? undefined };
|
|
143206
|
+
}
|
|
143207
|
+
const capable = required2.length ? all.filter((c) => supportsRequirements(c.spec, required2)) : all;
|
|
143208
|
+
if (capable.length === 0) {
|
|
143209
|
+
const missing = all.slice(0, 4).map((c) => `${c.spec.label}: ${missingRequirements(c.spec, required2).join(", ")}`).join("; ");
|
|
143210
|
+
throw new Error(`No configured model supports this turn (${required2.join(", ")} required). ${missing}`);
|
|
142770
143211
|
}
|
|
142771
143212
|
const need = (task.estTokens ?? 0) * 1.2;
|
|
142772
|
-
const
|
|
142773
|
-
|
|
142774
|
-
|
|
142775
|
-
const clears = pool.filter((
|
|
142776
|
-
|
|
143213
|
+
const fits = need > 0 ? capable.filter((c) => c.spec.contextWindow >= need) : capable;
|
|
143214
|
+
let pool = fits.length ? fits : capable;
|
|
143215
|
+
pool = applyGlobalPreference(pool);
|
|
143216
|
+
const clears = pool.filter((c) => qualityOf(c) >= bar);
|
|
143217
|
+
return { kind, bar, required: required2, ctx, pool, clears, estInputTokens };
|
|
143218
|
+
}
|
|
143219
|
+
preferredIn(kind, candidates) {
|
|
142777
143220
|
const pref = preferenceFor(kind);
|
|
142778
|
-
|
|
142779
|
-
|
|
142780
|
-
|
|
142781
|
-
|
|
143221
|
+
return pref?.modelId ? candidates.find((c) => c.canonicalId === pref.modelId || c.spec.id === pref.modelId) : pref?.provider ? candidates.find((c) => c.spec.provider === pref.provider) : undefined;
|
|
143222
|
+
}
|
|
143223
|
+
select(task) {
|
|
143224
|
+
const p = this.prepare(task);
|
|
143225
|
+
if (p.pool.length === 0) {
|
|
143226
|
+
if (!p.fallback) {
|
|
143227
|
+
throw new Error("No model available. Set a key: ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / DEEPSEEK_API_KEY");
|
|
143228
|
+
}
|
|
143229
|
+
return { model: p.fallback, reason: "only model with a key", backend: { kind: "in-loop" } };
|
|
143230
|
+
}
|
|
143231
|
+
const candidates = p.clears.length ? p.clears : p.pool;
|
|
143232
|
+
const preferred = this.preferredIn(p.kind, candidates);
|
|
143233
|
+
if (preferred)
|
|
143234
|
+
return { model: preferred.spec, reason: `${p.kind} · remembered preference`, backend: preferred.backend };
|
|
143235
|
+
const best = pickBest({ candidates: candidates.map(toScoreCandidate), now: p.ctx.now, estInputTokens: p.estInputTokens });
|
|
143236
|
+
const winner = candidates.find((c) => c.spec.id === best.candidate.id);
|
|
143237
|
+
return { model: winner.spec, reason: reasonFor(winner, p.kind, p.required), backend: winner.backend };
|
|
143238
|
+
}
|
|
143239
|
+
explain(task) {
|
|
143240
|
+
const p = this.prepare(task);
|
|
143241
|
+
const entries = [];
|
|
143242
|
+
if (p.pool.length === 0) {
|
|
143243
|
+
return { kind: p.kind, bar: p.bar, prompt: task.prompt, entries, note: p.fallback ? `only ${p.fallback.label} has a key` : "no model available" };
|
|
143244
|
+
}
|
|
143245
|
+
const candidates = p.clears.length ? p.clears : p.pool;
|
|
143246
|
+
const preferred = this.preferredIn(p.kind, candidates);
|
|
143247
|
+
const scored = new Map;
|
|
143248
|
+
for (const s2 of p.pool.map((c) => scoreCandidate(toScoreCandidate(c), { candidates: [], now: p.ctx.now, estInputTokens: p.estInputTokens })))
|
|
143249
|
+
scored.set(s2.candidate.id, s2);
|
|
143250
|
+
const winnerId = preferred ? preferred.spec.id : pickBest({ candidates: candidates.map(toScoreCandidate), now: p.ctx.now, estInputTokens: p.estInputTokens }).candidate.id;
|
|
143251
|
+
for (const c of p.pool) {
|
|
143252
|
+
const s2 = scored.get(c.spec.id);
|
|
143253
|
+
const clears = qualityOf(c) >= p.bar;
|
|
143254
|
+
const chosen = c.spec.id === winnerId;
|
|
143255
|
+
entries.push({
|
|
143256
|
+
label: c.spec.label,
|
|
143257
|
+
backend: c.backend.kind === "cli" ? "seat" : "api",
|
|
143258
|
+
quality: qualityOf(c),
|
|
143259
|
+
qualitySrc: profileFor(c.canonicalId ?? c.spec.id)?.quality.src ?? "seeded",
|
|
143260
|
+
estCostPerMtok: costPerMtok(c),
|
|
143261
|
+
balanceText: balanceText(c.state),
|
|
143262
|
+
headroomText: headroomText(c.state),
|
|
143263
|
+
score: s2.score,
|
|
143264
|
+
chosen,
|
|
143265
|
+
verdict: chosen ? preferred ? "preferred" : "chosen" : !clears ? "below bar" : verdictFor(c, s2)
|
|
143266
|
+
});
|
|
142782
143267
|
}
|
|
142783
|
-
|
|
142784
|
-
|
|
142785
|
-
const skipped = preferredPool && qualityOf(preferredPool) < bar ? ` · ${preferredPool.label} skipped below quality bar` : "";
|
|
142786
|
-
const caps = required2.length ? ` · ${required2.join("+")} required` : "";
|
|
142787
|
-
const reason = `${kind}${caps} · $${costOf(model).toFixed(2)}/Mtok${skipped}`;
|
|
142788
|
-
return { model, reason };
|
|
143268
|
+
entries.sort((a, b) => Number(b.chosen) - Number(a.chosen) || Number(b.verdict !== "below bar") - Number(a.verdict !== "below bar") || a.score - b.score);
|
|
143269
|
+
return { kind: p.kind, bar: p.bar, prompt: task.prompt, entries };
|
|
142789
143270
|
}
|
|
142790
143271
|
}
|
|
143272
|
+
function costPerMtok(c) {
|
|
143273
|
+
const { inUSDPerMtok, outUSDPerMtok } = costPair(c);
|
|
143274
|
+
return inUSDPerMtok + 0.2 * outUSDPerMtok;
|
|
143275
|
+
}
|
|
143276
|
+
function balanceText(s2) {
|
|
143277
|
+
if (s2.isSubscription || s2.balanceRemainingUSD === undefined)
|
|
143278
|
+
return;
|
|
143279
|
+
const v = s2.balanceRemainingUSD;
|
|
143280
|
+
const amt = v >= 100 ? `$${Math.round(v)}` : `$${v.toFixed(2)}`;
|
|
143281
|
+
return s2.balanceEstimated ? `${amt} est` : amt;
|
|
143282
|
+
}
|
|
143283
|
+
function headroomText(s2) {
|
|
143284
|
+
if (s2.isSubscription && s2.rateHeadroom !== undefined)
|
|
143285
|
+
return `${Math.round(s2.rateHeadroom * 100)}% left`;
|
|
143286
|
+
if (!s2.isSubscription && s2.apiThrottle !== undefined && s2.apiThrottle < 0.15)
|
|
143287
|
+
return "throttling";
|
|
143288
|
+
return;
|
|
143289
|
+
}
|
|
143290
|
+
function verdictFor(c, s2) {
|
|
143291
|
+
if (c.state.isSubscription)
|
|
143292
|
+
return "seat ~free";
|
|
143293
|
+
if (s2.terms.scarcity > s2.terms.costEst)
|
|
143294
|
+
return "scarce credit";
|
|
143295
|
+
if (s2.terms.apiThrottlePenalty > 0)
|
|
143296
|
+
return "near limit";
|
|
143297
|
+
return "ok";
|
|
143298
|
+
}
|
|
143299
|
+
function applyGlobalPreference(pool) {
|
|
143300
|
+
const g = globalPreference();
|
|
143301
|
+
if (!g)
|
|
143302
|
+
return pool;
|
|
143303
|
+
let p = pool;
|
|
143304
|
+
const keep = (next) => {
|
|
143305
|
+
if (next.length)
|
|
143306
|
+
p = next;
|
|
143307
|
+
};
|
|
143308
|
+
if (g.prefer === "subscription")
|
|
143309
|
+
keep(p.filter((c) => c.state.isSubscription));
|
|
143310
|
+
else if (g.prefer === "api")
|
|
143311
|
+
keep(p.filter((c) => !c.state.isSubscription));
|
|
143312
|
+
if (g.accountId)
|
|
143313
|
+
keep(p.filter((c) => c.state.accountId === g.accountId));
|
|
143314
|
+
if (g.provider)
|
|
143315
|
+
keep(p.filter((c) => c.spec.provider === g.provider || c.state.provider === g.provider));
|
|
143316
|
+
return p;
|
|
143317
|
+
}
|
|
143318
|
+
function reasonFor(c, kind, required2) {
|
|
143319
|
+
const caps = required2.length ? ` · ${required2.join("+")} required` : "";
|
|
143320
|
+
if (c.backend.kind === "cli")
|
|
143321
|
+
return `${kind}${caps} · ${c.backend.binary} subscription · seat`;
|
|
143322
|
+
const { inUSDPerMtok, outUSDPerMtok } = costPair(c);
|
|
143323
|
+
return `${kind}${caps} · $${(inUSDPerMtok + 0.2 * outUSDPerMtok).toFixed(2)}/Mtok`;
|
|
143324
|
+
}
|
|
143325
|
+
function ctx_now() {
|
|
143326
|
+
return Date.now();
|
|
143327
|
+
}
|
|
143328
|
+
|
|
143329
|
+
// src/model/rate-headers.ts
|
|
143330
|
+
function parseGoDuration(s2) {
|
|
143331
|
+
if (!s2)
|
|
143332
|
+
return;
|
|
143333
|
+
const re = /(\d+(?:\.\d+)?)(ms|h|m|s)/g;
|
|
143334
|
+
let total = 0;
|
|
143335
|
+
let matched = false;
|
|
143336
|
+
let m2;
|
|
143337
|
+
while (m2 = re.exec(s2)) {
|
|
143338
|
+
matched = true;
|
|
143339
|
+
const n = Number(m2[1]);
|
|
143340
|
+
total += m2[2] === "h" ? n * 3600 : m2[2] === "m" ? n * 60 : m2[2] === "ms" ? n / 1000 : n;
|
|
143341
|
+
}
|
|
143342
|
+
return matched ? total : undefined;
|
|
143343
|
+
}
|
|
143344
|
+
var num = (v) => {
|
|
143345
|
+
if (v == null)
|
|
143346
|
+
return;
|
|
143347
|
+
const n = Number(v);
|
|
143348
|
+
return Number.isFinite(n) ? n : undefined;
|
|
143349
|
+
};
|
|
143350
|
+
var clamp012 = (n) => Math.max(0, Math.min(1, n));
|
|
143351
|
+
function resetSeconds(raw, now2) {
|
|
143352
|
+
if (!raw)
|
|
143353
|
+
return;
|
|
143354
|
+
const iso = Date.parse(raw);
|
|
143355
|
+
if (!Number.isNaN(iso))
|
|
143356
|
+
return Math.floor(iso / 1000);
|
|
143357
|
+
const dur = parseGoDuration(raw);
|
|
143358
|
+
if (dur != null)
|
|
143359
|
+
return Math.floor(now2 / 1000) + Math.round(dur);
|
|
143360
|
+
const secs = num(raw);
|
|
143361
|
+
return secs != null ? Math.floor(now2 / 1000) + Math.round(secs) : undefined;
|
|
143362
|
+
}
|
|
143363
|
+
function window2(limit, remaining, reset, type, now2) {
|
|
143364
|
+
if (limit == null || limit <= 0 || remaining == null)
|
|
143365
|
+
return null;
|
|
143366
|
+
return { utilization: clamp012(1 - remaining / limit), resetsAt: resetSeconds(reset, now2), type };
|
|
143367
|
+
}
|
|
143368
|
+
function parseRateHeaders(_provider, headers, now2) {
|
|
143369
|
+
const h2 = {};
|
|
143370
|
+
for (const [k, v] of Object.entries(headers))
|
|
143371
|
+
h2[k.toLowerCase()] = v;
|
|
143372
|
+
const out = [];
|
|
143373
|
+
const aReq = window2(num(h2["anthropic-ratelimit-requests-limit"]), num(h2["anthropic-ratelimit-requests-remaining"]), h2["anthropic-ratelimit-requests-reset"], "api:requests", now2);
|
|
143374
|
+
const aTok = window2(num(h2["anthropic-ratelimit-tokens-limit"]), num(h2["anthropic-ratelimit-tokens-remaining"]), h2["anthropic-ratelimit-tokens-reset"], "api:tokens", now2);
|
|
143375
|
+
if (aReq)
|
|
143376
|
+
out.push(aReq);
|
|
143377
|
+
if (aTok)
|
|
143378
|
+
out.push(aTok);
|
|
143379
|
+
const oReq = window2(num(h2["x-ratelimit-limit-requests"]), num(h2["x-ratelimit-remaining-requests"]), h2["x-ratelimit-reset-requests"], "api:requests", now2);
|
|
143380
|
+
const oTok = window2(num(h2["x-ratelimit-limit-tokens"]), num(h2["x-ratelimit-remaining-tokens"]), h2["x-ratelimit-reset-tokens"], "api:tokens", now2);
|
|
143381
|
+
if (oReq && !aReq)
|
|
143382
|
+
out.push(oReq);
|
|
143383
|
+
if (oTok && !aTok)
|
|
143384
|
+
out.push(oTok);
|
|
143385
|
+
return out;
|
|
143386
|
+
}
|
|
142791
143387
|
|
|
142792
143388
|
// src/ui/App.tsx
|
|
142793
143389
|
init_reasoning();
|
|
142794
143390
|
init_providers();
|
|
142795
143391
|
|
|
142796
143392
|
// src/ui/panel.ts
|
|
142797
|
-
var
|
|
142798
|
-
var clampIndex = (i2, count) => count <= 0 ? 0 :
|
|
142799
|
-
var clampScroll = (s2, max2) =>
|
|
143393
|
+
var clamp3 = (n, lo, hi) => Math.max(lo, Math.min(n, hi));
|
|
143394
|
+
var clampIndex = (i2, count) => count <= 0 ? 0 : clamp3(i2, 0, count - 1);
|
|
143395
|
+
var clampScroll = (s2, max2) => clamp3(s2, 0, Math.max(0, max2));
|
|
142800
143396
|
var panelBodyHeight = (height) => Math.max(1, height - 2);
|
|
142801
143397
|
function windowStart(index, count, viewH) {
|
|
142802
143398
|
if (count <= viewH)
|
|
142803
143399
|
return 0;
|
|
142804
143400
|
const half = Math.floor(viewH / 2);
|
|
142805
|
-
return
|
|
143401
|
+
return clamp3(index - half, 0, count - viewH);
|
|
142806
143402
|
}
|
|
142807
143403
|
function filterModelRows(rows, filter7) {
|
|
142808
143404
|
const q = filter7.trim().toLowerCase();
|
|
@@ -142893,6 +143489,21 @@ function Panel({
|
|
|
142893
143489
|
r2.status
|
|
142894
143490
|
]
|
|
142895
143491
|
}, undefined, true, undefined, this),
|
|
143492
|
+
r2.detail ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
|
|
143493
|
+
color: color.faint,
|
|
143494
|
+
children: [
|
|
143495
|
+
" · ",
|
|
143496
|
+
r2.detail
|
|
143497
|
+
]
|
|
143498
|
+
}, undefined, true, undefined, this) : null,
|
|
143499
|
+
r2.type === "subscription" && !(r2.detail && r2.detail.includes("@")) ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
|
|
143500
|
+
color: color.accentDim,
|
|
143501
|
+
children: [
|
|
143502
|
+
" · /account login ",
|
|
143503
|
+
r2.alias,
|
|
143504
|
+
" to identify"
|
|
143505
|
+
]
|
|
143506
|
+
}, undefined, true, undefined, this) : null,
|
|
142896
143507
|
r2.active ? /* @__PURE__ */ jsx_dev_runtime12.jsxDEV(Text, {
|
|
142897
143508
|
color: color.ok,
|
|
142898
143509
|
children: [
|
|
@@ -142905,7 +143516,7 @@ function Panel({
|
|
|
142905
143516
|
}, r2.alias, true, undefined, this);
|
|
142906
143517
|
})
|
|
142907
143518
|
}, undefined, false, undefined, this);
|
|
142908
|
-
hint = "↑↓ move · ⏎ switch ·
|
|
143519
|
+
hint = "↑↓ move · ⏎ switch · esc close";
|
|
142909
143520
|
} else {
|
|
142910
143521
|
const rows = filterModelRows(models ?? [], panel.filter);
|
|
142911
143522
|
const idx = clampIndex(panel.index, rows.length);
|
|
@@ -144542,13 +145153,19 @@ var resultSummary = (out) => {
|
|
|
144542
145153
|
async function runTask(opts) {
|
|
144543
145154
|
const { model, messages, onEvent, signal, plan } = opts;
|
|
144544
145155
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
145156
|
+
let failureMessage;
|
|
144545
145157
|
const providerOptions = opts.effort ? reasoningOptions(model, opts.effort) : {};
|
|
144546
145158
|
let errored = false;
|
|
145159
|
+
let producedOutput = false;
|
|
145160
|
+
let failureRaw = undefined;
|
|
144547
145161
|
const emitErr = (err) => {
|
|
144548
145162
|
if (errored || signal?.aborted)
|
|
144549
145163
|
return;
|
|
144550
145164
|
errored = true;
|
|
144551
|
-
|
|
145165
|
+
failureMessage = cleanError(err);
|
|
145166
|
+
failureRaw = err;
|
|
145167
|
+
if (!opts.deferTerminal)
|
|
145168
|
+
onEvent({ type: "error", message: unavailableModelHint(failureMessage, model) });
|
|
144552
145169
|
};
|
|
144553
145170
|
onEvent({ type: "phase", label: "contacting model", detail: model.label, state: "running" });
|
|
144554
145171
|
const activeTools = await createToolset(onEvent, { readOnly: Boolean(plan) });
|
|
@@ -144603,6 +145220,7 @@ async function runTask(opts) {
|
|
|
144603
145220
|
case "text-delta": {
|
|
144604
145221
|
const t2 = part.text ?? part.textDelta ?? "";
|
|
144605
145222
|
if (t2) {
|
|
145223
|
+
producedOutput = true;
|
|
144606
145224
|
onEvent({ type: "text", text: t2 });
|
|
144607
145225
|
await maybePaint();
|
|
144608
145226
|
}
|
|
@@ -144613,6 +145231,7 @@ async function runTask(opts) {
|
|
|
144613
145231
|
const name31 = part.toolName ?? part.name ?? "tool";
|
|
144614
145232
|
names.set(id, name31);
|
|
144615
145233
|
started.add(id);
|
|
145234
|
+
producedOutput = true;
|
|
144616
145235
|
openStream(id, name31);
|
|
144617
145236
|
onEvent({ type: "tool-start", id, name: name31, arg: "" });
|
|
144618
145237
|
onEvent({ type: "phase", label: friendlyToolPhase(name31), state: "running" });
|
|
@@ -144626,6 +145245,7 @@ async function runTask(opts) {
|
|
|
144626
145245
|
const st = streams.get(id) ?? openStream(id, names.get(id) ?? "tool");
|
|
144627
145246
|
if (!started.has(id)) {
|
|
144628
145247
|
started.add(id);
|
|
145248
|
+
producedOutput = true;
|
|
144629
145249
|
onEvent({ type: "tool-start", id, name: st.name, arg: "" });
|
|
144630
145250
|
}
|
|
144631
145251
|
st.rawBuf += chunk2;
|
|
@@ -144653,6 +145273,7 @@ async function runTask(opts) {
|
|
|
144653
145273
|
onEvent({ type: "tool-stream", id, arg });
|
|
144654
145274
|
} else {
|
|
144655
145275
|
started.add(id);
|
|
145276
|
+
producedOutput = true;
|
|
144656
145277
|
onEvent({ type: "tool-start", id, name: name31, arg });
|
|
144657
145278
|
onEvent({ type: "phase", label: friendlyToolPhase(name31), detail: arg, state: "running" });
|
|
144658
145279
|
}
|
|
@@ -144692,15 +145313,20 @@ async function runTask(opts) {
|
|
|
144692
145313
|
emitErr(e2);
|
|
144693
145314
|
}
|
|
144694
145315
|
let next = messages;
|
|
145316
|
+
let headers;
|
|
144695
145317
|
if (result2) {
|
|
144696
145318
|
try {
|
|
144697
145319
|
const resp = await result2.response;
|
|
144698
145320
|
next = [...messages, ...resp.messages];
|
|
145321
|
+
headers = resp.headers;
|
|
144699
145322
|
} catch {}
|
|
144700
145323
|
}
|
|
144701
|
-
|
|
144702
|
-
|
|
144703
|
-
|
|
145324
|
+
const failure = errored ? { message: failureMessage ?? cleanError(failureRaw), raw: failureRaw, producedOutput } : undefined;
|
|
145325
|
+
if (!opts.deferTerminal) {
|
|
145326
|
+
onEvent({ type: "phase", label: errored ? "blocked" : "finished", state: errored ? "err" : "ok" });
|
|
145327
|
+
onEvent({ type: "done", usage });
|
|
145328
|
+
}
|
|
145329
|
+
return { messages: next, usage, headers, failure };
|
|
144704
145330
|
}
|
|
144705
145331
|
async function runCompletion(opts) {
|
|
144706
145332
|
const { model, system, prompt, onEvent, signal } = opts;
|
|
@@ -144900,7 +145526,7 @@ bun run typecheck
|
|
|
144900
145526
|
},
|
|
144901
145527
|
{
|
|
144902
145528
|
file: "CLAUDE.md",
|
|
144903
|
-
text: "# Gearbox — project guide\n\nGearbox is a multi-provider coding harness for the terminal: a beautiful, simple terminal agent that reads/writes code and runs commands, talking to any provider (Anthropic, OpenAI, Google, DeepSeek) through one clean loop.\n\n**The point of the project:** intelligent per-task *model routing* — automatically picking the right model for each task across every provider and account you pay for. Basic routing is live (`RoutingSelector` — classify → quality bar → cheapest winner); the richer engine (shadow-eval, credit/limit penalties, confidence display) layers on top of the same seam. See `DESIGN.md` for the full vision and `experiments/FINDINGS.md` for the validation behind it.\n\n## The one rule that matters\n\n**Keep the routing seam clean.** The agent must never hardcode a model. It asks a `ModelSelector` for the model to use. `RoutingSelector` is the live default (classify task → filter by quality bar → cheapest winner); `FixedSelector` is used only when a model is explicitly pinned (`--model` flag or `/model <name>`). Concretely:\n\n- `src/model/selector.ts` — the seam. `select(task) => ModelChoice`. Do not bypass it.\n- `src/model/router.ts` — `RoutingSelector`: classify prompt → quality bar → cost-sort candidates → respect `/prefer` preferences.\n- `src/model/profiles.ts` — the data corpus: quality, cost, latency, tokenizer calibration per model. Routing reads this.\n- `src/providers.ts` — maps a provider+model id to an AI SDK model instance. Already multi-provider. Adding a model is data, not code.\n- Every model call captures token usage (`src/agent/run.ts`) so the cost engine has data. Do not drop usage.\n- The UI consumes a normalized `AgentEvent` stream (`src/agent/events.ts`), never the AI SDK's raw types. This decouples the UI from the provider layer and from routing.\n\nIf you find yourself writing `anthropic('claude-...')` anywhere outside `providers.ts`, stop — route it through the selector.\n\n## Layout\n\n```\nsrc/\n cli.tsx entry point; renders the Ink app; picks RoutingSelector by default\n config.ts minimal config (default model, provider from env)\n providers.ts provider+model id -> AI SDK model (multi-provider; contextWindow per model)\n commands.ts slash-command metadata + pure helpers (fuzzy model match, /help, model list)\n tools.ts read / write / edit / list / search / glob / run_shell (AI SDK tools)\n model/\n selector.ts THE ROUTING SEAM — ModelSelector interface + FixedSelector (pinned model)\n router.ts RoutingSelector: classify → quality bar → cost-sort → preferences (the live default)\n profiles.ts model corpus: quality (SWE-bench), cost ($/Mtok), latency, tokenizer calibration\n tokens.ts calibrated token counting (js-tiktoken × per-model calibration factor)\n preferences.ts persist /prefer kind model choices to ~/.gearbox/routing-preferences.json\n reasoning.ts reasoning/thinking config helpers\n context/\n builder.ts context engine: system + memory + repo map + retrieved files + curated history\n retrieve.ts BM25 lexical retrieval — top-K relevant files for a prompt (no model call)\n repomap.ts repo structure summary for the system prompt\n memory.ts project memory (GEARBOX.md / CLAUDE.md loaded into context)\n compact.ts context compaction (/compact)\n accounts/\n types.ts Account + AuthMethod types (API key, AWS, Azure, Vertex, CLI, OpenAI-compat)\n store.ts accounts.json persistence (~/.gearbox/accounts.json)\n catalog.ts provider catalog (known providers, env vars, labels)\n detect.ts auto-detect env creds + cloud credentials\n onboard.ts interactive add/test account flows\n resolve.ts credential resolution (Account → ResolvedCreds, fetching secrets on demand)\n discover.ts per-account model discovery (Azure deployments / Foundry / gateway /models) → account.models; catalog defaultModels are seeds, not callable ids\n usage.ts per-account spend ledger + rate-limit snapshots + balance tracking\n balance.ts provider balance fetch helpers\n help/\n ask.ts /ask corpus: bundled docs + generated command reference, system prompt, meta-question auto-detect\n agent/\n events.ts AgentEvent — normalized stream the UI consumes\n run.ts real agent loop (AI SDK streamText -> AgentEvent), abort-aware; runCompletion = tool-less grounded answer (used by /ask)\n cli-backend.ts claude/codex CLI subprocess backend (for Pro/Max subscriptions)\n mock.ts scripted demo stream (runs with no API key; used by tests)\n ui/\n theme.ts colors + glyphs (the look)\n input.ts pure key→action reducer for the composer (tested)\n history.ts pure ↑/↓ prompt-history nav (tested)\n net.ts background online probe; status bar shows ⚠ offline when down\n useTerminalSize.ts reactive width on resize (everything reflows)\n git.ts current branch for the status line\n App.tsx the Ink app: state, useInput dispatch, commands, turns\n components/ Banner, Transcript, Composer, CommandPalette, StatusBar, PermissionPrompt, Panel\n panel.ts dismissable command-panel model + pure helpers (clamp/window/filter); tested\ntest/ pure-logic + render tests (ink-testing-library); no keys\nDESIGN.md full product vision (routing, requirements, UX)\nexperiments/ prototypes that validated the architecture\n```\n\nThe composer is custom (Ink `useInput` + `src/ui/input.ts`), not a third-party widget — full control over the cursor, ↑/↓ history, and esc-to-interrupt, with no focus/remount fragility. **Multi-line**: ⌃J (or shift/alt+⏎) inserts a newline, ⏎ submits; ↑/↓ move between lines and fall through to history at the top/bottom line; bracketed paste (enabled in `cli.tsx`) inserts multi-line text literally (CR normalized, paste markers stripped) instead of submitting per line. `caretPos()` is the shared line/col helper. **Readline editing** (all pure in `input.ts`, tested): ⌃U/⌃K kill to line start/end, ⌃W / ⌥⌫ kill word, ⌃D forward-delete, ⌥/⌃ + ←→ word-jump, ⌃A/⌃E line home/end. Keys: ⏎ send · ⌃J newline · ↑↓ line/history · ← → cursor · ⌥←→ word · tab complete @file · **shift+tab cycles mode (normal · auto-accept · plan)** · ⌃Y copy last reply · esc interrupt · ⌃c quit. `/keys` shows the cheatsheet.\n\n**Modes & effort.** Three input modes cycled by shift+tab (`App.tsx` `cycleMode`): **normal** (asks before writes/edits/shell), **auto-accept** (file writes/edits apply without asking — the permission broker auto-resolves `write`/`edit`; shell still gated; diffs still render), **plan** (read-only). Plus **yolo** (auto-approve everything) via `/yolo`. **Effort tiers** (`/effort fast|balanced|max`, or `setEffort`) pin the model through the routing seam (fast→haiku, balanced/max→sonnet) — the active mode + `⚡effort` show as badges in the `StatusBar`. **Click pickers** (fullscreen only): clicking the **model** or **effort** label in the status bar opens a floating picker above it (↑↓ select · ⏎ apply · esc close), reusing the same `/model`/`/effort` command path. The slash commands remain the keyboard path. The fragile row+column hit-test lives in pure, tested `statusBarHit`/`statusBarLayout` (`StatusBar.tsx`); `App.tsx` only supplies live layout (composer line count, `PALETTE_ROWS`, the rendered model/effort/mode) and toggles `quickPicker` state. Inline mode has no mouse grab, so the labels stay informational there. **Copy**: ⌃Y / `/copy` copies the last reply via OSC 52 (`src/ui/clipboard.ts`, works over SSH); `/export [file]` writes the transcript to Markdown. **Terminal integration** (`src/ui/terminal.ts`): the tab title (OSC 2) reflects working/idle, and a long turn (>8s) rings the bell + fires a desktop notification (macOS) so you can step away.\n\n**More UX affordances.** **Type-ahead**: prompts submitted while busy are queued (`queueRef`, shown as chips) and sent when the turn ends. **⌃C** interrupts a turn → clears the composer → \"press again to quit\" (`cli.tsx` renders with `exitOnCtrlC:false`). **Large pastes** collapse to a `[Pasted N lines]` chip (`pasteStoreRef`), expanded back on submit. **Fuzzy** `@file`/`/command` pickers (`src/ui/fuzzy.ts` — substring-first, then subsequence scored by boundary+contiguity; tested). **Cost**: live `$` estimate in the status bar from per-turn model+tokens (`estimateCost` + per-model pricing in `providers.ts`). **Syntax highlighting** for code blocks (`src/ui/highlight.ts` — lightweight per-line tokenizer → Ink spans, NEVER raw ANSI; used by both `lines.ts` `clipSpans` and `Markdown.tsx`). `?` on an empty composer shows the cheatsheet (`KEYS_HELP`).\n\n**Sessions** (`src/session.ts`): conversations persist per-project under `~/.gearbox/sessions/<slug>/` (`GEARBOX_HOME` overrides). Each record holds provider-neutral `messages` + the UI `items` + **per-turn `{model, usage, at}`** (routing/cost data — the record is deliberately not single-model). `gearbox --continue`/`-c` resumes the latest; `/resume [n]` lists/loads in-app; `/clear` starts a fresh session. Prompt history persists across runs (`history.json`). Saving is best-effort (never crashes the app); skipped in demo mode.\n\nFeatures: full markdown via **marked** (parse, `marked.lexer`) + **Ink** (render) in `Markdown.tsx` — headings, bold/italic/inline-code, tables, ordered+nested lists, blockquotes, code blocks. NO foreign ANSI in Ink (cli-highlight/marked-terminal were tried and removed — they corrupt Ink's width/wrapping; render marked's token tree as Ink elements instead). Markdown gets a `width` prop (threaded App→Transcript→Markdown) for table/rule sizing. Colored diffs under edits (`src/diff.ts`, edit/write tools return `{summary,diff}`), plan mode (read-only tools + plan prompt; `/plan` or shift+tab), `!cmd` runs a shell command directly (`src/shell.ts`), `@file` mentions (fuzzy picker `src/ui/mention.ts`+`files.ts`; expanded into the model message on send), live \"working · Ns\" timer.\n\n**Boo (the mascot).** A pixel ghost, now **parametric** (`src/ui/ghost/engine.ts`, ported from a Claude Design handoff). A 20×20 pixel sprite composited from composable layers — body (palette) + face (eyes/mouth) + accessory + persona + a frame-driven overlay (tears/dots/confetti/Z's/sparkle/hearts) — then FOLDED into half-block cells (`▀`/`▄`, top px → `t`/glyph color, bottom px → `b`/bg). `renderGhost(cfg)` is the source of truth for the **default blocks path**; it's pure + memoized. The data: 13 faces (`FACES`), 9 palettes (`PALETTES`), 6 accessories, 9 personas (personas/accessories ported but not yet surfaced in the live UI). Ink `color`/`backgroundColor` props only, NEVER raw ANSI (corrupts Ink's width math). PNG paths are **opt-in** via `GEARBOX_GHOST`:\n\n- `GEARBOX_GHOST=kitty` — real PNG via kitty graphics Unicode placeholders (`U+10EEEE`, fg encodes image id, diacritics encode row/col; PNGs transmitted once in `cli.tsx`). NOTE: the placeholder protocol is young and mis-rendered (squished) in Ghostty during testing — kept opt-in until that's solved.\n- `GEARBOX_GHOST=iterm` — OSC 1337 splash banner (iTerm2/WezTerm).\n\n`detectImageMode()` returns `blocks` unless `GEARBOX_GHOST` opts in. Baked PNGs live in `src/ui/mascot-png.ts`; `bun run scripts/ghost-preview.ts` previews the parametric engine (splash + all faces + the in-flow state crops). **Boo is animated but deliberately calm** on the blocks path (`AnimatedGhost` in `Mascot.tsx`): one shared, unhurried 240ms tick (leaf-local `useTick`, never lifted to App root); talk + overlays advance at half that (~480ms). There is NO idle bob/float and NO splash sparkle — motion is a quiet sign of life, not fidgeting (the splash just blinks every ~6s; in-flow only the state-meaningful overlay/talk moves). `GEARBOX_NO_MOTION=1` freezes to frame 0. `/ghost [mood]` cycles the skin (`skinToCfg` maps it to a cfg; `shades` is the cool face + shades accessory).\n\n**Layout: fullscreen by default; inline is opt-in.** **Fullscreen is the default** (alt-screen frame + virtualized scroll region + scrollbar + mouse wheel scroll); `--inline`, `GEARBOX_INLINE=1`, or `/config inline on` (pref `fullscreen: false`) opts into inline mode. `GEARBOX_FULLSCREEN=1` or `--fullscreen` forces fullscreen explicitly. The decision lives in `cli.tsx` (`wantsFullscreen`). Grabbing the mouse for wheel-scroll is exactly what disables native terminal selection, so in fullscreen mode text selection requires the terminal's modifier (e.g. Option-drag in Ghostty). **Inline mode** (the plain `Transcript` component): no alt-screen, no mouse grab — native click-drag selection / scrollback / copy all work with no modifier. The transcript is a **virtualized line buffer**: `src/ui/lines.ts` (`itemsToLines`) flattens items into styled `Line`s (markdown→lines, wrapping, diffs) — INVARIANT: every line ≤ width (tested), so nothing overflows. **Streaming perf**: flattening the markdown-heavy `assistant`/`user` items is super-linear with their length, so `staticItemLines` memoizes per item in a `WeakMap` keyed by object reference (unchanged items keep identity across renders, so only the changing tail re-parses — history is free; running tools are not cached since their spinner animates). On the producer side, assistant **text deltas are coalesced** on a ~45ms flush timer in `App.tsx`'s `onEvent` (mirroring the tool-stream coalescer), so streaming re-renders at ~22fps instead of per-token — both together stop the auto-scroll jitter that grew with reply length. `finishAssistant`/the turn `finally` flush any buffered text before marking done or on interrupt. In fullscreen, `App` renders only the visible window via `Viewport` (`src/ui/components/Viewport.tsx`) at a computed `transcriptHeight = rows − header − footer` (footer over-estimated so the frame never exceeds the screen; alt-screen clips, so under-filling is safe). Fullscreen scroll: mouse wheel (SGR mouse reporting enabled in `cli.tsx`; parsed off raw stdin in `App` since Ink doesn't model mouse — buttons 64/65) and PgUp/PgDn; new output re-pins to the bottom (`atBottomRef`); a scrollbar sits on the right. (In fullscreen, mouse reporting means text selection needs the terminal's modifier, e.g. Option-drag in Ghostty — which is why inline is now the default.) The virtualized buffer replaced an earlier flex/overflow fullscreen that corrupted on tall output. Chrome spans full width; prose wraps ≤100 cols. The plain `Transcript` component is the inline-fallback renderer. `scripts/gen-mascot.ts` still bakes the PNGs + baked sprites (`mascot-sprite.ts` `GHOSTS`) — but those now feed **only the opt-in kitty/iTerm image path** (`image.ts`); the default blocks path renders the parametric engine instead. The splash scales to the terminal (big=2×/mini=1×/none by rows×cols, in `App.tsx`). The inline/working presence is the compact **state ghost** (see below) — a native-resolution head crop so Boo never dominates the transcript.\n\nCommands are grouped in `/help` (models · conversation · accounts · save · modes · settings · other) and `src/commands.ts` carries plain-language descriptions: /model [name] (fuzzy — \"haiku\"; `/model auto` routes, `/model all` lists every provider) /effort [fast|balanced|max] /prefer [kind model] (remember a confirmed routing preference for a task type) /clear /resume /retry /compact /context /memory /ask <q> (answer questions about Gearbox itself from its bundled docs via a cheap routed model; plain meta-questions auto-route here with a visible affordance) /account (unified: list/add/login/use/rm/refresh — `/accounts` and `/login` are hidden aliases; `/account refresh` re-discovers each account's real callable models) /cost /copy /export [file] /plan /yolo /theme /config (theme·vim·notify·inline; `/vim` is a hidden alias) /init /keys /help /exit. **Hidden** (work but not listed): /accounts /login /vim /ghost. **Removed:** /cwd (the working dir now shows in `/context`). `formatModelList` shows usable models first and collapses no-key providers to a one-line count.\n\n**Command panel (fullscreen only).** Big info-dump commands open a dismissable, Esc-closable overlay instead of dumping into the transcript (`Panel.tsx` + pure `panel.ts`, wired in `App.tsx`): `/help` `/keys` `/context` `/cost` `/memory` are scrollable static dumps (reuse `itemsToLines` + `Viewport`); `/account` and `/model` are interactive lists (↑↓ select · ⏎ acts — they just dispatch the equivalent `/account <n>` / `/model <id>` command and close), and `/model` has type-to-filter (127 Foundry models). The panel replaces the transcript Viewport region while open and takes precedence over `welcome`; the key handler is a branch in `useInput` placed after ⌃C so Esc closes the panel rather than interrupting a turn. Short confirmations (`model → haiku`, `remembered`, `✓ added`, errors) stay inline. Inline mode keeps the old inline printing (no alt-screen to overlay). `openInfoPanel` returns false inline so callers fall back to `push`.\n\n**Permission gate:** `write_file`/`edit_file`/`run_shell` block on a confirm before mutating. Broker: `src/permission.ts` (`requestPermission` in the tools; `setPermissionHandler` installed by `App`; no handler → allow, so tests/headless are unchanged). Decisions: **once** (1), **always** (2, grants that kind for the session), **all/yolo** (a, auto-approves everything until toggled), **deny** (3/esc). YOLO is also toggled by `/yolo` or started with `--yolo`; a `⚡ yolo` badge shows in the status. The `!` prefix is user-initiated so it is NOT gated. Search/nav tools: `search` (ripgrep, Bun-walk fallback) and `glob` (`Bun.Glob`), both read-only (also in plan mode). The working indicator IS Boo now (`components/Working.tsx`): a compact head-crop ghost whose face follows the agent state — thinking (dots) → streaming (talk) → tool (loading dots) → a clean-finish celebrate (party hat + confetti) → error (crying with falling tears). `App.tsx` derives `mascotState` from the `onEvent` stream; the success/error beat **lingers ~1.5s** after the turn (`linger` state — the working line gates on `busy || linger`, since it would otherwise unmount the instant `busy` goes false). Crops are per-state (`stateView`): head (rows 4–14), head+dots (2–14), head+hat (0–14) so overlays outside the head still read. This deliberately supersedes the earlier \"Boo stays on the welcome splash only / in-flow movement reads as noise\" decision — the compact, state-bearing ghost is the point of the design port.\n\n## Conventions\n\n- Runtime: **Bun**. TypeScript + TSX. Run with `bun run src/cli.tsx`.\n- UI: **Ink** (React for terminals) + **@inkjs/ui**. Keep it calm and beautiful: restrained palette (one accent), generous spacing, consistent glyphs. The look lives in `src/ui/theme.ts` — change colors/glyphs there, not inline.\n- Open + free: MIT, no paid dependencies, no hosted backend, no telemetry. The only cost is the user's own model calls on their own keys.\n- Tools must be safe by default: confirm or sandbox anything destructive; never `rm -rf` or write outside the workspace without intent.\n\n## Run it\n\n```bash\nbun install\n# set at least one key:\nexport ANTHROPIC_API_KEY=... # or OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / DEEPSEEK_API_KEY\nbun run src/cli.tsx # or: bun start\n```\n\nWith no key it launches in demo mode (a scripted transcript) so the UI still runs.\n\n## Test\n\n```bash\nbun test # render tests + agent-loop tests; no API key needed\nbun run typecheck # tsc --noEmit\n```"
|
|
145529
|
+
text: "# Gearbox — project guide\n\nGearbox is a multi-provider coding harness for the terminal: a beautiful, simple terminal agent that reads/writes code and runs commands, talking to any provider (Anthropic, OpenAI, Google, DeepSeek) through one clean loop.\n\n**The point of the project:** intelligent per-task *model routing* — automatically picking the right model for each task across every provider and account you pay for. Basic routing is live (`RoutingSelector` — classify → quality bar → cheapest winner); the richer engine (shadow-eval, credit/limit penalties, confidence display) layers on top of the same seam. See `DESIGN.md` for the full vision and `experiments/FINDINGS.md` for the validation behind it.\n\n## The one rule that matters\n\n**Keep the routing seam clean.** The agent must never hardcode a model. It asks a `ModelSelector` for the model to use. `RoutingSelector` is the live default (classify task → filter by quality bar → cheapest winner); `FixedSelector` is used only when a model is explicitly pinned (`--model` flag or `/model <name>`). Concretely:\n\n- `src/model/selector.ts` — the seam. `select(task) => ModelChoice` (now carrying an optional `Backend` so the runner can dispatch in-loop vs a subscription seat). Do not bypass it.\n- `src/model/router.ts` — `RoutingSelector` is **account-aware**: it scores `(model, account)` PAIRS, not just models. Candidates = in-loop registry models × the accounts that serve them + flat-rate subscription **seats** (`providers.subscriptionSeats()`, a seat mirrors a canonical model but runs via the vendor binary). Flow: classify → quality bar → context fit → global/`/prefer` preference filter → score → return `{model, reason, backend}`. A seat is ~free until its rate limit, so it wins by default and fails over to metered API as the window fills. `SubscriptionPinSelector`/`FixedSelector` are hard pins (explicit `/account use` or `/model`) that beat auto-routing.\n- `src/model/scoring.ts` — the PURE scorer: `score = costEst + scarcity + switchPenalty + limitPenalty + apiThrottlePenalty − planBonus`, argmin tie-broken deterministically. No I/O; fixture-tested. Every account-state term degrades safely — and where a provider exposes nothing, we ESTIMATE rather than go blind: live API rate-limit headers (`src/model/rate-headers.ts`, parsed from each response → `apiThrottle`, gentle near-empty penalty) and a self-declared budget − tracked spend (`/budget`, an estimated balance feeding scarcity). A missing signal with no estimate is still neutral (no errors per provider).\n- `src/model/routing-context.ts` — `buildRoutingContext()`: the per-turn account-state snapshot (balance where exposed, subscription rate headroom = min over the 5h/weekly windows) read from disk-cached `usage.json`. No network on the hot path; balances refreshed in the background (App effect).\n- `src/model/profiles.ts` — the data corpus: quality, cost, latency, tokenizer calibration, **and the per-model effort vocabulary** (`efforts`) per the provider research. Routing reads this; effort is clamped/omitted against the chosen model's set, never sent unsupported.\n- `src/providers.ts` — maps a provider+model id to an AI SDK model instance. Already multi-provider. Adding a model is data, not code.\n- Every model call captures token usage (`src/agent/run.ts`) so the cost engine has data. Do not drop usage.\n- The UI consumes a normalized `AgentEvent` stream (`src/agent/events.ts`), never the AI SDK's raw types. This decouples the UI from the provider layer and from routing.\n\nIf you find yourself writing `anthropic('claude-...')` anywhere outside `providers.ts`, stop — route it through the selector.\n\n## Layout\n\n```\nsrc/\n cli.tsx entry point; renders the Ink app; picks RoutingSelector by default\n config.ts minimal config (default model, provider from env)\n providers.ts provider+model id -> AI SDK model (multi-provider; contextWindow per model)\n commands.ts slash-command metadata + pure helpers (fuzzy model match, /help, model list)\n tools.ts read / write / edit / list / search / glob / run_shell (AI SDK tools)\n model/\n selector.ts THE ROUTING SEAM — ModelSelector + ModelChoice.backend + FixedSelector (pinned model)\n router.ts RoutingSelector (account-aware): scores (model, account) pairs incl. subscription seats; SubscriptionPinSelector\n scoring.ts PURE scorer: cost + scarcity + limit − plan bonus; deterministic, fixture-tested\n routing-context.ts per-turn account-state snapshot (balance + subscription rate headroom) from usage.json\n cooldown.ts reactive-failover support: classify a failure + park an exhausted account so the router routes around it\n profiles.ts model corpus: quality (SWE-bench), cost ($/Mtok), latency, tokenizer calibration, per-model effort vocab\n tokens.ts calibrated token counting (js-tiktoken × per-model calibration factor)\n preferences.ts persist /prefer kind model choices to ~/.gearbox/routing-preferences.json\n reasoning.ts reasoning/thinking config helpers\n context/\n builder.ts context engine: system + memory + repo map + retrieved files + curated history\n retrieve.ts BM25 lexical retrieval — top-K relevant files for a prompt (no model call)\n repomap.ts repo structure summary for the system prompt\n memory.ts project memory (GEARBOX.md / CLAUDE.md loaded into context)\n compact.ts context compaction (/compact)\n accounts/\n types.ts Account + AuthMethod types (API key, AWS, Azure, Vertex, CLI, OpenAI-compat)\n store.ts accounts.json persistence (~/.gearbox/accounts.json)\n catalog.ts provider catalog (known providers, env vars, labels)\n detect.ts auto-detect env creds + cloud credentials\n onboard.ts interactive add/test account flows; addByPastedKey routes through sniff.ts\n sniff.ts pure credential sniffer: paste anything (key / AWS block / SA JSON / Azure URL / gateway key) → {kind, provider, fields, missing}\n resolve.ts credential resolution (Account → ResolvedCreds) + rank(model) → ordered failover pool (cross-provider, health-sorted)\n discover.ts per-account model discovery (Azure deployments / Foundry / gateway /models) → account.models; catalog defaultModels are seeds, not callable ids\n health.ts account health: classifyError (provider error → state), checkHealth (cached probe, timeout-bounded), recordHealth; no background polling\n usage.ts per-account spend ledger + rate-limit snapshots + balance tracking\n balance.ts provider balance fetch helpers\n help/\n ask.ts /ask corpus: bundled docs + generated command reference, system prompt, meta-question auto-detect\n agent/\n events.ts AgentEvent — normalized stream the UI consumes\n run.ts real agent loop (AI SDK streamText -> AgentEvent), abort-aware; runCompletion = tool-less grounded answer (used by /ask); returns a structured failure (for failover)\n failover.ts runWithFailover: run a turn over the ranked account pool; on a credential failure before output, advance to the next; clear exhaustion errors\n cli-backend.ts claude/codex CLI subprocess backend (for Pro/Max subscriptions)\n mock.ts scripted demo stream (runs with no API key; used by tests)\n ui/\n theme.ts colors + glyphs (the look)\n input.ts pure key→action reducer for the composer (tested)\n history.ts pure ↑/↓ prompt-history nav (tested)\n net.ts background online probe; status bar shows ⚠ offline when down\n useTerminalSize.ts reactive width on resize (everything reflows)\n git.ts current branch for the status line\n App.tsx the Ink app: state, useInput dispatch, commands, turns\n components/ Banner, Transcript, Composer, CommandPalette, StatusBar, PermissionPrompt, Panel\n panel.ts dismissable command-panel model + pure helpers (clamp/window/filter); tested\ntest/ pure-logic + render tests (ink-testing-library); no keys\nDESIGN.md full product vision (routing, requirements, UX)\nexperiments/ prototypes that validated the architecture\n```\n\nThe composer is custom (Ink `useInput` + `src/ui/input.ts`), not a third-party widget — full control over the cursor, ↑/↓ history, and esc-to-interrupt, with no focus/remount fragility. **Multi-line**: ⌃J (or shift/alt+⏎) inserts a newline, ⏎ submits; ↑/↓ move between lines and fall through to history at the top/bottom line; bracketed paste (enabled in `cli.tsx`) inserts multi-line text literally (CR normalized, paste markers stripped) instead of submitting per line. `caretPos()` is the shared line/col helper. **Readline editing** (all pure in `input.ts`, tested): ⌃U/⌃K kill to line start/end, ⌃W / ⌥⌫ kill word, ⌃D forward-delete, ⌥/⌃ + ←→ word-jump, ⌃A/⌃E line home/end. Keys: ⏎ send · ⌃J newline · ↑↓ line/history · ← → cursor · ⌥←→ word · tab complete @file · **shift+tab cycles mode (normal · auto-accept · plan)** · ⌃Y copy last reply · esc interrupt · ⌃c quit. `/keys` shows the cheatsheet.\n\n**Modes & effort.** Three input modes cycled by shift+tab (`App.tsx` `cycleMode`): **normal** (asks before writes/edits/shell), **auto-accept** (file writes/edits apply without asking — the permission broker auto-resolves `write`/`edit`; shell still gated; diffs still render), **plan** (read-only). Plus **yolo** (auto-approve everything) via `/yolo`. **Effort tiers** (`/effort fast|balanced|max`, or `setEffort`) pin the model through the routing seam (fast→haiku, balanced/max→sonnet) — the active mode + `⚡effort` show as badges in the `StatusBar`. **Click pickers** (fullscreen only): clicking the **model** or **effort** label in the status bar opens a floating picker above it (↑↓ select · ⏎ apply · esc close), reusing the same `/model`/`/effort` command path. The slash commands remain the keyboard path. The fragile row+column hit-test lives in pure, tested `statusBarHit`/`statusBarLayout` (`StatusBar.tsx`); `App.tsx` only supplies live layout (composer line count, `PALETTE_ROWS`, the rendered model/effort/mode) and toggles `quickPicker` state. Inline mode has no mouse grab, so the labels stay informational there. **Copy**: ⌃Y / `/copy` copies the last reply via OSC 52 (`src/ui/clipboard.ts`, works over SSH); `/export [file]` writes the transcript to Markdown. **Terminal integration** (`src/ui/terminal.ts`): the tab title (OSC 2) reflects working/idle, and a long turn (>8s) rings the bell + fires a desktop notification (macOS) so you can step away.\n\n**More UX affordances.** **Type-ahead**: prompts submitted while busy are queued (`queueRef`, shown as chips) and sent when the turn ends. **⌃C** interrupts a turn → clears the composer → \"press again to quit\" (`cli.tsx` renders with `exitOnCtrlC:false`). **Large pastes** collapse to a `[Pasted N lines]` chip (`pasteStoreRef`), expanded back on submit. **Fuzzy** `@file`/`/command` pickers (`src/ui/fuzzy.ts` — substring-first, then subsequence scored by boundary+contiguity; tested). **Cost**: live `$` estimate in the status bar from per-turn model+tokens (`estimateCost` + per-model pricing in `providers.ts`). **Syntax highlighting** for code blocks (`src/ui/highlight.ts` — lightweight per-line tokenizer → Ink spans, NEVER raw ANSI; used by both `lines.ts` `clipSpans` and `Markdown.tsx`). `?` on an empty composer shows the cheatsheet (`KEYS_HELP`).\n\n**Sessions** (`src/session.ts`): conversations persist per-project under `~/.gearbox/sessions/<slug>/` (`GEARBOX_HOME` overrides). Each record holds provider-neutral `messages` + the UI `items` + **per-turn `{model, usage, at}`** (routing/cost data — the record is deliberately not single-model). `gearbox --continue`/`-c` resumes the latest; `/resume [n]` lists/loads in-app; `/clear` starts a fresh session. Prompt history persists across runs (`history.json`). Saving is best-effort (never crashes the app); skipped in demo mode.\n\nFeatures: full markdown via **marked** (parse, `marked.lexer`) + **Ink** (render) in `Markdown.tsx` — headings, bold/italic/inline-code, tables, ordered+nested lists, blockquotes, code blocks. NO foreign ANSI in Ink (cli-highlight/marked-terminal were tried and removed — they corrupt Ink's width/wrapping; render marked's token tree as Ink elements instead). Markdown gets a `width` prop (threaded App→Transcript→Markdown) for table/rule sizing. Colored diffs under edits (`src/diff.ts`, edit/write tools return `{summary,diff}`), plan mode (read-only tools + plan prompt; `/plan` or shift+tab), `!cmd` runs a shell command directly (`src/shell.ts`), `@file` mentions (fuzzy picker `src/ui/mention.ts`+`files.ts`; expanded into the model message on send), live \"working · Ns\" timer.\n\n**Boo (the mascot).** A pixel ghost, now **parametric** (`src/ui/ghost/engine.ts`, ported from a Claude Design handoff). A 20×20 pixel sprite composited from composable layers — body (palette) + face (eyes/mouth) + accessory + persona + a frame-driven overlay (tears/dots/confetti/Z's/sparkle/hearts) — then FOLDED into half-block cells (`▀`/`▄`, top px → `t`/glyph color, bottom px → `b`/bg). `renderGhost(cfg)` is the source of truth for the **default blocks path**; it's pure + memoized. The data: 13 faces (`FACES`), 9 palettes (`PALETTES`), 6 accessories, 9 personas (personas/accessories ported but not yet surfaced in the live UI). Ink `color`/`backgroundColor` props only, NEVER raw ANSI (corrupts Ink's width math). PNG paths are **opt-in** via `GEARBOX_GHOST`:\n\n- `GEARBOX_GHOST=kitty` — real PNG via kitty graphics Unicode placeholders (`U+10EEEE`, fg encodes image id, diacritics encode row/col; PNGs transmitted once in `cli.tsx`). NOTE: the placeholder protocol is young and mis-rendered (squished) in Ghostty during testing — kept opt-in until that's solved.\n- `GEARBOX_GHOST=iterm` — OSC 1337 splash banner (iTerm2/WezTerm).\n\n`detectImageMode()` returns `blocks` unless `GEARBOX_GHOST` opts in. Baked PNGs live in `src/ui/mascot-png.ts`; `bun run scripts/ghost-preview.ts` previews the parametric engine (splash + all faces + the in-flow state crops). **Boo is animated but deliberately calm** on the blocks path (`AnimatedGhost` in `Mascot.tsx`): one shared, unhurried 240ms tick (leaf-local `useTick`, never lifted to App root); talk + overlays advance at half that (~480ms). There is NO idle bob/float and NO splash sparkle — motion is a quiet sign of life, not fidgeting (the splash just blinks every ~6s; in-flow only the state-meaningful overlay/talk moves). `GEARBOX_NO_MOTION=1` freezes to frame 0. `/ghost [mood]` cycles the skin (`skinToCfg` maps it to a cfg; `shades` is the cool face + shades accessory).\n\n**Layout: fullscreen by default; inline is opt-in.** **Fullscreen is the default** (alt-screen frame + virtualized scroll region + scrollbar + mouse wheel scroll); `--inline`, `GEARBOX_INLINE=1`, or `/config inline on` (pref `fullscreen: false`) opts into inline mode. `GEARBOX_FULLSCREEN=1` or `--fullscreen` forces fullscreen explicitly. The decision lives in `cli.tsx` (`wantsFullscreen`). Grabbing the mouse for wheel-scroll is exactly what disables native terminal selection, so in fullscreen mode text selection requires the terminal's modifier (e.g. Option-drag in Ghostty). **Inline mode** (the plain `Transcript` component): no alt-screen, no mouse grab — native click-drag selection / scrollback / copy all work with no modifier. The transcript is a **virtualized line buffer**: `src/ui/lines.ts` (`itemsToLines`) flattens items into styled `Line`s (markdown→lines, wrapping, diffs) — INVARIANT: every line ≤ width (tested), so nothing overflows. **Streaming perf**: flattening the markdown-heavy `assistant`/`user` items is super-linear with their length, so `staticItemLines` memoizes per item in a `WeakMap` keyed by object reference (unchanged items keep identity across renders, so only the changing tail re-parses — history is free; running tools are not cached since their spinner animates). On the producer side, assistant **text deltas are coalesced** on a ~45ms flush timer in `App.tsx`'s `onEvent` (mirroring the tool-stream coalescer), so streaming re-renders at ~22fps instead of per-token — both together stop the auto-scroll jitter that grew with reply length. `finishAssistant`/the turn `finally` flush any buffered text before marking done or on interrupt. In fullscreen, `App` renders only the visible window via `Viewport` (`src/ui/components/Viewport.tsx`) at a computed `transcriptHeight = rows − header − footer` (footer over-estimated so the frame never exceeds the screen; alt-screen clips, so under-filling is safe). Fullscreen scroll: mouse wheel (SGR mouse reporting enabled in `cli.tsx`; parsed off raw stdin in `App` since Ink doesn't model mouse — buttons 64/65) and PgUp/PgDn; new output re-pins to the bottom (`atBottomRef`); a scrollbar sits on the right. (In fullscreen, mouse reporting means text selection needs the terminal's modifier, e.g. Option-drag in Ghostty — which is why inline is now the default.) The virtualized buffer replaced an earlier flex/overflow fullscreen that corrupted on tall output. Chrome spans full width; prose wraps ≤100 cols. The plain `Transcript` component is the inline-fallback renderer. `scripts/gen-mascot.ts` still bakes the PNGs + baked sprites (`mascot-sprite.ts` `GHOSTS`) — but those now feed **only the opt-in kitty/iTerm image path** (`image.ts`); the default blocks path renders the parametric engine instead. The splash scales to the terminal (big=2×/mini=1×/none by rows×cols, in `App.tsx`). The inline/working presence is the compact **state ghost** (see below) — a native-resolution head crop so Boo never dominates the transcript.\n\nCommands are grouped in `/help` (models · conversation · accounts · save · modes · settings · other) and `src/commands.ts` carries plain-language descriptions: /model [name] (fuzzy — \"haiku\"; `/model auto` routes, `/model all` lists every provider) /effort [fast|balanced|max] /prefer [kind model] (remember a confirmed routing preference for a task type) /clear /resume /retry /compact /context /memory /ask <q> (answer questions about Gearbox itself from its bundled docs via a cheap routed model; plain meta-questions auto-route here with a visible affordance) /account (unified: list/add/login/use/rm/refresh — `/accounts` and `/login` are hidden aliases; `/account refresh` re-discovers each account's real callable models) /cost /copy /export [file] /plan /yolo /theme /config (theme·vim·notify·inline; `/vim` is a hidden alias) /init /keys /help /exit. **Hidden** (work but not listed): /accounts /login /vim /ghost. **Removed:** /cwd (the working dir now shows in `/context`). `formatModelList` shows usable models first and collapses no-key providers to a one-line count.\n\n**Command panel (fullscreen only).** Big info-dump commands open a dismissable, Esc-closable overlay instead of dumping into the transcript (`Panel.tsx` + pure `panel.ts`, wired in `App.tsx`): `/help` `/keys` `/context` `/cost` `/memory` are scrollable static dumps (reuse `itemsToLines` + `Viewport`); `/account` and `/model` are interactive lists (↑↓ select · ⏎ acts — they just dispatch the equivalent `/account <n>` / `/model <id>` command and close), and `/model` has type-to-filter (127 Foundry models). The panel replaces the transcript Viewport region while open and takes precedence over `welcome`; the key handler is a branch in `useInput` placed after ⌃C so Esc closes the panel rather than interrupting a turn. Short confirmations (`model → haiku`, `remembered`, `✓ added`, errors) stay inline. Inline mode keeps the old inline printing (no alt-screen to overlay). `openInfoPanel` returns false inline so callers fall back to `push`.\n\n**Accounts: reliability by design.** Every subscription, API key, and cloud credential is meant to work all the time. Switching is **by name only** (a stable unique `slug` per account, e.g. `claude-work`; positional numbers are gone — removing an account never repoints another). Each account carries a cached **health** state (`✓ ready · ⚠ expired · ✗ invalid · ⏳ limited · — unknown`) refreshed at natural touchpoints (boot sweep, opening `/account`, on switch, on live failure) — never by background polling; probes are timeout-bounded. A turn runs through a **failover pool**: `resolve.rank(model)` returns every account that can serve the model's family (cross-provider — Claude can fall Anthropic key → Bedrock → Vertex), health-sorted; `agent/failover.ts` tries them best-first and, on a credential-class failure **before any output** (expired/invalid/no-credit/rate-limited), transparently advances to the next and tells the user which account ran. A real (network/model) error or any failure after output streamed does NOT churn the pool. When the pool is exhausted, one consolidated error names each account, why it failed, and the one command to fix it. Expired subscriptions get one-step re-login: `/account login <name>` (and CLI failures name that exact command). `/account add <paste>` runs the credential **sniffer** (`accounts/sniff.ts`) — paste an API key, an AWS access key or credentials block, a Vertex service-account JSON, an Azure endpoint, or a Vercel gateway key, and it identifies the provider, fills the gaps interactively, and live-tests it.\n\n**Permission gate:** `write_file`/`edit_file`/`run_shell` block on a confirm before mutating. Broker: `src/permission.ts` (`requestPermission` in the tools; `setPermissionHandler` installed by `App`; no handler → allow, so tests/headless are unchanged). Decisions: **once** (1), **always** (2, grants that kind for the session), **all/yolo** (a, auto-approves everything until toggled), **deny** (3/esc). YOLO is also toggled by `/yolo` or started with `--yolo`; a `⚡ yolo` badge shows in the status. The `!` prefix is user-initiated so it is NOT gated. Search/nav tools: `search` (ripgrep, Bun-walk fallback) and `glob` (`Bun.Glob`), both read-only (also in plan mode). The working indicator IS Boo now (`components/Working.tsx`): a compact head-crop ghost whose face follows the agent state — thinking (dots) → streaming (talk) → tool (loading dots) → a clean-finish celebrate (party hat + confetti) → error (crying with falling tears). `App.tsx` derives `mascotState` from the `onEvent` stream; the success/error beat **lingers ~1.5s** after the turn (`linger` state — the working line gates on `busy || linger`, since it would otherwise unmount the instant `busy` goes false). Crops are per-state (`stateView`): head (rows 4–14), head+dots (2–14), head+hat (0–14) so overlays outside the head still read. This deliberately supersedes the earlier \"Boo stays on the welcome splash only / in-flow movement reads as noise\" decision — the compact, state-bearing ghost is the point of the design port.\n\n## Conventions\n\n- Runtime: **Bun**. TypeScript + TSX. Run with `bun run src/cli.tsx`.\n- UI: **Ink** (React for terminals) + **@inkjs/ui**. Keep it calm and beautiful: restrained palette (one accent), generous spacing, consistent glyphs. The look lives in `src/ui/theme.ts` — change colors/glyphs there, not inline.\n- Open + free: MIT, no paid dependencies, no hosted backend, no telemetry. The only cost is the user's own model calls on their own keys.\n- Tools must be safe by default: confirm or sandbox anything destructive; never `rm -rf` or write outside the workspace without intent.\n\n## Run it\n\n```bash\nbun install\n# set at least one key:\nexport ANTHROPIC_API_KEY=... # or OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / DEEPSEEK_API_KEY\nbun run src/cli.tsx # or: bun start\n```\n\nWith no key it launches in demo mode (a scripted transcript) so the UI still runs.\n\n## Test\n\n```bash\nbun test # render tests + agent-loop tests; no API key needed\nbun run typecheck # tsc --noEmit\n```"
|
|
144904
145530
|
},
|
|
144905
145531
|
{
|
|
144906
145532
|
file: "DESIGN.md",
|
|
@@ -145354,11 +145980,30 @@ var PROVIDERS = {
|
|
|
145354
145980
|
"vercel-gateway": {
|
|
145355
145981
|
url: "https://ai-gateway.vercel.sh/v1/credits",
|
|
145356
145982
|
parse: (j) => {
|
|
145357
|
-
const bal = j?.balance ?? j?.credits?.balance;
|
|
145358
|
-
return
|
|
145983
|
+
const bal = num2(j?.balance ?? j?.credits?.balance);
|
|
145984
|
+
return bal == null ? null : { remainingUSD: bal };
|
|
145985
|
+
}
|
|
145986
|
+
},
|
|
145987
|
+
deepseek: {
|
|
145988
|
+
url: "https://api.deepseek.com/user/balance",
|
|
145989
|
+
parse: (j) => {
|
|
145990
|
+
const infos = Array.isArray(j?.balance_infos) ? j.balance_infos : [];
|
|
145991
|
+
const pick3 = infos.find((b) => b?.currency === "USD") ?? infos[0];
|
|
145992
|
+
const remaining = num2(pick3?.total_balance);
|
|
145993
|
+
return remaining == null ? null : { remainingUSD: remaining };
|
|
145359
145994
|
}
|
|
145360
145995
|
}
|
|
145361
145996
|
};
|
|
145997
|
+
function num2(v) {
|
|
145998
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
145999
|
+
return v;
|
|
146000
|
+
if (typeof v === "string") {
|
|
146001
|
+
const n = Number(v);
|
|
146002
|
+
if (Number.isFinite(n))
|
|
146003
|
+
return n;
|
|
146004
|
+
}
|
|
146005
|
+
return;
|
|
146006
|
+
}
|
|
145362
146007
|
function balanceExposed(provider) {
|
|
145363
146008
|
return provider in PROVIDERS;
|
|
145364
146009
|
}
|
|
@@ -145708,6 +146353,83 @@ function writeProjectGuide(cwd2 = process.cwd()) {
|
|
|
145708
146353
|
return { path, summary: `wrote GEARBOX.md (${diffStat(diff2)})`, diff: diff2 };
|
|
145709
146354
|
}
|
|
145710
146355
|
|
|
146356
|
+
// src/accounts/health.ts
|
|
146357
|
+
init_store();
|
|
146358
|
+
init_onboard();
|
|
146359
|
+
function statusOf(err) {
|
|
146360
|
+
return err?.statusCode ?? err?.status ?? err?.response?.status ?? err?.data?.error?.status;
|
|
146361
|
+
}
|
|
146362
|
+
function textOf2(err) {
|
|
146363
|
+
return String(err?.message ?? err?.error?.message ?? err?.responseBody ?? err?.error ?? err ?? "").toLowerCase();
|
|
146364
|
+
}
|
|
146365
|
+
function classifyError(_provider, err) {
|
|
146366
|
+
const status = statusOf(err);
|
|
146367
|
+
const t2 = textOf2(err);
|
|
146368
|
+
if (/credit balance|insufficient_quota|insufficient funds|billing|payment|quota exceeded/.test(t2))
|
|
146369
|
+
return "no-credit";
|
|
146370
|
+
if (/not logged in|not signed in|re-?authenticate|token (?:has )?expired|expired|session expired|login required|refresh token/.test(t2))
|
|
146371
|
+
return "expired";
|
|
146372
|
+
if (status === 429 || /rate.?limit|too many requests|overloaded|capacity/.test(t2))
|
|
146373
|
+
return "rate-limited";
|
|
146374
|
+
if (status === 401 || status === 403 || /invalid.*(api.?key|x-api-key|credential|token)|incorrect api key|unauthorized|authentication.?fail|permission denied/.test(t2))
|
|
146375
|
+
return "invalid";
|
|
146376
|
+
return "real-error";
|
|
146377
|
+
}
|
|
146378
|
+
var HEALTH_TTL_MS = 5 * 60000;
|
|
146379
|
+
var HEALTH_CHECK_TIMEOUT_MS = 8000;
|
|
146380
|
+
function withTimeout(p, ms, fallback) {
|
|
146381
|
+
return new Promise((resolve12, reject2) => {
|
|
146382
|
+
let done = false;
|
|
146383
|
+
const timer = setTimeout(() => {
|
|
146384
|
+
if (!done) {
|
|
146385
|
+
done = true;
|
|
146386
|
+
resolve12(fallback);
|
|
146387
|
+
}
|
|
146388
|
+
}, ms);
|
|
146389
|
+
p.then((v) => {
|
|
146390
|
+
if (!done) {
|
|
146391
|
+
done = true;
|
|
146392
|
+
clearTimeout(timer);
|
|
146393
|
+
resolve12(v);
|
|
146394
|
+
}
|
|
146395
|
+
}, (e2) => {
|
|
146396
|
+
if (!done) {
|
|
146397
|
+
done = true;
|
|
146398
|
+
clearTimeout(timer);
|
|
146399
|
+
reject2(e2);
|
|
146400
|
+
}
|
|
146401
|
+
});
|
|
146402
|
+
});
|
|
146403
|
+
}
|
|
146404
|
+
function isFresh(h2, now3) {
|
|
146405
|
+
return !!h2 && now3 - h2.checkedAt < HEALTH_TTL_MS;
|
|
146406
|
+
}
|
|
146407
|
+
function recordHealth(account, state, detail) {
|
|
146408
|
+
const at3 = Date.now();
|
|
146409
|
+
const cur = getAccount(account.id) ?? account;
|
|
146410
|
+
putAccount({ ...cur, health: { state, checkedAt: at3, detail } });
|
|
146411
|
+
}
|
|
146412
|
+
function checkHealth(account) {
|
|
146413
|
+
const at3 = Date.now();
|
|
146414
|
+
const probe = (async () => {
|
|
146415
|
+
try {
|
|
146416
|
+
if (account.exec === "cli") {
|
|
146417
|
+
const bin = account.auth.binary;
|
|
146418
|
+
const profile = account.auth.loginProfile;
|
|
146419
|
+
const st = await cliAuthStatus(bin, profile);
|
|
146420
|
+
return { state: st.loggedIn ? "ok" : "expired", checkedAt: at3, detail: st.detail };
|
|
146421
|
+
}
|
|
146422
|
+
const r2 = await testAccount(account);
|
|
146423
|
+
if (r2.ok)
|
|
146424
|
+
return { state: "ok", checkedAt: at3 };
|
|
146425
|
+
return { state: classifyError(account.provider, { message: r2.message }), checkedAt: at3, detail: r2.message };
|
|
146426
|
+
} catch (e2) {
|
|
146427
|
+
return { state: classifyError(account.provider, e2), checkedAt: at3, detail: String(e2?.message ?? e2) };
|
|
146428
|
+
}
|
|
146429
|
+
})();
|
|
146430
|
+
return withTimeout(probe, HEALTH_CHECK_TIMEOUT_MS, { state: "unknown", checkedAt: at3, detail: "health check timed out" });
|
|
146431
|
+
}
|
|
146432
|
+
|
|
145711
146433
|
// src/ui/App.tsx
|
|
145712
146434
|
init_mcp();
|
|
145713
146435
|
|
|
@@ -145917,7 +146639,6 @@ import { basename as basename3, extname, resolve as resolve12 } from "node:path"
|
|
|
145917
146639
|
import { existsSync as existsSync11, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
|
|
145918
146640
|
import { writeFile as fsWriteFile } from "node:fs/promises";
|
|
145919
146641
|
import { spawnSync as nodeSpawnSync2 } from "node:child_process";
|
|
145920
|
-
var accountResolver = new AccountResolver;
|
|
145921
146642
|
var KEYS_HELP = [
|
|
145922
146643
|
"Keyboard shortcuts",
|
|
145923
146644
|
" ⏎ send · ⌃J newline · esc interrupt · ⌃C twice to quit",
|
|
@@ -145984,6 +146705,18 @@ var FALLBACK_CODEX_MODELS = [
|
|
|
145984
146705
|
{ id: "gpt-5.4", label: "gpt-5.4", provider: "codex", efforts: ["low", "medium", "high", "xhigh"] },
|
|
145985
146706
|
{ id: "gpt-5.4-mini", label: "gpt-5.4-mini", provider: "codex", efforts: ["low", "medium", "high", "xhigh"] }
|
|
145986
146707
|
];
|
|
146708
|
+
function shortFailure(message) {
|
|
146709
|
+
const m2 = (message || "").toLowerCase();
|
|
146710
|
+
if (/\b402\b|credit|payment|billing|out of credit/.test(m2))
|
|
146711
|
+
return "out of credit";
|
|
146712
|
+
if (/over(loaded|capacity)|\b529\b/.test(m2))
|
|
146713
|
+
return "overloaded";
|
|
146714
|
+
if (/usage.?limit/.test(m2))
|
|
146715
|
+
return "at its usage limit";
|
|
146716
|
+
if (/quota|insufficient_quota/.test(m2))
|
|
146717
|
+
return "out of quota";
|
|
146718
|
+
return "rate-limited";
|
|
146719
|
+
}
|
|
145987
146720
|
var codexModelCache = null;
|
|
145988
146721
|
function codexCliModels() {
|
|
145989
146722
|
if (codexModelCache)
|
|
@@ -146287,7 +147020,7 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146287
147020
|
setPanelState(p);
|
|
146288
147021
|
};
|
|
146289
147022
|
const panelMaxScrollRef = import_react26.useRef(0);
|
|
146290
|
-
const
|
|
147023
|
+
const panelAccountSlugsRef = import_react26.useRef([]);
|
|
146291
147024
|
const buildPanelModelRows = (cur) => modelRegistry().filter((m2) => providerAvailable(m2.provider)).map((m2) => ({ id: m2.id, label: m2.label, provider: m2.provider, current: m2.id === cur }));
|
|
146292
147025
|
const openInfoPanel = (title, item) => {
|
|
146293
147026
|
if (!fullscreen)
|
|
@@ -146428,6 +147161,43 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146428
147161
|
notice(`loaded the real model list for ${learned} account${learned === 1 ? "" : "s"} — /model to see them`);
|
|
146429
147162
|
})();
|
|
146430
147163
|
}, []);
|
|
147164
|
+
import_react26.useEffect(() => {
|
|
147165
|
+
let cancelled = false;
|
|
147166
|
+
(async () => {
|
|
147167
|
+
const now3 = Date.now();
|
|
147168
|
+
const stale = listAccounts().filter((a) => !isFresh(a.health, now3));
|
|
147169
|
+
await Promise.all(stale.map(async (a) => {
|
|
147170
|
+
try {
|
|
147171
|
+
const h2 = await checkHealth(a);
|
|
147172
|
+
if (cancelled)
|
|
147173
|
+
return;
|
|
147174
|
+
recordHealth(a, h2.state, h2.detail);
|
|
147175
|
+
} catch {}
|
|
147176
|
+
}));
|
|
147177
|
+
})();
|
|
147178
|
+
return () => {
|
|
147179
|
+
cancelled = true;
|
|
147180
|
+
};
|
|
147181
|
+
}, []);
|
|
147182
|
+
import_react26.useEffect(() => {
|
|
147183
|
+
let alive = true;
|
|
147184
|
+
const refresh = async () => {
|
|
147185
|
+
const targets = listAccounts().filter((a) => a.enabled && a.exec !== "cli" && balanceExposed(a.provider));
|
|
147186
|
+
for (const a of targets) {
|
|
147187
|
+
if (!alive)
|
|
147188
|
+
return;
|
|
147189
|
+
const bal = await fetchBalance(a);
|
|
147190
|
+
if (bal?.remainingUSD != null)
|
|
147191
|
+
recordBalance(a.id, bal);
|
|
147192
|
+
}
|
|
147193
|
+
};
|
|
147194
|
+
refresh();
|
|
147195
|
+
const t2 = setInterval(() => void refresh(), 5 * 60000);
|
|
147196
|
+
return () => {
|
|
147197
|
+
alive = false;
|
|
147198
|
+
clearInterval(t2);
|
|
147199
|
+
};
|
|
147200
|
+
}, []);
|
|
146431
147201
|
import_react26.useEffect(() => {
|
|
146432
147202
|
setPermissionHandler((req) => new Promise((resolve13) => {
|
|
146433
147203
|
if (modeRef.current === "auto-accept" && (req.kind === "write" || req.kind === "edit")) {
|
|
@@ -146439,12 +147209,48 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146439
147209
|
}));
|
|
146440
147210
|
return () => setPermissionHandler(null);
|
|
146441
147211
|
}, []);
|
|
146442
|
-
const
|
|
146443
|
-
|
|
146444
|
-
|
|
146445
|
-
|
|
146446
|
-
|
|
147212
|
+
const scrollTargetRef = import_react26.useRef(null);
|
|
147213
|
+
const scrollAnimRef = import_react26.useRef(null);
|
|
147214
|
+
const noMotion = process.env.GEARBOX_NO_MOTION === "1";
|
|
147215
|
+
const stopScrollAnim = import_react26.useCallback(() => {
|
|
147216
|
+
if (scrollAnimRef.current) {
|
|
147217
|
+
clearInterval(scrollAnimRef.current);
|
|
147218
|
+
scrollAnimRef.current = null;
|
|
147219
|
+
}
|
|
146447
147220
|
}, []);
|
|
147221
|
+
const scrollBy = import_react26.useCallback((delta) => {
|
|
147222
|
+
const max2 = maxScrollRef.current;
|
|
147223
|
+
const cur = atBottomRef.current ? max2 : scrollTopRef.current;
|
|
147224
|
+
const target = Math.max(0, Math.min(max2, (scrollTargetRef.current ?? cur) + delta));
|
|
147225
|
+
if (noMotion) {
|
|
147226
|
+
scrollTargetRef.current = null;
|
|
147227
|
+
atBottomRef.current = target >= max2;
|
|
147228
|
+
setScrollTop(target);
|
|
147229
|
+
return;
|
|
147230
|
+
}
|
|
147231
|
+
scrollTargetRef.current = target;
|
|
147232
|
+
if (scrollAnimRef.current)
|
|
147233
|
+
return;
|
|
147234
|
+
let pos = cur;
|
|
147235
|
+
scrollAnimRef.current = setInterval(() => {
|
|
147236
|
+
const m2 = maxScrollRef.current;
|
|
147237
|
+
const tgt = Math.max(0, Math.min(m2, scrollTargetRef.current ?? pos));
|
|
147238
|
+
const diff2 = tgt - pos;
|
|
147239
|
+
if (Math.abs(diff2) < 1) {
|
|
147240
|
+
pos = tgt;
|
|
147241
|
+
scrollTargetRef.current = null;
|
|
147242
|
+
stopScrollAnim();
|
|
147243
|
+
} else {
|
|
147244
|
+
const step = Math.sign(diff2) * Math.max(1, Math.round(Math.abs(diff2) * 0.35));
|
|
147245
|
+
pos += step;
|
|
147246
|
+
if (diff2 > 0 && pos > tgt || diff2 < 0 && pos < tgt)
|
|
147247
|
+
pos = tgt;
|
|
147248
|
+
}
|
|
147249
|
+
atBottomRef.current = pos >= m2;
|
|
147250
|
+
setScrollTop(pos);
|
|
147251
|
+
}, 16);
|
|
147252
|
+
}, [noMotion, stopScrollAnim]);
|
|
147253
|
+
import_react26.useEffect(() => stopScrollAnim, [stopScrollAnim]);
|
|
146448
147254
|
const copyWithFeedback = import_react26.useCallback((text2) => {
|
|
146449
147255
|
const clean = text2.replace(/[ \t]+\n/g, `
|
|
146450
147256
|
`).trim();
|
|
@@ -146681,7 +147487,7 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146681
147487
|
if (p.kind === "static")
|
|
146682
147488
|
setPanel({ ...p, scroll: clampScroll(p.scroll + delta, panelMaxScrollRef.current) });
|
|
146683
147489
|
else if (p.kind === "accounts")
|
|
146684
|
-
setPanel({ ...p, index: clampIndex(p.index + delta,
|
|
147490
|
+
setPanel({ ...p, index: clampIndex(p.index + delta, panelAccountSlugsRef.current.length) });
|
|
146685
147491
|
else
|
|
146686
147492
|
setPanel({ ...p, index: clampIndex(p.index + delta, filterModelRows(buildPanelModelRows(), p.filter).length) });
|
|
146687
147493
|
} else
|
|
@@ -146936,10 +147742,10 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146936
147742
|
const pushUsage = (view) => push({ kind: "usage", id: idRef.current++, view });
|
|
146937
147743
|
const pushAccounts = (view) => push({ kind: "accounts", id: idRef.current++, view });
|
|
146938
147744
|
const normalizeAccountRef = (s2) => s2.toLowerCase().replace(/[()]/g, " ").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
146939
|
-
const accountAliases = (a
|
|
147745
|
+
const accountAliases = (a) => {
|
|
146940
147746
|
const name31 = accountName(a);
|
|
146941
147747
|
const slug3 = accountSlug(a);
|
|
146942
|
-
const aliases = new Set([
|
|
147748
|
+
const aliases = new Set([slug3, normalizeAccountRef(name31), normalizeAccountRef(a.label), normalizeAccountRef(a.id)]);
|
|
146943
147749
|
const nick = name31.match(/\(([^)]+)\)/)?.[1];
|
|
146944
147750
|
if (nick)
|
|
146945
147751
|
aliases.add(normalizeAccountRef(nick));
|
|
@@ -146952,13 +147758,13 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146952
147758
|
const findAccountRef = (query, accounts = listAccounts()) => {
|
|
146953
147759
|
const q = normalizeAccountRef(query);
|
|
146954
147760
|
if (!q)
|
|
146955
|
-
return { error: "which account? use /account <name
|
|
146956
|
-
const exact = accounts.map((a
|
|
147761
|
+
return { error: "which account? use /account <name>" };
|
|
147762
|
+
const exact = accounts.map((a) => ({ a, aliases: accountAliases(a) })).filter(({ aliases }) => aliases.has(q));
|
|
146957
147763
|
if (exact.length === 1)
|
|
146958
147764
|
return { account: exact[0].a };
|
|
146959
147765
|
if (exact.length > 1)
|
|
146960
147766
|
return { error: `"${query}" matches ${exact.map(({ a }) => accountName(a)).join(", ")} — use the full alias` };
|
|
146961
|
-
const fuzzy = accounts.map((a
|
|
147767
|
+
const fuzzy = accounts.map((a) => ({ a, aliases: [...accountAliases(a)] })).filter(({ aliases }) => aliases.some((x2) => x2.includes(q)));
|
|
146962
147768
|
if (fuzzy.length === 1)
|
|
146963
147769
|
return { account: fuzzy[0].a };
|
|
146964
147770
|
if (fuzzy.length > 1)
|
|
@@ -146967,19 +147773,19 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146967
147773
|
};
|
|
146968
147774
|
const buildAccountView = (accounts, activeCliId, importable, statuses) => {
|
|
146969
147775
|
const active = activeCliId ? accounts.find((a) => a.id === activeCliId) : null;
|
|
146970
|
-
const rows2 = accounts.map((a
|
|
147776
|
+
const rows2 = accounts.map((a) => {
|
|
146971
147777
|
const st = statuses[a.id];
|
|
146972
147778
|
const activeRow = a.id === activeCliId;
|
|
146973
|
-
const status = activeRow ? "active" : st?.duplicateOf ? "duplicate" : st?.signedIn === false ? "not signed in" : st?.signedIn === true ? "signed in" : a.exec === "cli" ? "not checked" :
|
|
147779
|
+
const status = activeRow ? "active" : st?.duplicateOf ? "duplicate" : st?.signedIn === false ? "not signed in" : st?.signedIn === true ? "signed in" : a.exec === "cli" ? "not checked" : badgeFor(a.health?.state);
|
|
146974
147780
|
return {
|
|
146975
147781
|
name: accountName(a),
|
|
146976
147782
|
type: a.exec === "cli" ? "subscription" : "API key",
|
|
146977
147783
|
status,
|
|
146978
147784
|
active: activeRow,
|
|
146979
147785
|
alias: accountSlug(a),
|
|
146980
|
-
|
|
146981
|
-
|
|
146982
|
-
|
|
147786
|
+
detail: (st?.signedIn ? st.detail : undefined) ?? a.identity?.label,
|
|
147787
|
+
duplicateOf: st?.duplicateOf,
|
|
147788
|
+
health: a.health?.state
|
|
146983
147789
|
};
|
|
146984
147790
|
});
|
|
146985
147791
|
return {
|
|
@@ -146990,7 +147796,77 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
146990
147796
|
statusPad: Math.max(6, ...rows2.map((r2) => r2.status.length))
|
|
146991
147797
|
};
|
|
146992
147798
|
};
|
|
147799
|
+
const refreshCliStatuses = import_react26.useCallback(async () => {
|
|
147800
|
+
const accounts = listAccounts().filter((a) => a.exec === "cli");
|
|
147801
|
+
const statuses = { ...accountStatusCacheRef.current };
|
|
147802
|
+
await Promise.all(accounts.map(async (a) => {
|
|
147803
|
+
const bin = a.auth.binary;
|
|
147804
|
+
const profile = a.auth.loginProfile;
|
|
147805
|
+
try {
|
|
147806
|
+
const st = await cliAuthStatus(bin, profile);
|
|
147807
|
+
statuses[a.id] = { signedIn: st.loggedIn, detail: st.detail, identity: st.identity };
|
|
147808
|
+
if (st.loggedIn && st.identityLabel) {
|
|
147809
|
+
putAccount({ ...a, identity: { key: st.identity ?? a.id, label: st.identityLabel, checkedAt: Date.now() } });
|
|
147810
|
+
}
|
|
147811
|
+
} catch {}
|
|
147812
|
+
}));
|
|
147813
|
+
accountStatusCacheRef.current = statuses;
|
|
147814
|
+
}, []);
|
|
146993
147815
|
const askModeRef = import_react26.useRef(false);
|
|
147816
|
+
const runCliBackend = import_react26.useCallback(async (args) => {
|
|
147817
|
+
const { binary, profile, modelId, accountId, efforts, label, pinned, prompt, messages, onEvent, signal } = args;
|
|
147818
|
+
usedAccountRef.current = accountId;
|
|
147819
|
+
const detail = pinned ? `${binary}${label ? ` · ${label}` : ""} owns tools and permissions` : `${binary}${label ? ` · ${label}` : ""} subscription · seat (~free) owns tools and permissions`;
|
|
147820
|
+
onEvent({ type: "phase", label: "using subscription", detail, state: "running" });
|
|
147821
|
+
const _cliEffortRaw = normalizeEffort(effortRef.current, efforts);
|
|
147822
|
+
if (_cliEffortRaw === null && effortRef.current !== "medium") {
|
|
147823
|
+
const { level: nearest } = clampEffort(effortRef.current, efforts);
|
|
147824
|
+
const hint = efforts.length ? ` — try /effort ${nearest}` : "";
|
|
147825
|
+
throw new Error(`effort "${effortRef.current}" is not supported by ${label ?? binary} (supports: ${efforts.join(", ") || "none"}${hint})`);
|
|
147826
|
+
}
|
|
147827
|
+
const cliEffort = _cliEffortRaw ?? undefined;
|
|
147828
|
+
const activeAccount = getAccount(accountId);
|
|
147829
|
+
const activeName = activeAccount ? accountName(activeAccount).match(/\((.*)\)/)?.[1] : undefined;
|
|
147830
|
+
const reloginCommand = binary.includes("codex") ? `/account add codex${activeName ? ` ${activeName}` : ""}` : `/account add claude${activeName ? ` ${activeName}` : ""}`;
|
|
147831
|
+
let cliPrompt = prompt;
|
|
147832
|
+
if (!cliSessionRef.current) {
|
|
147833
|
+
try {
|
|
147834
|
+
const cwd2 = process.cwd();
|
|
147835
|
+
const allFiles = listProjectFiles(cwd2).slice(0, 300);
|
|
147836
|
+
const map4 = repoMap(cwd2, 3000);
|
|
147837
|
+
const fileList = allFiles.join(`
|
|
147838
|
+
`);
|
|
147839
|
+
cliPrompt = `<project-context cwd="${cwd2}">
|
|
147840
|
+
` + `<files>
|
|
147841
|
+
${fileList}
|
|
147842
|
+
</files>
|
|
147843
|
+
` + (map4 ? `<signatures>
|
|
147844
|
+
${map4}
|
|
147845
|
+
</signatures>
|
|
147846
|
+
` : "") + `</project-context>
|
|
147847
|
+
|
|
147848
|
+
` + prompt;
|
|
147849
|
+
} catch {}
|
|
147850
|
+
}
|
|
147851
|
+
const r2 = await runCliTask({
|
|
147852
|
+
binary,
|
|
147853
|
+
prompt: cliPrompt,
|
|
147854
|
+
messages,
|
|
147855
|
+
onEvent,
|
|
147856
|
+
signal,
|
|
147857
|
+
sessionId: cliSessionRef.current,
|
|
147858
|
+
autoApprove: isYolo(),
|
|
147859
|
+
profile,
|
|
147860
|
+
modelId,
|
|
147861
|
+
effort: cliEffort,
|
|
147862
|
+
accountLabel: activeAccount ? accountLabel(activeAccount) : accountId,
|
|
147863
|
+
reloginCommand,
|
|
147864
|
+
deferTerminal: args.deferTerminal
|
|
147865
|
+
});
|
|
147866
|
+
cliSessionRef.current = r2.sessionId ?? cliSessionRef.current;
|
|
147867
|
+
cliMetaRef.current = { costUSD: r2.costUSD, rates: r2.rates };
|
|
147868
|
+
return { messages: r2.messages, usage: r2.usage, failure: r2.failure };
|
|
147869
|
+
}, []);
|
|
146994
147870
|
const defaultRunner = import_react26.useCallback(async ({ prompt, messages, onEvent, selector: sel, signal }) => {
|
|
146995
147871
|
const isAsk = askModeRef.current;
|
|
146996
147872
|
askModeRef.current = false;
|
|
@@ -147001,115 +147877,146 @@ function App2({ selector: initialSelector, runner, fullscreen = false, resumeId
|
|
|
147001
147877
|
return { messages, usage: { inputTokens: 0, outputTokens: 0 } };
|
|
147002
147878
|
}
|
|
147003
147879
|
const choice3 = new RoutingSelector().select({ prompt, kind: "search" });
|
|
147880
|
+
if (choice3.backend?.kind === "cli") {
|
|
147881
|
+
onEvent({ type: "error", message: "/ask needs an API-key account — it can't run on a subscription. Add one with /account add <key>." });
|
|
147882
|
+
return { messages, usage: { inputTokens: 0, outputTokens: 0 } };
|
|
147883
|
+
}
|
|
147004
147884
|
routedRef.current = { model: choice3.model, reason: choice3.reason };
|
|
147005
147885
|
onEvent({ type: "model-pick", model: choice3.model.label, provider: choice3.model.provider, reason: choice3.reason });
|
|
147006
|
-
const acct =
|
|
147007
|
-
const
|
|
147886
|
+
const acct = choice3.backend?.kind === "in-loop" && choice3.backend.account || defaultAccount(choice3.model.provider);
|
|
147887
|
+
const creds = acct ? await resolveCreds(acct) : undefined;
|
|
147008
147888
|
usedAccountRef.current = acct?.id ?? null;
|
|
147009
147889
|
cliMetaRef.current = null;
|
|
147010
147890
|
if (acct)
|
|
147011
147891
|
markUsed(acct.id);
|
|
147012
|
-
const
|
|
147013
|
-
return { messages, usage:
|
|
147892
|
+
const r2 = await runCompletion({ model: choice3.model, system: buildAskSystem(docs), prompt, onEvent, signal, creds });
|
|
147893
|
+
return { messages, usage: r2.usage };
|
|
147014
147894
|
}
|
|
147015
|
-
const
|
|
147016
|
-
|
|
147017
|
-
|
|
147018
|
-
|
|
147019
|
-
|
|
147020
|
-
|
|
147021
|
-
|
|
147022
|
-
|
|
147023
|
-
|
|
147895
|
+
const imagesPresent = activeImagesRef.current.length > 0;
|
|
147896
|
+
const cliImageGuard = () => {
|
|
147897
|
+
onEvent({
|
|
147898
|
+
type: "error",
|
|
147899
|
+
message: "image attachments need an API-backed model in Gearbox right now. Use `/account off` or an API-key account, then retry with the image path."
|
|
147900
|
+
});
|
|
147901
|
+
return { messages, usage: { inputTokens: 0, outputTokens: 0 } };
|
|
147902
|
+
};
|
|
147903
|
+
const pin = activeCliRef.current;
|
|
147904
|
+
if (pin) {
|
|
147905
|
+
if (imagesPresent)
|
|
147906
|
+
return cliImageGuard();
|
|
147024
147907
|
routedRef.current = null;
|
|
147025
|
-
|
|
147026
|
-
const
|
|
147027
|
-
|
|
147028
|
-
|
|
147029
|
-
|
|
147030
|
-
|
|
147031
|
-
|
|
147032
|
-
|
|
147033
|
-
|
|
147034
|
-
|
|
147035
|
-
|
|
147036
|
-
|
|
147037
|
-
const cliEffort = _cliEffortRaw ?? undefined;
|
|
147038
|
-
const activeAccount = getAccount(cli.id);
|
|
147039
|
-
const activeName = activeAccount ? accountName(activeAccount).match(/\((.*)\)/)?.[1] : undefined;
|
|
147040
|
-
const reloginCommand = cli.binary.includes("codex") ? `/account add codex${activeName ? ` ${activeName}` : ""}` : `/account add claude${activeName ? ` ${activeName}` : ""}`;
|
|
147041
|
-
let cliPrompt = prompt;
|
|
147042
|
-
if (!cliSessionRef.current) {
|
|
147043
|
-
try {
|
|
147044
|
-
const cwd2 = process.cwd();
|
|
147045
|
-
const allFiles = listProjectFiles(cwd2).slice(0, 300);
|
|
147046
|
-
const map4 = repoMap(cwd2, 3000);
|
|
147047
|
-
const fileList = allFiles.join(`
|
|
147048
|
-
`);
|
|
147049
|
-
cliPrompt = `<project-context cwd="${cwd2}">
|
|
147050
|
-
` + `<files>
|
|
147051
|
-
${fileList}
|
|
147052
|
-
</files>
|
|
147053
|
-
` + (map4 ? `<signatures>
|
|
147054
|
-
${map4}
|
|
147055
|
-
</signatures>
|
|
147056
|
-
` : "") + `</project-context>
|
|
147057
|
-
|
|
147058
|
-
` + prompt;
|
|
147059
|
-
} catch {}
|
|
147060
|
-
}
|
|
147061
|
-
const r3 = await runCliTask({
|
|
147062
|
-
binary: cli.binary,
|
|
147063
|
-
prompt: cliPrompt,
|
|
147908
|
+
cliMetaRef.current = null;
|
|
147909
|
+
const choices = cliModelChoices(pin.binary);
|
|
147910
|
+
const cliChoice = choices.find((m2) => m2.id === activeCliModelRef.current) ?? choices[0];
|
|
147911
|
+
return runCliBackend({
|
|
147912
|
+
binary: pin.binary,
|
|
147913
|
+
profile: pin.profile,
|
|
147914
|
+
modelId: activeCliModelRef.current,
|
|
147915
|
+
accountId: pin.id,
|
|
147916
|
+
efforts: cliChoice?.efforts ?? [],
|
|
147917
|
+
label: cliModelLabel(activeCliModelRef.current) || undefined,
|
|
147918
|
+
pinned: true,
|
|
147919
|
+
prompt,
|
|
147064
147920
|
messages,
|
|
147065
147921
|
onEvent,
|
|
147066
|
-
signal
|
|
147067
|
-
sessionId: cliSessionRef.current,
|
|
147068
|
-
autoApprove: isYolo(),
|
|
147069
|
-
profile: cli.profile,
|
|
147070
|
-
modelId: activeCliModelRef.current,
|
|
147071
|
-
effort: cliEffort,
|
|
147072
|
-
accountLabel: activeAccount ? accountLabel(activeAccount) : cli.id,
|
|
147073
|
-
reloginCommand
|
|
147922
|
+
signal
|
|
147074
147923
|
});
|
|
147075
|
-
cliSessionRef.current = r3.sessionId ?? cliSessionRef.current;
|
|
147076
|
-
cliMetaRef.current = { costUSD: r3.costUSD, rates: r3.rates };
|
|
147077
|
-
return { messages: r3.messages, usage: r3.usage };
|
|
147078
147924
|
}
|
|
147079
147925
|
const plan = modeRef.current === "plan";
|
|
147080
|
-
const requires = ["tools", ...
|
|
147081
|
-
const
|
|
147082
|
-
|
|
147083
|
-
|
|
147084
|
-
|
|
147085
|
-
|
|
147086
|
-
|
|
147087
|
-
|
|
147088
|
-
|
|
147089
|
-
|
|
147090
|
-
|
|
147091
|
-
|
|
147092
|
-
|
|
147093
|
-
|
|
147094
|
-
|
|
147095
|
-
|
|
147096
|
-
|
|
147097
|
-
|
|
147098
|
-
|
|
147099
|
-
|
|
147100
|
-
|
|
147101
|
-
|
|
147102
|
-
|
|
147103
|
-
|
|
147104
|
-
|
|
147105
|
-
|
|
147106
|
-
|
|
147107
|
-
|
|
147108
|
-
|
|
147926
|
+
const requires = ["tools", ...imagesPresent ? ["images"] : []];
|
|
147927
|
+
const emitTerminal = (errored, message, usage) => {
|
|
147928
|
+
if (errored && message)
|
|
147929
|
+
onEvent({ type: "error", message });
|
|
147930
|
+
onEvent({ type: "phase", label: errored ? "blocked" : "finished", state: errored ? "err" : "ok" });
|
|
147931
|
+
onEvent({ type: "done", usage });
|
|
147932
|
+
};
|
|
147933
|
+
const runAttempt = async (choice3) => {
|
|
147934
|
+
if (choice3.backend?.kind === "cli") {
|
|
147935
|
+
const acct = choice3.backend.account;
|
|
147936
|
+
if (imagesPresent)
|
|
147937
|
+
return { ...cliImageGuard(), failure: { message: "image attachments need an API-backed model" }, cooldownKey: acct.id };
|
|
147938
|
+
routedRef.current = { model: choice3.model, reason: choice3.reason };
|
|
147939
|
+
setLastPick({ model: choice3.model, reason: choice3.reason });
|
|
147940
|
+
onEvent({ type: "model-pick", model: choice3.model.label, provider: choice3.model.provider, reason: choice3.reason });
|
|
147941
|
+
const out = await runCliBackend({
|
|
147942
|
+
binary: choice3.backend.binary,
|
|
147943
|
+
profile: choice3.backend.profile,
|
|
147944
|
+
modelId: choice3.model.sdkId,
|
|
147945
|
+
accountId: acct.id,
|
|
147946
|
+
efforts: choice3.model.efforts ?? [],
|
|
147947
|
+
label: choice3.model.label,
|
|
147948
|
+
pinned: false,
|
|
147949
|
+
deferTerminal: true,
|
|
147950
|
+
prompt,
|
|
147951
|
+
messages,
|
|
147952
|
+
onEvent,
|
|
147953
|
+
signal
|
|
147954
|
+
});
|
|
147955
|
+
return { messages: out.messages, usage: out.usage, failure: out.failure, cooldownKey: acct.id };
|
|
147956
|
+
}
|
|
147957
|
+
const missing = missingRequirements(choice3.model, requires);
|
|
147958
|
+
if (missing.length)
|
|
147959
|
+
throw new Error(`${choice3.model.label} cannot run this turn (${missing.join(", ")} unsupported). Use /model auto or pick a compatible model.`);
|
|
147960
|
+
routedRef.current = { model: choice3.model, reason: choice3.reason };
|
|
147961
|
+
setLastPick({ model: choice3.model, reason: choice3.reason });
|
|
147962
|
+
onEvent({ type: "model-pick", model: choice3.model.label, provider: choice3.model.provider, reason: choice3.reason });
|
|
147963
|
+
onEvent({ type: "phase", label: "building context", detail: choice3.model.label, state: "running" });
|
|
147964
|
+
const userContent = imageContent(prompt, activeImagesRef.current);
|
|
147965
|
+
const { system, messages: ctx } = buildContext({ history: messages, userText: prompt, userContent, model: choice3.model, plan });
|
|
147966
|
+
const account = choice3.backend?.kind === "in-loop" && choice3.backend.account || defaultAccount(choice3.model.provider);
|
|
147967
|
+
const creds = account ? await resolveCreds(account) : undefined;
|
|
147968
|
+
usedAccountRef.current = account?.id ?? null;
|
|
147969
|
+
cliMetaRef.current = null;
|
|
147970
|
+
if (account)
|
|
147971
|
+
markUsed(account.id);
|
|
147972
|
+
const _effortRaw = normalizeEffort(effortRef.current, effortLevels(choice3.model));
|
|
147973
|
+
if (_effortRaw === null && effortRef.current !== "medium") {
|
|
147974
|
+
const supported = effortLevels(choice3.model);
|
|
147975
|
+
const { level: nearest } = clampEffort(effortRef.current, supported);
|
|
147976
|
+
const hint = supported.length ? ` — try /effort ${nearest}` : "";
|
|
147977
|
+
throw new Error(`effort "${effortRef.current}" is not supported by ${choice3.model.label} (supports: ${supported.join(", ") || "none"}${hint})`);
|
|
147978
|
+
}
|
|
147979
|
+
const r2 = await runTask({ model: choice3.model, messages: ctx, onEvent, signal, plan, system, creds, effort: _effortRaw ?? undefined, deferTerminal: true });
|
|
147980
|
+
if (account && r2.headers) {
|
|
147981
|
+
const apiRates = parseRateHeaders(account.provider, r2.headers, Date.now());
|
|
147982
|
+
if (apiRates.length)
|
|
147983
|
+
cliMetaRef.current = { costUSD: undefined, rates: apiRates };
|
|
147984
|
+
}
|
|
147985
|
+
const produced = r2.messages.slice(ctx.length);
|
|
147986
|
+
const imageNote = activeImagesRef.current.length ? `
|
|
147109
147987
|
|
|
147110
147988
|
[Attached images: ${activeImagesRef.current.map((img) => basename3(img.path)).join(", ")}]` : "";
|
|
147111
|
-
|
|
147112
|
-
|
|
147989
|
+
const ledger = sanitizeToolPairs([...messages, { role: "user", content: prompt + imageNote }, ...produced]);
|
|
147990
|
+
return { messages: ledger, usage: r2.usage, failure: r2.failure, cooldownKey: account?.id ?? `env:${choice3.model.provider}` };
|
|
147991
|
+
};
|
|
147992
|
+
const MAX_FAILOVERS = 2;
|
|
147993
|
+
let choice2 = sel.select({ prompt, kind: plan ? "plan" : undefined, requires });
|
|
147994
|
+
for (let hop = 0;; hop++) {
|
|
147995
|
+
const a = await runAttempt(choice2);
|
|
147996
|
+
if (!a.failure) {
|
|
147997
|
+
emitTerminal(false, undefined, a.usage);
|
|
147998
|
+
return { messages: a.messages, usage: a.usage };
|
|
147999
|
+
}
|
|
148000
|
+
const exhausted = classifyFailure(a.failure.message) === "exhausted";
|
|
148001
|
+
if (!exhausted || hop >= MAX_FAILOVERS) {
|
|
148002
|
+
emitTerminal(true, a.failure.message, a.usage);
|
|
148003
|
+
return { messages: a.messages, usage: a.usage };
|
|
148004
|
+
}
|
|
148005
|
+
markExhausted(a.cooldownKey, DEFAULT_COOLDOWN_MS, a.failure.message);
|
|
148006
|
+
let next = null;
|
|
148007
|
+
try {
|
|
148008
|
+
next = sel.select({ prompt, kind: plan ? "plan" : undefined, requires });
|
|
148009
|
+
} catch {
|
|
148010
|
+
next = null;
|
|
148011
|
+
}
|
|
148012
|
+
const nextKey = next?.backend?.kind === "cli" ? next.backend.account.id : next?.backend?.kind === "in-loop" && next.backend.account ? next.backend.account.id : next ? `env:${next.model.provider}` : null;
|
|
148013
|
+
if (!next || nextKey === a.cooldownKey) {
|
|
148014
|
+
emitTerminal(true, a.failure.message, a.usage);
|
|
148015
|
+
return { messages: a.messages, usage: a.usage };
|
|
148016
|
+
}
|
|
148017
|
+
onEvent({ type: "phase", label: "failover", detail: `${choice2.model.label} ${shortFailure(a.failure.message)} → ${next.model.label}, continuing`, state: "running" });
|
|
148018
|
+
choice2 = next;
|
|
148019
|
+
}
|
|
147113
148020
|
}, []);
|
|
147114
148021
|
const compactNow = import_react26.useCallback(async (keepRecent, signal) => {
|
|
147115
148022
|
let model2;
|
|
@@ -147580,17 +148487,39 @@ ${fetched.join(`
|
|
|
147580
148487
|
} else {
|
|
147581
148488
|
accountStatusCacheRef.current[acctId] = { signedIn: true, detail: st.detail };
|
|
147582
148489
|
}
|
|
147583
|
-
activeCliRef.current = { id: acctId, binary: bin, profile };
|
|
147584
148490
|
cliSessionRef.current = undefined;
|
|
147585
148491
|
setLastPick(null);
|
|
147586
|
-
|
|
147587
|
-
|
|
147588
|
-
|
|
148492
|
+
const otherUsable = listAccounts().some((a) => a.enabled && a.id !== acctId) || [process.env.ANTHROPIC_API_KEY, process.env.OPENAI_API_KEY, process.env.GOOGLE_GENERATIVE_AI_API_KEY, process.env.DEEPSEEK_API_KEY].some(Boolean);
|
|
148493
|
+
if (otherUsable) {
|
|
148494
|
+
activeCliRef.current = null;
|
|
148495
|
+
setActiveCli(null);
|
|
148496
|
+
updatePrefs({ activeAccount: null });
|
|
148497
|
+
updatePhase(phaseId, "ok", `${accountLabel(res.account)} added`, `routing will prefer this seat (~free) and fall back to your API keys at its limit. /account ${accountSlug(res.account)} to pin it; /account off anytime.`);
|
|
148498
|
+
} else {
|
|
148499
|
+
activeCliRef.current = { id: acctId, binary: bin, profile };
|
|
148500
|
+
setActiveCli({ id: acctId, label: shortLabel });
|
|
148501
|
+
updatePrefs({ activeAccount: acctId });
|
|
148502
|
+
updatePhase(phaseId, "ok", `${accountLabel(res.account)} active`, `using ${bin}${st.detail ? `; ${st.detail}` : ""}. Own tools/permissions; /account off returns to API routing`);
|
|
148503
|
+
}
|
|
147589
148504
|
} catch (e2) {
|
|
147590
148505
|
updatePhase(phaseId, "err", `${accountLabel(res.account)} sign-in`, e2?.message ?? String(e2));
|
|
147591
148506
|
}
|
|
147592
148507
|
})();
|
|
147593
148508
|
};
|
|
148509
|
+
const reloginByRef = (arg2) => {
|
|
148510
|
+
const ref = findAccountRef(arg2, listAccounts());
|
|
148511
|
+
const a = ref.account ?? (activeCliRef.current ? getAccount(activeCliRef.current.id) : undefined);
|
|
148512
|
+
if (a && a.exec === "cli") {
|
|
148513
|
+
const nick = accountName(a).match(/\((.*)\)/)?.[1];
|
|
148514
|
+
signInCli(`${a.provider.replace(/-cli$/, "")}${nick ? ` ${nick}` : ""}`.trim());
|
|
148515
|
+
return;
|
|
148516
|
+
}
|
|
148517
|
+
if (a && a.exec !== "cli") {
|
|
148518
|
+
notice(`${accountName(a)} is an API-key account — nothing to re-login. Use /account ${accountSlug(a)} to switch to it, or /account add ${a.provider} <key> to replace the key.`);
|
|
148519
|
+
return;
|
|
148520
|
+
}
|
|
148521
|
+
signInCli(arg2);
|
|
148522
|
+
};
|
|
147594
148523
|
try {
|
|
147595
148524
|
switch (name31) {
|
|
147596
148525
|
case "exit":
|
|
@@ -147878,6 +148807,33 @@ ${fetched.join(`
|
|
|
147878
148807
|
notice(`remembered: prefer ${pref.modelId} for ${pref.kind} tasks`);
|
|
147879
148808
|
return;
|
|
147880
148809
|
}
|
|
148810
|
+
case "budget": {
|
|
148811
|
+
echo(text2);
|
|
148812
|
+
const parts = arg.split(/\s+/).filter(Boolean);
|
|
148813
|
+
if (parts.length === 0) {
|
|
148814
|
+
const b = loadBudgets();
|
|
148815
|
+
const keys2 = Object.keys(b);
|
|
148816
|
+
notice(keys2.length ? `budgets (estimates remaining = budget − tracked spend):
|
|
148817
|
+
` + keys2.map((k) => ` ${k}: $${b[k].amountUSD} ${b[k].period}`).join(`
|
|
148818
|
+
`) : "no budgets set. /budget <provider|account> <amount> [monthly|total] — lets routing estimate remaining credit for providers that don't expose a balance");
|
|
148819
|
+
return;
|
|
148820
|
+
}
|
|
148821
|
+
const [target, amountRaw, periodRaw] = parts;
|
|
148822
|
+
if (amountRaw && /^off$/i.test(amountRaw)) {
|
|
148823
|
+
setBudget(target, null);
|
|
148824
|
+
notice(`cleared budget for ${target}`);
|
|
148825
|
+
return;
|
|
148826
|
+
}
|
|
148827
|
+
const amount = Number(amountRaw);
|
|
148828
|
+
if (!target || !amountRaw || !Number.isFinite(amount) || amount <= 0) {
|
|
148829
|
+
notice("usage: /budget <provider|account> <amountUSD> [monthly|total] · /budget <target> off");
|
|
148830
|
+
return;
|
|
148831
|
+
}
|
|
148832
|
+
const period = periodRaw && /^total$/i.test(periodRaw) ? "total" : "monthly";
|
|
148833
|
+
setBudget(target, { amountUSD: amount, period });
|
|
148834
|
+
notice(`budget set: ${target} → $${amount} ${period}. Routing will preserve it as it runs low (estimated from your spend).`);
|
|
148835
|
+
return;
|
|
148836
|
+
}
|
|
147881
148837
|
case "memory": {
|
|
147882
148838
|
if (arg) {
|
|
147883
148839
|
echo(text2);
|
|
@@ -147916,6 +148872,21 @@ ${fetched.join(`
|
|
|
147916
148872
|
push(it);
|
|
147917
148873
|
return;
|
|
147918
148874
|
}
|
|
148875
|
+
case "why": {
|
|
148876
|
+
echo(text2);
|
|
148877
|
+
const sel = selectorRef.current;
|
|
148878
|
+
if (!sel.explain) {
|
|
148879
|
+
notice("routing is off — a model or subscription is pinned. Use /model auto to route per task, then /why.");
|
|
148880
|
+
return;
|
|
148881
|
+
}
|
|
148882
|
+
try {
|
|
148883
|
+
const card = sel.explain({ prompt: lastPromptRef.current || "(your next message)", kind: modeRef.current === "plan" ? "plan" : undefined });
|
|
148884
|
+
push({ kind: "scorecard", id: idRef.current++, card });
|
|
148885
|
+
} catch (e2) {
|
|
148886
|
+
notice(e2?.message ?? "couldn't build the scorecard");
|
|
148887
|
+
}
|
|
148888
|
+
return;
|
|
148889
|
+
}
|
|
147919
148890
|
case "onboard": {
|
|
147920
148891
|
echo(text2);
|
|
147921
148892
|
if (arg.trim().toLowerCase() === "providers") {
|
|
@@ -147979,6 +148950,10 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
147979
148950
|
case "account": {
|
|
147980
148951
|
if (!arg.trim() && fullscreen) {
|
|
147981
148952
|
setPanel({ kind: "accounts", title: "accounts · ⏎ to switch", index: 0 });
|
|
148953
|
+
refreshCliStatuses().then(() => {
|
|
148954
|
+
if (panelRef.current?.kind === "accounts")
|
|
148955
|
+
setPanel({ ...panelRef.current });
|
|
148956
|
+
});
|
|
147982
148957
|
return;
|
|
147983
148958
|
}
|
|
147984
148959
|
echo(text2);
|
|
@@ -148016,17 +148991,20 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148016
148991
|
try {
|
|
148017
148992
|
const fresh = listAccounts();
|
|
148018
148993
|
const statuses = await checkCliAccounts(fresh);
|
|
148019
|
-
|
|
148994
|
+
await Promise.all(fresh.filter((a) => a.exec !== "cli" && !isFresh(a.health, Date.now())).map(async (a) => {
|
|
148995
|
+
try {
|
|
148996
|
+
const h2 = await checkHealth(a);
|
|
148997
|
+
recordHealth(a, h2.state, h2.detail);
|
|
148998
|
+
} catch {}
|
|
148999
|
+
}));
|
|
149000
|
+
const withHealth = listAccounts();
|
|
149001
|
+
pushAccounts(buildAccountView(withHealth, activeCliRef.current?.id ?? null, importableEnvCreds(), statuses));
|
|
148020
149002
|
} catch (e2) {
|
|
148021
149003
|
notice(`couldn't check subscription accounts — ${e2?.message ?? String(e2)}`);
|
|
148022
149004
|
pushAccounts(buildAccountView(listAccounts(), activeCliRef.current?.id ?? null, importableEnvCreds(), accountStatusCacheRef.current));
|
|
148023
149005
|
}
|
|
148024
149006
|
})();
|
|
148025
149007
|
};
|
|
148026
|
-
const byNumber = (s2) => {
|
|
148027
|
-
const n = Number(s2);
|
|
148028
|
-
return Number.isInteger(n) && n >= 1 && n <= all.length ? all[n - 1] : undefined;
|
|
148029
|
-
};
|
|
148030
149008
|
const activate = (a) => {
|
|
148031
149009
|
if (a.exec === "cli") {
|
|
148032
149010
|
const bin = a.auth.binary;
|
|
@@ -148079,18 +149057,11 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148079
149057
|
notice("left the subscription — back to your API keys");
|
|
148080
149058
|
return;
|
|
148081
149059
|
}
|
|
148082
|
-
|
|
148083
|
-
|
|
148084
|
-
activate(numbered);
|
|
148085
|
-
return;
|
|
148086
|
-
}
|
|
148087
|
-
if (/^\d+$/.test(subL)) {
|
|
148088
|
-
notice(all.length ? `there's no account ${subL} — pick 1–${all.length}.
|
|
148089
|
-
|
|
148090
|
-
` + formatAccounts(all, activeId, []) : "no accounts yet — /account add to add one");
|
|
149060
|
+
if (subL === "login") {
|
|
149061
|
+
reloginByRef(parts.slice(1).join(" "));
|
|
148091
149062
|
return;
|
|
148092
149063
|
}
|
|
148093
|
-
if (!["add", "remove", "rm", "import", "off", "refresh"].includes(subL)) {
|
|
149064
|
+
if (!["add", "remove", "rm", "import", "off", "login", "refresh"].includes(subL)) {
|
|
148094
149065
|
const ref = findAccountRef(arg, all);
|
|
148095
149066
|
if (ref.account) {
|
|
148096
149067
|
activate(ref.account);
|
|
@@ -148136,14 +149107,12 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148136
149107
|
res = await addOpenAICompatAccount(parts[2] ?? "", parts[3] ?? "", parts[4] ?? "", parts.slice(5));
|
|
148137
149108
|
} else if (catalogProvider(first)?.authKind === "openai-compat" && !catalogProvider(first)?.baseUrl && /^https?:\/\//i.test(parts[2] ?? "")) {
|
|
148138
149109
|
res = await addOpenAICompatAccount(first, parts[2] ?? "", parts[3] ?? "", parts.slice(4));
|
|
149110
|
+
} else if (["bedrock", "aws"].includes(first)) {
|
|
149111
|
+
res = await addBedrockAccount(parts[2] ?? "", parts[3] ?? "", parts[4] ?? "");
|
|
148139
149112
|
} else if (provGiven)
|
|
148140
149113
|
res = await addApiKeyAccount(provGiven, keyVal);
|
|
148141
|
-
else
|
|
149114
|
+
else
|
|
148142
149115
|
res = await addByPastedKey(key);
|
|
148143
|
-
else {
|
|
148144
|
-
notice(`"${key}" isn't a recognized key. Try /account add claude, /account add codex, or paste a full API key.`);
|
|
148145
|
-
return;
|
|
148146
|
-
}
|
|
148147
149116
|
if (!res.ok || !res.account) {
|
|
148148
149117
|
notice(res.message);
|
|
148149
149118
|
return;
|
|
@@ -148224,7 +149193,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148224
149193
|
}
|
|
148225
149194
|
case "login": {
|
|
148226
149195
|
echo(text2);
|
|
148227
|
-
|
|
149196
|
+
reloginByRef(arg);
|
|
148228
149197
|
return;
|
|
148229
149198
|
}
|
|
148230
149199
|
case "cost":
|
|
@@ -148518,23 +149487,17 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148518
149487
|
return;
|
|
148519
149488
|
}
|
|
148520
149489
|
if (p.kind === "accounts") {
|
|
148521
|
-
const
|
|
148522
|
-
const n =
|
|
149490
|
+
const slugs = panelAccountSlugsRef.current;
|
|
149491
|
+
const n = slugs.length;
|
|
148523
149492
|
if (key.upArrow)
|
|
148524
149493
|
setPanel({ ...p, index: clampIndex(p.index - 1, n) });
|
|
148525
149494
|
else if (key.downArrow)
|
|
148526
149495
|
setPanel({ ...p, index: clampIndex(p.index + 1, n) });
|
|
148527
149496
|
else if (key.return) {
|
|
148528
|
-
const
|
|
149497
|
+
const slug3 = slugs[clampIndex(p.index, n)];
|
|
148529
149498
|
setPanel(null);
|
|
148530
|
-
if (
|
|
148531
|
-
handleCommand(`/account ${
|
|
148532
|
-
} else if (/^[1-9]$/.test(input)) {
|
|
148533
|
-
const k = Number(input);
|
|
148534
|
-
if (k <= n) {
|
|
148535
|
-
setPanel(null);
|
|
148536
|
-
handleCommand(`/account ${k}`);
|
|
148537
|
-
}
|
|
149499
|
+
if (slug3)
|
|
149500
|
+
handleCommand(`/account ${slug3}`);
|
|
148538
149501
|
}
|
|
148539
149502
|
return;
|
|
148540
149503
|
}
|
|
@@ -148820,7 +149783,7 @@ Example: /mcp add github npx -y @modelcontextprotocol/server-github`);
|
|
|
148820
149783
|
panelMaxScrollRef.current = Math.max(0, panelStaticLines.length - panelBodyHeight(transcriptHeight));
|
|
148821
149784
|
} else if (panel?.kind === "accounts") {
|
|
148822
149785
|
panelAccountView = buildAccountView(listAccounts(), activeCliRef.current?.id ?? null, importableEnvCreds(), accountStatusCacheRef.current);
|
|
148823
|
-
|
|
149786
|
+
panelAccountSlugsRef.current = panelAccountView.rows.map((r2) => r2.alias);
|
|
148824
149787
|
} else if (panel?.kind === "models") {
|
|
148825
149788
|
panelModels = buildPanelModelRows(panelCurrentModel);
|
|
148826
149789
|
}
|
|
@@ -149276,7 +150239,7 @@ async function runCliOnboarding() {
|
|
|
149276
150239
|
const { importableEnvCreds: importableEnvCreds2, importEnvCred: importEnvCred2, importableCloudCreds: importableCloudCreds2, importCloudCred: importCloudCred2 } = await Promise.resolve().then(() => (init_detect(), exports_detect));
|
|
149277
150240
|
const { addApiKeyAccount: addApiKeyAccount2, addAzureAccount: addAzureAccount2, addAzureFoundryAccount: addAzureFoundryAccount2, addByPastedKey: addByPastedKey2, testAccount: testAccount2, addableProviders: addableProviders2, addCliAccount: addCliAccount2, cliAuthStatus: cliAuthStatus2, cliLoginArgs: cliLoginArgs2 } = await Promise.resolve().then(() => (init_onboard(), exports_onboard));
|
|
149278
150241
|
const { subscriptionEnv: subscriptionEnv2 } = await Promise.resolve().then(() => (init_cli_backend(), exports_cli_backend));
|
|
149279
|
-
const { detectProviderByKey:
|
|
150242
|
+
const { detectProviderByKey: detectProviderByKey3 } = await Promise.resolve().then(() => (init_catalog(), exports_catalog));
|
|
149280
150243
|
const { which: which2 } = await Promise.resolve().then(() => (init_proc(), exports_proc));
|
|
149281
150244
|
const pipedAnswers = process.stdin.isTTY ? null : (await readStdin()).split(/\r?\n/);
|
|
149282
150245
|
const rl = pipedAnswers ? null : createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -149369,7 +150332,7 @@ async function runCliOnboarding() {
|
|
|
149369
150332
|
const key = await ask("Paste API key: ");
|
|
149370
150333
|
if (!key)
|
|
149371
150334
|
continue;
|
|
149372
|
-
const detected =
|
|
150335
|
+
const detected = detectProviderByKey3(key);
|
|
149373
150336
|
if (!detected) {
|
|
149374
150337
|
console.log(warn("Could not detect the provider from that key. Use option 3."));
|
|
149375
150338
|
continue;
|
|
@@ -149545,7 +150508,7 @@ if (args[0] === "auth") {
|
|
|
149545
150508
|
const { importableEnvCreds: importableEnvCreds2, importEnvCred: importEnvCred2, importableCloudCreds: importableCloudCreds2, importCloudCred: importCloudCred2 } = await Promise.resolve().then(() => (init_detect(), exports_detect));
|
|
149546
150509
|
const { addApiKeyAccount: addApiKeyAccount2, addByPastedKey: addByPastedKey2, addOpenAICompatAccount: addOpenAICompatAccount2, testAccount: testAccount2, addableProviders: addableProviders2, addCliAccount: addCliAccount2, cliAuthStatus: cliAuthStatus2, cliLoginArgs: cliLoginArgs2 } = await Promise.resolve().then(() => (init_onboard(), exports_onboard));
|
|
149547
150510
|
const { subscriptionEnv: subscriptionEnv2 } = await Promise.resolve().then(() => (init_cli_backend(), exports_cli_backend));
|
|
149548
|
-
const { detectProviderByKey:
|
|
150511
|
+
const { detectProviderByKey: detectProviderByKey3 } = await Promise.resolve().then(() => (init_catalog(), exports_catalog));
|
|
149549
150512
|
const sub = args[1];
|
|
149550
150513
|
const rest2 = args.slice(2);
|
|
149551
150514
|
if (sub === "list" || !sub) {
|
|
@@ -149570,7 +150533,7 @@ Importable from your env (gearbox auth import): ${imp.map((c) => c.envVar).join(
|
|
|
149570
150533
|
} else if (sub === "add") {
|
|
149571
150534
|
const head2 = (rest2[0] ?? "").toLowerCase();
|
|
149572
150535
|
const cliProvider = head2 === "codex" || head2 === "chatgpt" ? "codex-cli" : head2 === "claude" ? "claude-cli" : "";
|
|
149573
|
-
const res = cliProvider ? addCliAccount2(cliProvider, rest2.slice(1).join(" ").trim() || undefined) : ["openai-compat", "openai-compatible", "custom", "proxy"].includes(head2) ? await addOpenAICompatAccount2(rest2[1] ?? "", rest2[2] ?? "", rest2[3] ?? "", rest2.slice(4)) : rest2[0] && !rest2[1] &&
|
|
150536
|
+
const res = cliProvider ? addCliAccount2(cliProvider, rest2.slice(1).join(" ").trim() || undefined) : ["openai-compat", "openai-compatible", "custom", "proxy"].includes(head2) ? await addOpenAICompatAccount2(rest2[1] ?? "", rest2[2] ?? "", rest2[3] ?? "", rest2.slice(4)) : rest2[0] && !rest2[1] && detectProviderByKey3(rest2[0]) ? await addByPastedKey2(rest2[0]) : rest2[0] && rest2[1] ? await addApiKeyAccount2(rest2[0], rest2[1]) : { ok: false, message: "usage: gearbox auth add <key> | gearbox auth add <provider> <key> | gearbox auth add openai-compat <name> <base-url> <key> <model> | gearbox auth add codex [name]" };
|
|
149574
150537
|
console.log(res.message);
|
|
149575
150538
|
if (res.ok && res.account) {
|
|
149576
150539
|
if (res.account.exec === "cli" && res.account.auth.kind === "cli") {
|