@spinabot/brigade 1.22.1 → 1.24.0
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/README.md +1 -1
- package/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +73 -3
- package/dist/agents/agent-loop.js.map +1 -1
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +6 -0
- package/dist/agents/channels/inbound-pipeline.js.map +1 -1
- package/dist/agents/claude-cli/availability.d.ts +12 -0
- package/dist/agents/claude-cli/availability.d.ts.map +1 -0
- package/dist/agents/claude-cli/availability.js +79 -0
- package/dist/agents/claude-cli/availability.js.map +1 -0
- package/dist/agents/claude-cli/catalog.d.ts +91 -0
- package/dist/agents/claude-cli/catalog.d.ts.map +1 -0
- package/dist/agents/claude-cli/catalog.js +260 -0
- package/dist/agents/claude-cli/catalog.js.map +1 -0
- package/dist/agents/claude-cli/claude-config.d.ts +38 -0
- package/dist/agents/claude-cli/claude-config.d.ts.map +1 -0
- package/dist/agents/claude-cli/claude-config.js +111 -0
- package/dist/agents/claude-cli/claude-config.js.map +1 -0
- package/dist/agents/claude-cli/register.d.ts +32 -0
- package/dist/agents/claude-cli/register.d.ts.map +1 -0
- package/dist/agents/claude-cli/register.js +68 -0
- package/dist/agents/claude-cli/register.js.map +1 -0
- package/dist/agents/claude-cli/spawn.d.ts +42 -0
- package/dist/agents/claude-cli/spawn.d.ts.map +1 -0
- package/dist/agents/claude-cli/spawn.js +179 -0
- package/dist/agents/claude-cli/spawn.js.map +1 -0
- package/dist/agents/claude-cli/stream-json.d.ts +129 -0
- package/dist/agents/claude-cli/stream-json.d.ts.map +1 -0
- package/dist/agents/claude-cli/stream-json.js +84 -0
- package/dist/agents/claude-cli/stream-json.js.map +1 -0
- package/dist/agents/claude-cli/stream.d.ts +27 -0
- package/dist/agents/claude-cli/stream.d.ts.map +1 -0
- package/dist/agents/claude-cli/stream.js +380 -0
- package/dist/agents/claude-cli/stream.js.map +1 -0
- package/dist/agents/error-classifier.d.ts +2 -2
- package/dist/agents/error-classifier.d.ts.map +1 -1
- package/dist/agents/error-classifier.js +43 -0
- package/dist/agents/error-classifier.js.map +1 -1
- package/dist/agents/model-resolution.d.ts +36 -0
- package/dist/agents/model-resolution.d.ts.map +1 -1
- package/dist/agents/model-resolution.js +59 -0
- package/dist/agents/model-resolution.js.map +1 -1
- package/dist/agents/retry-policy.d.ts.map +1 -1
- package/dist/agents/retry-policy.js +25 -1
- package/dist/agents/retry-policy.js.map +1 -1
- package/dist/agents/tools/sessions/shared.d.ts.map +1 -1
- package/dist/agents/tools/sessions/shared.js +4 -0
- package/dist/agents/tools/sessions/shared.js.map +1 -1
- package/dist/auth/auth-health.d.ts +107 -1
- package/dist/auth/auth-health.d.ts.map +1 -1
- package/dist/auth/auth-health.js +245 -2
- package/dist/auth/auth-health.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/auth.js +18 -0
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +46 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/gateway.d.ts +4 -0
- package/dist/cli/commands/gateway.d.ts.map +1 -1
- package/dist/cli/commands/gateway.js +52 -6
- package/dist/cli/commands/gateway.js.map +1 -1
- package/dist/cli/commands/login.d.ts.map +1 -1
- package/dist/cli/commands/login.js +107 -19
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/secrets-audit.d.ts.map +1 -1
- package/dist/cli/commands/secrets-audit.js +1 -0
- package/dist/cli/commands/secrets-audit.js.map +1 -1
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +16 -0
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +13 -0
- package/dist/core/server.js.map +1 -1
- package/dist/logging/redact.d.ts +12 -8
- package/dist/logging/redact.d.ts.map +1 -1
- package/dist/logging/redact.js +13 -8
- package/dist/logging/redact.js.map +1 -1
- package/dist/providers/catalog.d.ts +13 -0
- package/dist/providers/catalog.d.ts.map +1 -1
- package/dist/providers/catalog.js +31 -0
- package/dist/providers/catalog.js.map +1 -1
- package/dist/system-prompt/runtime-params.d.ts +5 -0
- package/dist/system-prompt/runtime-params.d.ts.map +1 -1
- package/dist/system-prompt/runtime-params.js +2 -1
- package/dist/system-prompt/runtime-params.js.map +1 -1
- package/dist/ui/onboarding.d.ts +11 -0
- package/dist/ui/onboarding.d.ts.map +1 -1
- package/dist/ui/onboarding.js +397 -6
- package/dist/ui/onboarding.js.map +1 -1
- package/package.json +2 -1
package/dist/ui/onboarding.js
CHANGED
|
@@ -25,9 +25,12 @@ import { upsertApiKeyProfile, upsertApiKeyRefProfile, upsertOAuthProfile, upsert
|
|
|
25
25
|
import { DEFAULT_AGENT_ID, resolveAuthProfilesPath, resolveModelsPath } from "../config/paths.js";
|
|
26
26
|
import { saveConfig } from "../core/config.js";
|
|
27
27
|
import { readClaudeCliLogin, readCodexCliLogin } from "../integrations/cli-login.js";
|
|
28
|
+
import { isClaudeCliAvailable } from "../agents/claude-cli/availability.js";
|
|
29
|
+
import { CLAUDE_CLI_DEFAULT_MODEL, CLAUDE_CLI_MODELS } from "../agents/claude-cli/catalog.js";
|
|
30
|
+
import { hasBrigadeClaudeLogin, writeBrigadeClaudeCredential } from "../agents/claude-cli/claude-config.js";
|
|
28
31
|
import { writeCustomProviderToModelsJson } from "../integrations/custom-provider.js";
|
|
29
32
|
import { discoverOllamaModels, writeOllamaToModelsJson } from "../integrations/ollama.js";
|
|
30
|
-
import { findProvider, PROVIDERS, readProviderEnvKey, resolveProviderEnvVarSource, } from "../providers/catalog.js";
|
|
33
|
+
import { findProvider, PROVIDERS, readProviderEnvKey, resolveProviderEnvVarSource, routesToCustomProvider, } from "../providers/catalog.js";
|
|
31
34
|
import { validateApiKeyOnline } from "../providers/validate-key.js";
|
|
32
35
|
import { renderBrandHeader } from "./brand.js";
|
|
33
36
|
import { brand, selectListTheme } from "./theme.js";
|
|
@@ -194,6 +197,19 @@ export async function runOnboarding(tui, authStorage, modelRegistry, opts = {})
|
|
|
194
197
|
step = "model";
|
|
195
198
|
continue;
|
|
196
199
|
}
|
|
200
|
+
// claude-cli backend — no key + no browser flow: the `claude` binary
|
|
201
|
+
// authenticates with its OWN login. Validate the binary is installed
|
|
202
|
+
// and logged in; if not, guide the operator to run `claude` once. Models
|
|
203
|
+
// are synthesized, so nothing is written to models.json.
|
|
204
|
+
if (providerInfo?.id === "claude-cli") {
|
|
205
|
+
const result = await ensureClaudeCli(tui, authStorage);
|
|
206
|
+
if (result === "back") {
|
|
207
|
+
step = "provider";
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
step = "model";
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
197
213
|
// CLI-login reuse — if the provider can adopt an already-logged-in
|
|
198
214
|
// vendor CLI's token on this machine (Claude Code, Codex), offer the
|
|
199
215
|
// one-keystroke "reuse this login" path FIRST. "other" means no CLI
|
|
@@ -233,7 +249,12 @@ export async function runOnboarding(tui, authStorage, modelRegistry, opts = {})
|
|
|
233
249
|
// Custom (catalog-defined) providers — a key + a known
|
|
234
250
|
// Anthropic-compatible endpoint (GLM, Kimi, Qwen, MiniMax, DeepSeek).
|
|
235
251
|
// Paste the key, register the endpoint + models into models.json, done.
|
|
236
|
-
|
|
252
|
+
// Also handles the generic "Custom (OpenAI-compatible)" entry, which
|
|
253
|
+
// has `custom: true` but no pre-set `baseUrl`. `ensureCustomProvider`
|
|
254
|
+
// prompts for the URL when it's missing. Without this, the generic
|
|
255
|
+
// custom path falls through to `ensureApiKey`, which never writes
|
|
256
|
+
// `models.json` — leaving the model unresolvable at gateway startup.
|
|
257
|
+
if (routesToCustomProvider(providerInfo)) {
|
|
237
258
|
const r = await ensureCustomProvider(tui, authStorage, modelRegistry, providerInfo);
|
|
238
259
|
if (r === "back") {
|
|
239
260
|
step = "provider";
|
|
@@ -256,6 +277,17 @@ export async function runOnboarding(tui, authStorage, modelRegistry, opts = {})
|
|
|
256
277
|
}
|
|
257
278
|
// step === "model"
|
|
258
279
|
renderScreen(tui, "Step 4 of 5 · Default model");
|
|
280
|
+
// claude-cli models are synthesized (not in the registry), so pick from the
|
|
281
|
+
// backend's own catalog instead of the registry-backed picker.
|
|
282
|
+
if (provider === "claude-cli") {
|
|
283
|
+
const result = await pickClaudeCliModel(tui);
|
|
284
|
+
if (result === "back") {
|
|
285
|
+
step = "provider";
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
modelId = result.modelId;
|
|
289
|
+
break; // model chosen — exit the wizard loop to persist + finish
|
|
290
|
+
}
|
|
259
291
|
const result = await pickModel(tui, modelRegistry, findProvider(provider)?.providerId ?? provider);
|
|
260
292
|
if (result === "back") {
|
|
261
293
|
step = "provider"; // go all the way back so they can change provider too
|
|
@@ -378,10 +410,22 @@ async function pickProvider(tui) {
|
|
|
378
410
|
const isBYO = p.custom === true && !p.baseUrl;
|
|
379
411
|
let rank;
|
|
380
412
|
let badge;
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
413
|
+
// The claude-cli backend: it's `local+noAuth` in the catalog, but it's a
|
|
414
|
+
// SUBSCRIPTION path, not a local server. Rank it with the subscription tier
|
|
415
|
+
// and surface it at the very top when the binary is installed + logged in
|
|
416
|
+
// (the cleanest "no extra-usage" route). Checked BEFORE the generic
|
|
417
|
+
// cliReady / local branches so it doesn't get the wrong badge.
|
|
418
|
+
if (p.id === "claude-cli") {
|
|
419
|
+
const ready = isClaudeCliAvailable() && (readClaudeCliLogin() !== null || hasBrigadeClaudeLogin());
|
|
420
|
+
rank = ready ? 0 : 2;
|
|
421
|
+
badge = ready
|
|
422
|
+
? "installed + signed in — subscription, no key, no extra-usage"
|
|
423
|
+
: "your Claude subscription — browser sign-in, no key, no extra-usage";
|
|
424
|
+
}
|
|
425
|
+
else if (cliReady && !isSubscription) {
|
|
426
|
+
// A subscription provider stays "log in" (browser-first, multi-account)
|
|
427
|
+
// even when a CLI login is on disk — reuse is offered as a secondary
|
|
428
|
+
// option inside the flow. Only a pure CLI-login provider gets rank-0.
|
|
385
429
|
rank = 0;
|
|
386
430
|
badge = "logged in — reuse, no key";
|
|
387
431
|
}
|
|
@@ -970,6 +1014,269 @@ export async function ensureSubscriptionLogin(tui, authStorage, provider) {
|
|
|
970
1014
|
return "ok";
|
|
971
1015
|
}
|
|
972
1016
|
}
|
|
1017
|
+
/** True when EITHER the operator's own `claude` login OR Brigade's managed
|
|
1018
|
+
* dedicated login is present — the backend can run with either. */
|
|
1019
|
+
function claudeCliLoggedIn() {
|
|
1020
|
+
return readClaudeCliLogin() !== null || hasBrigadeClaudeLogin();
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Connect the claude-cli backend end to end — no terminal, no token paste:
|
|
1024
|
+
* 1. Ensure the `claude` binary is installed (offer to `npm i -g` it).
|
|
1025
|
+
* 2. Ensure a login exists — if not, drive the SAME browser OAuth Brigade uses
|
|
1026
|
+
* for Claude Pro/Max and write the result into Brigade's OWN Claude config
|
|
1027
|
+
* dir (a dedicated grant, isolated from the operator's personal ~/.claude).
|
|
1028
|
+
* Turns then run on the Claude subscription via the binary (no extra-usage).
|
|
1029
|
+
*
|
|
1030
|
+
* Returns "ok" to proceed to model selection, or "back" to re-pick the provider.
|
|
1031
|
+
*/
|
|
1032
|
+
export async function ensureClaudeCli(tui, authStorage) {
|
|
1033
|
+
while (true) {
|
|
1034
|
+
renderScreen(tui, "Step 3 of 5 · Connect Claude");
|
|
1035
|
+
// ── 1. binary present? ──
|
|
1036
|
+
if (!isClaudeCliAvailable({ force: true })) {
|
|
1037
|
+
tui.addChild(new Text(` ${brand.white("Brigade runs on your Claude subscription via the Claude Code engine.")}`, 0, 0));
|
|
1038
|
+
tui.addChild(new Text(brand.dim(" The `claude` command isn't installed yet. Brigade can install it for you."), 0, 0));
|
|
1039
|
+
tui.addChild(new Text("", 0, 0));
|
|
1040
|
+
const choice = new SelectList([
|
|
1041
|
+
{ value: "install", label: "Install it now", description: "runs: npm i -g @anthropic-ai/claude-code" },
|
|
1042
|
+
{ value: "recheck", label: "I'll install it myself — re-check", description: "" },
|
|
1043
|
+
], 2, selectListTheme, { minPrimaryColumnWidth: 18, maxPrimaryColumnWidth: 24 });
|
|
1044
|
+
tui.addChild(choice);
|
|
1045
|
+
tui.setFocus(choice);
|
|
1046
|
+
tui.requestRender();
|
|
1047
|
+
let pick;
|
|
1048
|
+
try {
|
|
1049
|
+
const chosen = await new Promise((resolve, reject) => {
|
|
1050
|
+
choice.onSelect = (item) => resolve(item);
|
|
1051
|
+
choice.onCancel = () => reject(new Error("back"));
|
|
1052
|
+
});
|
|
1053
|
+
pick = chosen.value === "install" ? "install" : "recheck";
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
return "back";
|
|
1057
|
+
}
|
|
1058
|
+
if (pick === "install") {
|
|
1059
|
+
renderScreen(tui, "Step 3 of 5 · Connect Claude");
|
|
1060
|
+
const loader = new CancellableLoader(tui, (s) => brand.amber(s), (s) => brand.dim(s), "Installing Claude Code (npm i -g @anthropic-ai/claude-code)…");
|
|
1061
|
+
tui.addChild(loader);
|
|
1062
|
+
tui.requestRender();
|
|
1063
|
+
const ok = await installClaudeCode();
|
|
1064
|
+
loader.stop?.();
|
|
1065
|
+
if (!ok) {
|
|
1066
|
+
renderScreen(tui, "Step 3 of 5 · Connect Claude");
|
|
1067
|
+
tui.addChild(new Text(` ${brand.error("✗")} ${brand.error("Install failed. Run `npm i -g @anthropic-ai/claude-code` manually, then Enter.")}`, 0, 0));
|
|
1068
|
+
const c = new Input();
|
|
1069
|
+
tui.addChild(c);
|
|
1070
|
+
tui.setFocus(c);
|
|
1071
|
+
tui.requestRender();
|
|
1072
|
+
try {
|
|
1073
|
+
await new Promise((res, rej) => {
|
|
1074
|
+
c.onSubmit = () => res();
|
|
1075
|
+
c.onEscape = () => rej(new Error("back"));
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
return "back";
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
continue; // re-loop: re-check install state
|
|
1084
|
+
}
|
|
1085
|
+
// ── 2. login present? ──
|
|
1086
|
+
if (claudeCliLoggedIn()) {
|
|
1087
|
+
tui.addChild(new Text(` ${brand.amber("✓")} ${brand.dim("Claude is installed and signed in.")}`, 0, 0));
|
|
1088
|
+
tui.addChild(new Text(brand.dim(" Turns run on your Claude subscription — no key, no extra-usage billing."), 0, 0));
|
|
1089
|
+
tui.addChild(new Text("", 0, 0));
|
|
1090
|
+
tui.addChild(new Text(brand.dim(" Enter to continue · Esc to go back"), 0, 0));
|
|
1091
|
+
const confirm = new Input();
|
|
1092
|
+
tui.addChild(confirm);
|
|
1093
|
+
tui.setFocus(confirm);
|
|
1094
|
+
tui.requestRender();
|
|
1095
|
+
try {
|
|
1096
|
+
await new Promise((resolve, reject) => {
|
|
1097
|
+
confirm.onSubmit = () => resolve();
|
|
1098
|
+
confirm.onEscape = () => reject(new Error("back"));
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
return "back";
|
|
1103
|
+
}
|
|
1104
|
+
return "ok";
|
|
1105
|
+
}
|
|
1106
|
+
// ── 3. no login → drive the browser OAuth ourselves, write Brigade's grant ──
|
|
1107
|
+
tui.addChild(new Text(` ${brand.white("Sign in to your Claude account")}`, 0, 0));
|
|
1108
|
+
tui.addChild(new Text(brand.dim(" We'll open your browser. Approve it there — no key, no terminal."), 0, 0));
|
|
1109
|
+
tui.addChild(new Text(brand.dim(" Enter to start · Esc to go back"), 0, 0));
|
|
1110
|
+
const start = new Input();
|
|
1111
|
+
tui.addChild(start);
|
|
1112
|
+
tui.setFocus(start);
|
|
1113
|
+
tui.requestRender();
|
|
1114
|
+
try {
|
|
1115
|
+
await new Promise((resolve, reject) => {
|
|
1116
|
+
start.onSubmit = () => resolve();
|
|
1117
|
+
start.onEscape = () => reject(new Error("back"));
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
return "back";
|
|
1122
|
+
}
|
|
1123
|
+
tui.removeChild(start);
|
|
1124
|
+
const result = await runClaudeBrowserLogin(tui, authStorage);
|
|
1125
|
+
if (result === "ok") {
|
|
1126
|
+
tui.addChild(new Text("", 0, 0));
|
|
1127
|
+
tui.addChild(new Text(` ${brand.amber("✓")} Signed in — your Claude subscription is connected.`, 0, 0));
|
|
1128
|
+
tui.requestRender();
|
|
1129
|
+
await delay(600);
|
|
1130
|
+
return "ok";
|
|
1131
|
+
}
|
|
1132
|
+
if (result === "back")
|
|
1133
|
+
return "back";
|
|
1134
|
+
// "retry" → loop shows the sign-in prompt again.
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Install Claude Code globally via npm. Returns true on success. Best-effort +
|
|
1139
|
+
* bounded; surfaces nothing itself (the caller renders status).
|
|
1140
|
+
*/
|
|
1141
|
+
async function installClaudeCode() {
|
|
1142
|
+
return await new Promise((resolve) => {
|
|
1143
|
+
try {
|
|
1144
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1145
|
+
const child = spawn(npm, ["install", "-g", "@anthropic-ai/claude-code"], {
|
|
1146
|
+
stdio: "ignore",
|
|
1147
|
+
shell: process.platform === "win32",
|
|
1148
|
+
});
|
|
1149
|
+
const timer = setTimeout(() => {
|
|
1150
|
+
try {
|
|
1151
|
+
child.kill();
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
/* already gone */
|
|
1155
|
+
}
|
|
1156
|
+
resolve(false);
|
|
1157
|
+
}, 180_000);
|
|
1158
|
+
timer.unref?.();
|
|
1159
|
+
child.on("close", (code) => {
|
|
1160
|
+
clearTimeout(timer);
|
|
1161
|
+
resolve(code === 0 && isClaudeCliAvailable({ force: true }));
|
|
1162
|
+
});
|
|
1163
|
+
child.on("error", () => {
|
|
1164
|
+
clearTimeout(timer);
|
|
1165
|
+
resolve(false);
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
resolve(false);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Drive Brigade's browser OAuth for Anthropic (the same flow Claude Pro/Max
|
|
1175
|
+
* onboarding uses — pi-ai requests the full Claude Code scopes) and write the
|
|
1176
|
+
* result into Brigade's OWN managed Claude config dir, so the `claude` binary
|
|
1177
|
+
* authenticates + refreshes from Brigade's dedicated grant. Also mirrors the
|
|
1178
|
+
* credential into Brigade's anthropic profile so the HTTP path is available too.
|
|
1179
|
+
*
|
|
1180
|
+
* Returns "ok" on success, "back" on user abort, "retry" on a recoverable error.
|
|
1181
|
+
*/
|
|
1182
|
+
async function runClaudeBrowserLogin(tui, authStorage) {
|
|
1183
|
+
const oauthProvider = getOAuthProvider("anthropic");
|
|
1184
|
+
if (!oauthProvider) {
|
|
1185
|
+
tui.addChild(new Text(` ${brand.error("✗")} ${brand.error("Browser sign-in isn't available in this build.")}`, 0, 0));
|
|
1186
|
+
tui.requestRender();
|
|
1187
|
+
await delay(1200);
|
|
1188
|
+
return "back";
|
|
1189
|
+
}
|
|
1190
|
+
const controller = new AbortController();
|
|
1191
|
+
let creds;
|
|
1192
|
+
try {
|
|
1193
|
+
creds = (await oauthProvider.login({
|
|
1194
|
+
onAuth: (info) => {
|
|
1195
|
+
tui.addChild(new Text(` ${brand.amber("→")} Opening your browser to sign in…`, 0, 0));
|
|
1196
|
+
openSubscriptionBrowser(info.url);
|
|
1197
|
+
tui.addChild(new Text("", 0, 0));
|
|
1198
|
+
tui.addChild(new Text(" " + brand.amber(info.url), 0, 0));
|
|
1199
|
+
tui.addChild(new Text(brand.dim(" If your browser didn't open, copy the link above. Paste the code here if asked."), 0, 0));
|
|
1200
|
+
const waiter = new CancellableLoader(tui, (s) => brand.amber(s), (s) => brand.dim(s), "Waiting for you to authorize…");
|
|
1201
|
+
waiter.onAbort = () => controller.abort();
|
|
1202
|
+
tui.addChild(waiter);
|
|
1203
|
+
tui.setFocus(waiter);
|
|
1204
|
+
tui.requestRender();
|
|
1205
|
+
},
|
|
1206
|
+
onManualCodeInput: () => new Promise((resolve, reject) => {
|
|
1207
|
+
tui.addChild(new Text("", 0, 0));
|
|
1208
|
+
tui.addChild(new Text(brand.dim(" Paste the code or redirect URL, then press Enter · Esc to cancel"), 0, 0));
|
|
1209
|
+
const input = new Input();
|
|
1210
|
+
tui.addChild(input);
|
|
1211
|
+
tui.setFocus(input);
|
|
1212
|
+
tui.requestRender();
|
|
1213
|
+
input.onSubmit = (value) => resolve(sanitizePastedValue(value));
|
|
1214
|
+
input.onEscape = () => reject(new Error("cancelled"));
|
|
1215
|
+
}),
|
|
1216
|
+
onProgress: (msg) => {
|
|
1217
|
+
tui.addChild(new Text(brand.dim(" " + msg), 0, 0));
|
|
1218
|
+
tui.requestRender();
|
|
1219
|
+
},
|
|
1220
|
+
// Anthropic's loopback flow never uses these, but the callback contract
|
|
1221
|
+
// requires them — provide minimal implementations so the type + runtime
|
|
1222
|
+
// are both satisfied.
|
|
1223
|
+
onDeviceCode: () => {
|
|
1224
|
+
/* not used by the anthropic loopback flow */
|
|
1225
|
+
},
|
|
1226
|
+
onPrompt: (p) => new Promise((resolve, reject) => {
|
|
1227
|
+
tui.addChild(new Text("", 0, 0));
|
|
1228
|
+
tui.addChild(new Text(` ${p.message}`, 0, 0));
|
|
1229
|
+
const input = new Input();
|
|
1230
|
+
tui.addChild(input);
|
|
1231
|
+
tui.setFocus(input);
|
|
1232
|
+
tui.requestRender();
|
|
1233
|
+
input.onSubmit = (value) => {
|
|
1234
|
+
const v = value.trim();
|
|
1235
|
+
if (!v && !p.allowEmpty)
|
|
1236
|
+
return;
|
|
1237
|
+
resolve(v);
|
|
1238
|
+
};
|
|
1239
|
+
input.onEscape = () => reject(new Error("cancelled"));
|
|
1240
|
+
}),
|
|
1241
|
+
onSelect: (p) => new Promise((resolve) => {
|
|
1242
|
+
const list = new SelectList(p.options.map((o) => ({ value: o.id, label: o.label })), Math.min(p.options.length, 6), selectListTheme, { minPrimaryColumnWidth: 12, maxPrimaryColumnWidth: 28 });
|
|
1243
|
+
tui.addChild(list);
|
|
1244
|
+
tui.setFocus(list);
|
|
1245
|
+
tui.requestRender();
|
|
1246
|
+
list.onSelect = (item) => resolve(item.value);
|
|
1247
|
+
list.onCancel = () => resolve(undefined);
|
|
1248
|
+
}),
|
|
1249
|
+
signal: controller.signal,
|
|
1250
|
+
}));
|
|
1251
|
+
}
|
|
1252
|
+
catch (err) {
|
|
1253
|
+
controller.abort();
|
|
1254
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1255
|
+
const softCancel = /^login cancelled$/i.test(reason) || reason === "cancelled" || reason === "back";
|
|
1256
|
+
tui.addChild(new Text(` ${brand.error("✗")} ${brand.error(softCancel ? "Sign-in cancelled." : "Couldn't finish signing in — check your connection and try again.")}`, 0, 0));
|
|
1257
|
+
tui.requestRender();
|
|
1258
|
+
await delay(1000);
|
|
1259
|
+
return softCancel ? "back" : "retry";
|
|
1260
|
+
}
|
|
1261
|
+
// Write Brigade's dedicated Claude login ONLY into the managed config dir.
|
|
1262
|
+
// The `claude` binary owns this grant from here on: it authenticates AND
|
|
1263
|
+
// refreshes (rotating the refresh token) in-place. We deliberately DON'T
|
|
1264
|
+
// also mirror it into Brigade's anthropic auth-profile — that would create a
|
|
1265
|
+
// SECOND independent refresher (Brigade's HTTP-path backend) for the same
|
|
1266
|
+
// grant, and the two would rotate each other's refresh token to death
|
|
1267
|
+
// (the split-brain failure). Single grant, single owner (the binary).
|
|
1268
|
+
void authStorage; // intentionally unused now — kept for signature stability
|
|
1269
|
+
try {
|
|
1270
|
+
writeBrigadeClaudeCredential({ access: creds.access, refresh: creds.refresh, expires: creds.expires });
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
tui.addChild(new Text(` ${brand.error("✗")} ${brand.error(`Couldn't save the login: ${err.message}`)}`, 0, 0));
|
|
1274
|
+
tui.requestRender();
|
|
1275
|
+
await delay(1200);
|
|
1276
|
+
return "retry";
|
|
1277
|
+
}
|
|
1278
|
+
return "ok";
|
|
1279
|
+
}
|
|
973
1280
|
/**
|
|
974
1281
|
* Connect a subscription provider that ALSO has a vendor CLI login on disk
|
|
975
1282
|
* (Claude Code / Codex). When such a login exists we present a choice that LEADS
|
|
@@ -1022,6 +1329,12 @@ async function ensureCliLogin(tui, authStorage, provider) {
|
|
|
1022
1329
|
access: cred.access,
|
|
1023
1330
|
refresh: cred.refresh,
|
|
1024
1331
|
expires: cred.expires,
|
|
1332
|
+
// Borrowed from the vendor CLI's on-disk login — mark the family so
|
|
1333
|
+
// `adoptNewerClaudeCliLogin` keeps adopting the CLI's rotations
|
|
1334
|
+
// instead of refreshing (and rotating) a stale shared grant.
|
|
1335
|
+
...(provider.cliLogin.read === "claude"
|
|
1336
|
+
? { metadata: { importedFrom: "claude-cli" } }
|
|
1337
|
+
: {}),
|
|
1025
1338
|
});
|
|
1026
1339
|
authStorage.set(cred.provider, {
|
|
1027
1340
|
type: "oauth",
|
|
@@ -1085,6 +1398,53 @@ async function ensureCliLogin(tui, authStorage, provider) {
|
|
|
1085
1398
|
* - "back" → user pressed Esc; caller rewinds to the provider picker
|
|
1086
1399
|
*/
|
|
1087
1400
|
async function ensureCustomProvider(tui, authStorage, modelRegistry, provider) {
|
|
1401
|
+
// Generic custom provider — the catalog entry has `custom: true` but no
|
|
1402
|
+
// pre-set `baseUrl` (it varies per user). Prompt for the URL before the
|
|
1403
|
+
// key-entry loop, then attach it to a shallow copy so the downstream
|
|
1404
|
+
// write path sees a fully resolved provider without mutating the catalog.
|
|
1405
|
+
if (!provider.baseUrl) {
|
|
1406
|
+
let urlError = null;
|
|
1407
|
+
while (true) {
|
|
1408
|
+
renderScreen(tui, `Step 3 of 5 · ${provider.name}`);
|
|
1409
|
+
// Render the error at the TOP of the loop (before the prompt), with the
|
|
1410
|
+
// single requestRender() below. Adding it AFTER requestRender lets the
|
|
1411
|
+
// next iteration's clear() wipe it before it ever paints — a silent
|
|
1412
|
+
// re-prompt with no reason shown. Mirrors the key-entry loop's lastError.
|
|
1413
|
+
if (urlError) {
|
|
1414
|
+
tui.addChild(new Text(` ${brand.error("✗")} ${brand.error(urlError)}`, 0, 0));
|
|
1415
|
+
tui.addChild(new Text("", 0, 0));
|
|
1416
|
+
}
|
|
1417
|
+
tui.addChild(new Text(` Enter your OpenAI-compatible base URL.`, 0, 0));
|
|
1418
|
+
tui.addChild(new Text(brand.dim(" Example: https://api.example.com/v1"), 0, 0));
|
|
1419
|
+
tui.addChild(new Text(brand.dim(" Enter to continue · Esc to go back"), 0, 0));
|
|
1420
|
+
tui.addChild(new Text("", 0, 0));
|
|
1421
|
+
const urlInput = new Input();
|
|
1422
|
+
tui.addChild(urlInput);
|
|
1423
|
+
tui.setFocus(urlInput);
|
|
1424
|
+
tui.requestRender();
|
|
1425
|
+
let rawUrl;
|
|
1426
|
+
try {
|
|
1427
|
+
rawUrl = await new Promise((resolve, reject) => {
|
|
1428
|
+
urlInput.onSubmit = (value) => resolve(value.trim());
|
|
1429
|
+
urlInput.onEscape = () => reject(new Error("back"));
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return "back";
|
|
1434
|
+
}
|
|
1435
|
+
if (!rawUrl) {
|
|
1436
|
+
urlError = "Enter a base URL, or press Esc to go back.";
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
// Minimal URL sanity check — must start with http(s)://.
|
|
1440
|
+
if (!/^https?:\/\//i.test(rawUrl)) {
|
|
1441
|
+
urlError = "URL must start with http:// or https://";
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
provider = { ...provider, baseUrl: rawUrl, api: provider.api ?? "openai-completions" };
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1088
1448
|
let lastError = null;
|
|
1089
1449
|
// If the operator already has this provider's key in their environment (e.g.
|
|
1090
1450
|
// NVIDIA_API_KEY), OFFER it — confirm-then-validate, never silent-adopt. A
|
|
@@ -1278,6 +1638,37 @@ async function pickModel(tui, modelRegistry, providerId) {
|
|
|
1278
1638
|
return "back";
|
|
1279
1639
|
}
|
|
1280
1640
|
}
|
|
1641
|
+
/** Model picker for the claude-cli backend — its models are synthesized (not in
|
|
1642
|
+
* the registry), so we present the backend's own catalog. */
|
|
1643
|
+
async function pickClaudeCliModel(tui) {
|
|
1644
|
+
const items = CLAUDE_CLI_MODELS.map((m) => ({
|
|
1645
|
+
value: m.id,
|
|
1646
|
+
label: m.id,
|
|
1647
|
+
description: m.name,
|
|
1648
|
+
}));
|
|
1649
|
+
// Default first = the catalog default (Sonnet), then the rest as listed.
|
|
1650
|
+
items.sort((a, b) => a.value === CLAUDE_CLI_DEFAULT_MODEL ? -1 : b.value === CLAUDE_CLI_DEFAULT_MODEL ? 1 : 0);
|
|
1651
|
+
const list = new SearchableSelectList(items, 8, selectListTheme, {
|
|
1652
|
+
minPrimaryColumnWidth: 26,
|
|
1653
|
+
maxPrimaryColumnWidth: 38,
|
|
1654
|
+
formatHeader: (q, matchCount, total) => brand.dim(q.length > 0
|
|
1655
|
+
? ` search: ${q}▌ (${matchCount}/${total})`
|
|
1656
|
+
: ` ${total} models · ↑↓ move · Enter select · Esc back`),
|
|
1657
|
+
});
|
|
1658
|
+
tui.addChild(list);
|
|
1659
|
+
tui.setFocus(list);
|
|
1660
|
+
tui.requestRender();
|
|
1661
|
+
try {
|
|
1662
|
+
const chosen = await new Promise((resolve, reject) => {
|
|
1663
|
+
list.onSelect = (item) => resolve(item);
|
|
1664
|
+
list.onCancel = () => reject(new Error("back"));
|
|
1665
|
+
});
|
|
1666
|
+
return { modelId: chosen.value };
|
|
1667
|
+
}
|
|
1668
|
+
catch {
|
|
1669
|
+
return "back";
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1281
1672
|
function renderDone(tui, provider, modelId) {
|
|
1282
1673
|
const p = findProvider(provider)?.name ?? provider;
|
|
1283
1674
|
tui.addChild(new Text("", 0, 0));
|