@boundless.network/setup 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +53 -33
- package/dist/harnesses/claude-code.js +1 -2
- package/dist/harnesses/codebuddy.js +48 -0
- package/dist/harnesses/codex.js +1 -2
- package/dist/harnesses/gui.js +50 -5
- package/dist/harnesses/hermes.js +1 -2
- package/dist/harnesses/index.js +24 -3
- package/dist/harnesses/omp.js +1 -2
- package/dist/harnesses/openclaw.js +47 -0
- package/dist/harnesses/opencode.js +69 -46
- package/dist/harnesses/openhands.js +36 -0
- package/dist/harnesses/qwen-code.js +40 -0
- package/dist/lib/catalog.js +4 -0
- package/dist/lib/files.js +30 -1
- package/dist/ui/menu.js +5 -2
- package/dist/ui/table.js +9 -6
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
// Async screens run their task exactly once, on mount; the wizard object is mutable and shared.
|
|
5
5
|
import { HARNESSES } from "./harnesses/index.js";
|
|
6
6
|
import { openBrowser } from "./lib/browser.js";
|
|
7
|
-
import { keyWorks, listModels } from "./lib/catalog.js";
|
|
7
|
+
import { keyWorks, listModels, RECOMMENDED_BACKGROUND, RECOMMENDED_MODEL } from "./lib/catalog.js";
|
|
8
8
|
import { CONSOLE, KEY_ENV, KEYS_URL, discoverSetup, SetupError } from "./lib/config.js";
|
|
9
9
|
import { inspectEnvironment } from "./lib/env.js";
|
|
10
10
|
import { exchangeForKey } from "./lib/exchange.js";
|
|
@@ -22,12 +22,30 @@ import { LOGO, LOGO_COLUMNS, LOGO_ROWS } from "./ui/logo.js";
|
|
|
22
22
|
import { ModelSelect } from "./ui/table.js";
|
|
23
23
|
import { BAD, BRAND, MUTED, OK, WARN } from "./ui/theme.js";
|
|
24
24
|
const NAV = "↑↓ navigate enter select";
|
|
25
|
+
const STEPS = new Set(["welcome", "existing", "harnesses", "scope", "store", "model", "review"]);
|
|
26
|
+
const withBack = (hints, canBack) => (canBack ? `${hints} esc back` : hints);
|
|
25
27
|
function message(error) {
|
|
26
28
|
return error instanceof Error ? error.message : String(error);
|
|
27
29
|
}
|
|
28
30
|
export function App({ onFinish }) {
|
|
29
31
|
const { exit } = useApp();
|
|
30
32
|
const [screen, setScreen] = useState({ kind: "boot" });
|
|
33
|
+
const history = useRef([]).current;
|
|
34
|
+
const canBack = history.length > 0;
|
|
35
|
+
const go = (next) => {
|
|
36
|
+
if (STEPS.has(screen.kind)) {
|
|
37
|
+
history.push(screen);
|
|
38
|
+
}
|
|
39
|
+
setScreen(next);
|
|
40
|
+
};
|
|
41
|
+
const back = () => {
|
|
42
|
+
const previous = history.pop();
|
|
43
|
+
if (previous) {
|
|
44
|
+
setScreen(previous);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const forget = () => history.splice(0);
|
|
48
|
+
const pickModels = () => go(wizard.catalog.length > 0 ? { kind: "model", role: "primary" } : { kind: "catalog" });
|
|
31
49
|
const wizard = useRef({
|
|
32
50
|
background: "",
|
|
33
51
|
backups: [],
|
|
@@ -76,17 +94,17 @@ export function App({ onFinish }) {
|
|
|
76
94
|
smokes: wizard.smokes,
|
|
77
95
|
tests: wizard.tests,
|
|
78
96
|
});
|
|
79
|
-
const hasBackgroundSlot = () => chosen().some((harness) => harness.
|
|
97
|
+
const hasBackgroundSlot = () => chosen().some((harness) => harness.backgroundSlot);
|
|
80
98
|
const chooseModel = (role, id) => {
|
|
81
99
|
if (role === "primary") {
|
|
82
100
|
wizard.primary = id;
|
|
83
101
|
if (hasBackgroundSlot()) {
|
|
84
|
-
|
|
102
|
+
go({ kind: "model", role: "background" });
|
|
85
103
|
return;
|
|
86
104
|
}
|
|
87
105
|
}
|
|
88
106
|
wizard.background = id;
|
|
89
|
-
|
|
107
|
+
go({ kind: "review" });
|
|
90
108
|
};
|
|
91
109
|
const finish = () => {
|
|
92
110
|
onFinish(summary());
|
|
@@ -98,69 +116,70 @@ export function App({ onFinish }) {
|
|
|
98
116
|
case "welcome":
|
|
99
117
|
return (_jsx(Welcome, { onSelect: (choice) => {
|
|
100
118
|
if (choice === "continue") {
|
|
101
|
-
|
|
119
|
+
go(wizard.key ? { kind: "harnesses" } : wizard.existing?.valid ? { kind: "existing" } : { kind: "signin" });
|
|
102
120
|
}
|
|
103
121
|
else if (choice === "cancel") {
|
|
104
122
|
exit();
|
|
105
123
|
}
|
|
106
124
|
else {
|
|
107
|
-
|
|
125
|
+
go({ kind: "info", topic: choice });
|
|
108
126
|
}
|
|
109
127
|
}, wizard: wizard }));
|
|
110
128
|
case "info":
|
|
111
|
-
return _jsx(Info, { onBack:
|
|
129
|
+
return _jsx(Info, { onBack: back, topic: screen.topic });
|
|
112
130
|
case "existing":
|
|
113
|
-
return (_jsx(Existing, { onSelect: (choice) => {
|
|
131
|
+
return (_jsx(Existing, { canBack: canBack, onBack: back, onSelect: (choice) => {
|
|
114
132
|
if (choice === "reuse") {
|
|
115
133
|
wizard.key = wizard.existing?.key ?? null;
|
|
116
134
|
wizard.keyStore = wizard.existing?.source ?? "";
|
|
117
|
-
|
|
135
|
+
go({ kind: "harnesses" });
|
|
118
136
|
}
|
|
119
137
|
else if (choice === "new") {
|
|
120
|
-
|
|
138
|
+
go({ kind: "signin" });
|
|
121
139
|
}
|
|
122
140
|
else {
|
|
123
141
|
exit();
|
|
124
142
|
}
|
|
125
143
|
}, source: wizard.existing?.source ?? "" }));
|
|
126
144
|
case "signin":
|
|
127
|
-
return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel:
|
|
145
|
+
return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel: back, onError: (error) => fail(error, { kind: "signin" }), onToken: (token) => setScreen({ kind: "exchange", token }) }));
|
|
128
146
|
case "exchange":
|
|
129
147
|
return (_jsx(Exchange, { onDone: (key, name) => {
|
|
130
148
|
wizard.key = key;
|
|
131
149
|
wizard.keyName = name;
|
|
150
|
+
forget();
|
|
132
151
|
setScreen({ kind: "harnesses" });
|
|
133
152
|
}, onError: (error) => fail(error, error instanceof SetupError && error.code === "setup_recovery_required" ? null : screen), token: screen.token }));
|
|
134
153
|
case "error":
|
|
135
154
|
return (_jsx(ErrorScreen, { message: screen.message, onSelect: (choice) => (choice === "retry" && screen.retry ? setScreen(screen.retry) : exit()), retry: screen.retry !== null }));
|
|
136
155
|
case "harnesses":
|
|
137
|
-
return (_jsx(Harnesses, { initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onSubmit: (ids) => {
|
|
156
|
+
return (_jsx(Harnesses, { canBack: canBack, initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onBack: back, onSubmit: (ids) => {
|
|
138
157
|
wizard.harnesses = ids;
|
|
139
|
-
|
|
158
|
+
go({ kind: "scope" });
|
|
140
159
|
} }));
|
|
141
160
|
case "scope":
|
|
142
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { onSelect: (scope) => {
|
|
161
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { initial: wizard.scope, onCancel: back, onSelect: (scope) => {
|
|
143
162
|
wizard.scope = scope;
|
|
144
163
|
if (scope === "project") {
|
|
145
164
|
wizard.store = null;
|
|
146
165
|
wizard.keyFile = projectKeyFile(wizard.env.cwd);
|
|
147
166
|
wizard.keyStore = tilde(wizard.keyFile);
|
|
148
|
-
|
|
167
|
+
pickModels();
|
|
149
168
|
}
|
|
150
169
|
else {
|
|
151
|
-
|
|
170
|
+
go({ kind: "store" });
|
|
152
171
|
}
|
|
153
172
|
}, options: [
|
|
154
173
|
{ hint: "recommended", label: "My user account", value: "user" },
|
|
155
174
|
{ hint: tilde(wizard.env.cwd), label: "Only this project", value: "project" },
|
|
156
175
|
] })] }));
|
|
157
176
|
case "store":
|
|
158
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { onSelect: (kind) => {
|
|
177
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Where should I keep the key?" }), _jsx(Muted, { center: true, children: "Your shell profile will read it from there; the key itself never goes into a profile." }), _jsx(Select, { initial: wizard.store?.kind, onCancel: back, onSelect: (kind) => {
|
|
159
178
|
const store = wizard.stores.find((candidate) => candidate.kind === kind) ?? null;
|
|
160
179
|
wizard.store = store;
|
|
161
180
|
wizard.keyFile = store?.path ?? null;
|
|
162
181
|
wizard.keyStore = store?.label ?? "";
|
|
163
|
-
|
|
182
|
+
pickModels();
|
|
164
183
|
}, options: wizard.stores.map((store, index) => ({
|
|
165
184
|
hint: index === 0 ? "recommended" : undefined,
|
|
166
185
|
label: store.label,
|
|
@@ -172,11 +191,14 @@ export function App({ onFinish }) {
|
|
|
172
191
|
setScreen({ kind: "model", role: "primary" });
|
|
173
192
|
}, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
|
|
174
193
|
case "model":
|
|
175
|
-
return (_jsx(ModelPicker, { catalog: wizard.catalog,
|
|
194
|
+
return (_jsx(ModelPicker, { canBack: canBack, catalog: wizard.catalog, initial: screen.role === "primary" ? wizard.primary : wizard.background, onBack: back, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
|
|
176
195
|
case "review":
|
|
177
|
-
return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
|
|
196
|
+
return (_jsx(Review, { harnesses: chosen(), input: planInput(), canBack: canBack, onBack: back, onCancel: () => exit(), onPlans: (plans) => {
|
|
178
197
|
wizard.plans = plans;
|
|
179
|
-
}, onProceed: () =>
|
|
198
|
+
}, onProceed: () => {
|
|
199
|
+
forget();
|
|
200
|
+
setScreen({ kind: "apply" });
|
|
201
|
+
}, wizard: wizard }));
|
|
180
202
|
case "apply":
|
|
181
203
|
return (_jsx(Apply, { onDone: () => setScreen(wizard.plans.some(({ plan }) => plan.manual.length > 0) ? { kind: "manual" } : { kind: "test" }), onError: (error) => fail(error, { kind: "review" }), wizard: wizard }));
|
|
182
204
|
case "manual":
|
|
@@ -247,8 +269,8 @@ function Info({ onBack, topic }) {
|
|
|
247
269
|
});
|
|
248
270
|
return (_jsxs(Frame, { hints: "enter back", children: [_jsx(Title, { children: topic === "what" ? "What this does" : "Privacy & data" }), topic === "what" ? (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["1. Opens ", CONSOLE, "/authorize in your browser. You sign in (or sign up) and approve \"Boundless local setup\"."] }), _jsx(Para, { children: "2. Exchanges that approval for one inference key named setup-\u2026 on your account. The secret is shown once, to this program only." }), _jsxs(Para, { children: ["3. Stores the key in your credential store or a private 0600 file, and points your shell at it via ", KEY_ENV, "."] }), _jsx(Para, { children: "4. Backs up and merges each chosen harness's configuration, preserving unrelated providers and settings." }), _jsx(Para, { children: "5. Sends one small request per model to prove it works. That request uses account credits; nothing is purchased." })] })) : (_jsxs(_Fragment, { children: [_jsxs(Para, { children: ["Nothing from this directory is uploaded. The only network calls are to ", CONSOLE, ", its authorization server, and the inference gateway."] }), _jsx(Para, { children: "The sign-in token is used once for the key exchange and then discarded; no refresh token is kept." }), _jsx(Para, { children: "The API key is written only to the store you pick and to harness files that need it, all with owner-only permissions. It never appears in command arguments, shell history, logs, or this screen." }), _jsxs(Para, { children: ["Manage or revoke the key at ", KEYS_URL, "."] })] }))] }));
|
|
249
271
|
}
|
|
250
|
-
function Existing({ onSelect, source }) {
|
|
251
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: [
|
|
272
|
+
function Existing({ canBack, onBack, onSelect, source, }) {
|
|
273
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "A Boundless key is already configured" }), _jsxs(Muted, { center: true, children: ["Found in ", source, ", and the gateway accepts it."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: onSelect, options: [
|
|
252
274
|
{ hint: "recommended", label: "Reuse it", value: "reuse" },
|
|
253
275
|
{ label: "Sign in and create a separate key", value: "new" },
|
|
254
276
|
{ label: "Cancel", value: "cancel" },
|
|
@@ -294,14 +316,14 @@ function Exchange({ onDone, onError, token }) {
|
|
|
294
316
|
function ErrorScreen({ message: text, onSelect, retry }) {
|
|
295
317
|
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Something went wrong" }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: BAD, children: text }) }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: retry ? [{ label: "Try again", value: "retry" }, { label: "Cancel", value: "cancel" }] : [{ label: "Exit", value: "cancel" }] }) })] }));
|
|
296
318
|
}
|
|
297
|
-
function Harnesses({ initial, installed, onSubmit }) {
|
|
319
|
+
function Harnesses({ canBack, initial, installed, onBack, onSubmit, }) {
|
|
298
320
|
const [empty, setEmpty] = useState(false);
|
|
299
321
|
const options = HARNESSES.map((harness) => ({
|
|
300
322
|
hint: installed.has(harness.id) ? "installed" : "not found",
|
|
301
323
|
label: harness.name,
|
|
302
324
|
value: harness.id,
|
|
303
325
|
}));
|
|
304
|
-
return (_jsxs(Frame, { hints: "space toggle enter continue", children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
|
|
326
|
+
return (_jsxs(Frame, { hints: withBack("space toggle a all/none enter continue", canBack), children: [_jsx(Title, { children: "Which harnesses should I configure?" }), _jsx(Muted, { center: true, children: "Installed ones are preselected. Harnesses that are not installed get their configuration written for when they are." }), _jsx(Box, { justifyContent: "center", children: _jsx(MultiSelect, { initial: initial, onCancel: onBack, onSubmit: (ids) => (ids.length === 0 ? setEmpty(true) : onSubmit(ids)), options: options }) }), empty ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: WARN, children: "Pick at least one." }) })) : null] }));
|
|
305
327
|
}
|
|
306
328
|
function Catalog({ onDone, onError, wizard }) {
|
|
307
329
|
useEffect(() => {
|
|
@@ -309,12 +331,10 @@ function Catalog({ onDone, onError, wizard }) {
|
|
|
309
331
|
}, []);
|
|
310
332
|
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
|
|
311
333
|
}
|
|
312
|
-
function ModelPicker({ catalog,
|
|
313
|
-
|
|
314
|
-
const recommended = role === "primary" ? lead?.defaultModel : lead?.backgroundDefault;
|
|
315
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog", lead ? `; the recommendation is ${lead.name}'s documented default.` : "."] }), _jsx(ModelSelect, { models: catalog, onSelect: onSelect, recommended: recommended ?? undefined }, role)] }));
|
|
334
|
+
function ModelPicker({ canBack, catalog, initial, onBack, onSelect, role, }) {
|
|
335
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { initial: initial, models: catalog, onBack: onBack, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
|
|
316
336
|
}
|
|
317
|
-
function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
337
|
+
function Review({ canBack, harnesses, input, onBack, onCancel, onPlans, onProceed, wizard, }) {
|
|
318
338
|
const [plans, setPlans] = useState(null);
|
|
319
339
|
useEffect(() => {
|
|
320
340
|
Promise.all(harnesses.map(async (harness) => ({ harness, plan: await harness.plan(input) }))).then((planned) => {
|
|
@@ -331,11 +351,11 @@ function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
|
331
351
|
: env.os === "windows"
|
|
332
352
|
? "user environment variable"
|
|
333
353
|
: `exported as ${KEY_ENV} by your shell profile`;
|
|
334
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
|
|
354
|
+
return (_jsxs(Frame, { hints: withBack(NAV, canBack), children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
|
|
335
355
|
{ label: "Key", value: `${wizard.keyStore} · ${exposure}` },
|
|
336
356
|
{ label: "Model", value: wizard.primary },
|
|
337
357
|
...(wizard.background !== wizard.primary ? [{ label: "Background", value: wizard.background }] : []),
|
|
338
|
-
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
|
|
358
|
+
] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.map(({ harness, plan }) => (_jsxs(Box, { children: [_jsx(Text, { children: harness.name.padEnd(16) }), _jsx(Text, { color: plan.changes.length > 0 ? BRAND : MUTED, children: plan.changes.length > 0 ? plan.changes.map((change) => tilde(change.path)).join(", ") : "manual steps, shown after apply" })] }, harness.id))) }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: plans.flatMap(({ plan }) => plan.notes.slice(0, 1)).map((note) => (_jsx(Text, { color: MUTED, children: note }, note))) }), _jsx(Muted, { children: "Existing files are backed up next to themselves before they change. Credentials are never displayed." }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onCancel: onBack, onSelect: (choice) => (choice === "apply" ? onProceed() : onCancel()), options: [
|
|
339
359
|
{ label: "Apply", value: "apply" },
|
|
340
360
|
{ label: "Cancel", value: "cancel" },
|
|
341
361
|
] }) })] }));
|
|
@@ -398,7 +418,7 @@ function Test({ harnesses, onDone, wizard }) {
|
|
|
398
418
|
const pairs = new Map();
|
|
399
419
|
for (const harness of harnesses) {
|
|
400
420
|
pairs.set(`${harness.wire}:${wizard.primary}`, { model: wizard.primary, wire: harness.wire });
|
|
401
|
-
if (harness.
|
|
421
|
+
if (harness.backgroundSlot) {
|
|
402
422
|
pairs.set(`${harness.wire}:${wizard.background}`, { model: wizard.background, wire: harness.wire });
|
|
403
423
|
}
|
|
404
424
|
}
|
|
@@ -6,8 +6,7 @@ function settingsPath(input) {
|
|
|
6
6
|
return input.scope === "user" ? join(claudeHome(), "settings.json") : join(input.env.cwd, ".claude", "settings.local.json");
|
|
7
7
|
}
|
|
8
8
|
export const claudeCode = {
|
|
9
|
-
|
|
10
|
-
defaultModel: "glm-5.2",
|
|
9
|
+
backgroundSlot: true,
|
|
11
10
|
detect: () => installed("claude"),
|
|
12
11
|
id: "claude-code",
|
|
13
12
|
name: "Claude Code",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
const VENDOR = "Boundless";
|
|
4
|
+
export const codebuddy = {
|
|
5
|
+
backgroundSlot: false,
|
|
6
|
+
detect: () => installed("codebuddy"),
|
|
7
|
+
id: "codebuddy",
|
|
8
|
+
name: "CodeBuddy",
|
|
9
|
+
plan: (input) => {
|
|
10
|
+
const path = home(".codebuddy", "models.json");
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = await readJson(path);
|
|
16
|
+
const others = (Array.isArray(current.models) ? current.models : []).filter((entry) => !(typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.vendor === VENDOR));
|
|
17
|
+
const ours = input.catalog.map((model) => ({
|
|
18
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
19
|
+
id: model.id,
|
|
20
|
+
maxInputTokens: model.contextTokens,
|
|
21
|
+
maxOutputTokens: 32_000,
|
|
22
|
+
name: model.id,
|
|
23
|
+
supportsReasoning: model.supports.includes("reasoning"),
|
|
24
|
+
supportsToolCall: model.supports.includes("tool-use"),
|
|
25
|
+
url: `${input.gatewayUrl}/v1/chat/completions`,
|
|
26
|
+
vendor: VENDOR,
|
|
27
|
+
}));
|
|
28
|
+
const available = Array.isArray(current.availableModels) ? current.availableModels : [];
|
|
29
|
+
const ids = new Set(input.catalog.map((model) => model.id));
|
|
30
|
+
const next = {
|
|
31
|
+
...current,
|
|
32
|
+
availableModels: [...available.filter((id) => typeof id !== "string" || !ids.has(id)), ...ids],
|
|
33
|
+
models: [...others, ...ours],
|
|
34
|
+
};
|
|
35
|
+
const saved = await backup(path);
|
|
36
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
37
|
+
return { backup: saved };
|
|
38
|
+
},
|
|
39
|
+
path,
|
|
40
|
+
summary: `${input.catalog.length} Boundless models at ${input.gatewayUrl}/v1/chat/completions, key read from ${KEY_ENV}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
manual: [],
|
|
44
|
+
notes: [`CodeBuddy has no default-model setting: start it with codebuddy --model "${input.primary}" or pick it in the IDE (tagged custom).`],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
wire: "openai",
|
|
48
|
+
};
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -5,8 +5,7 @@ import { codexHome } from "../lib/env.js";
|
|
|
5
5
|
import { installed } from "../lib/files.js";
|
|
6
6
|
import { backup, readText, tilde, writePrivate } from "../lib/files.js";
|
|
7
7
|
export const codex = {
|
|
8
|
-
|
|
9
|
-
defaultModel: "qwen3.6",
|
|
8
|
+
backgroundSlot: false,
|
|
10
9
|
detect: () => installed("codex"),
|
|
11
10
|
id: "codex",
|
|
12
11
|
name: "Codex CLI",
|
package/dist/harnesses/gui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { anyExists, home } from "../lib/files.js";
|
|
1
|
+
import { anyExists, home, installed } from "../lib/files.js";
|
|
2
2
|
import { readExpression } from "../lib/store.js";
|
|
3
3
|
function reveal(input) {
|
|
4
4
|
if (input.keyFile) {
|
|
@@ -7,8 +7,7 @@ function reveal(input) {
|
|
|
7
7
|
return input.store ? readExpression(input.store) : "echo $BOUNDLESS_API_KEY";
|
|
8
8
|
}
|
|
9
9
|
export const cursor = {
|
|
10
|
-
|
|
11
|
-
defaultModel: "glm-5.2",
|
|
10
|
+
backgroundSlot: false,
|
|
12
11
|
detect: () => anyExists(["/Applications/Cursor.app", home(".cursor")]),
|
|
13
12
|
id: "cursor",
|
|
14
13
|
name: "Cursor",
|
|
@@ -25,8 +24,7 @@ export const cursor = {
|
|
|
25
24
|
wire: "openai",
|
|
26
25
|
};
|
|
27
26
|
export const copilot = {
|
|
28
|
-
|
|
29
|
-
defaultModel: "glm-5.2",
|
|
27
|
+
backgroundSlot: false,
|
|
30
28
|
detect: () => anyExists([home(".vscode"), home(".copilot")]),
|
|
31
29
|
id: "copilot",
|
|
32
30
|
name: "GitHub Copilot",
|
|
@@ -41,3 +39,50 @@ export const copilot = {
|
|
|
41
39
|
}),
|
|
42
40
|
wire: "openai",
|
|
43
41
|
};
|
|
42
|
+
export const cline = {
|
|
43
|
+
backgroundSlot: false,
|
|
44
|
+
detect: () => anyExists([home(".vscode", "extensions"), home(".cursor", "extensions")]),
|
|
45
|
+
id: "cline",
|
|
46
|
+
name: "Cline",
|
|
47
|
+
plan: (input) => Promise.resolve({
|
|
48
|
+
changes: [],
|
|
49
|
+
manual: [
|
|
50
|
+
'In Cline choose "OpenAI Compatible" as the API provider.',
|
|
51
|
+
`Base URL ${input.gatewayUrl}/v1, model ID ${input.primary}, key revealed with: ${reveal(input)}`,
|
|
52
|
+
"In Model Configuration set the context window and max output tokens from the model lineup; Cline's defaults are conservative.",
|
|
53
|
+
],
|
|
54
|
+
notes: [],
|
|
55
|
+
}),
|
|
56
|
+
wire: "openai",
|
|
57
|
+
};
|
|
58
|
+
export const cherryStudio = {
|
|
59
|
+
backgroundSlot: false,
|
|
60
|
+
detect: () => anyExists(["/Applications/Cherry Studio.app", home(".config", "CherryStudio")]),
|
|
61
|
+
id: "cherry-studio",
|
|
62
|
+
name: "Cherry Studio",
|
|
63
|
+
plan: (input) => Promise.resolve({
|
|
64
|
+
changes: [],
|
|
65
|
+
manual: [
|
|
66
|
+
"Settings → Model Provider → Add Provider, endpoint type OpenAI.",
|
|
67
|
+
`API address ${input.gatewayUrl} (without /v1; Cherry appends it), key revealed with: ${reveal(input)}`,
|
|
68
|
+
`In the provider wizard select all models under "Choose models", then "Verify and enable"; start on ${input.primary}.`,
|
|
69
|
+
"Settings → Default Model: point Default Assistant Model, Quick Model and the translate model at this provider.",
|
|
70
|
+
],
|
|
71
|
+
notes: ["Cherry Studio stores the key in its own database; it cannot read an environment variable."],
|
|
72
|
+
}),
|
|
73
|
+
wire: "openai",
|
|
74
|
+
};
|
|
75
|
+
export const muse = {
|
|
76
|
+
backgroundSlot: false,
|
|
77
|
+
detect: () => installed("muse"),
|
|
78
|
+
id: "muse",
|
|
79
|
+
name: "Muse",
|
|
80
|
+
plan: (input) => Promise.resolve({
|
|
81
|
+
changes: [],
|
|
82
|
+
manual: [
|
|
83
|
+
`Muse has no provider setting; run it as: META_API_KEY="$BOUNDLESS_API_KEY" muse --provider meta --base-url "${input.gatewayUrl}/v1" --model "${input.primary}"`,
|
|
84
|
+
],
|
|
85
|
+
notes: ["Both Muse flags are undocumented and unverified against this gateway."],
|
|
86
|
+
}),
|
|
87
|
+
wire: "openai",
|
|
88
|
+
};
|
package/dist/harnesses/hermes.js
CHANGED
|
@@ -3,8 +3,7 @@ import { KEY_ENV } from "../lib/config.js";
|
|
|
3
3
|
import { installed } from "../lib/files.js";
|
|
4
4
|
import { backup, home, readText, writePrivate } from "../lib/files.js";
|
|
5
5
|
export const hermes = {
|
|
6
|
-
|
|
7
|
-
defaultModel: "qwen3.6",
|
|
6
|
+
backgroundSlot: false,
|
|
8
7
|
detect: () => installed("hermes"),
|
|
9
8
|
id: "hermes",
|
|
10
9
|
name: "Hermes",
|
package/dist/harnesses/index.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { claudeCode } from "./claude-code.js";
|
|
2
|
+
import { codebuddy } from "./codebuddy.js";
|
|
2
3
|
import { codex } from "./codex.js";
|
|
3
|
-
import { copilot, cursor } from "./gui.js";
|
|
4
|
+
import { cherryStudio, cline, copilot, cursor, muse } from "./gui.js";
|
|
4
5
|
import { hermes } from "./hermes.js";
|
|
5
6
|
import { omp } from "./omp.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import { openclaw } from "./openclaw.js";
|
|
8
|
+
import { kiloCode, mimoCode, opencode } from "./opencode.js";
|
|
9
|
+
import { openhands } from "./openhands.js";
|
|
10
|
+
import { qwenCode } from "./qwen-code.js";
|
|
11
|
+
export const HARNESSES = [
|
|
12
|
+
claudeCode,
|
|
13
|
+
codex,
|
|
14
|
+
opencode,
|
|
15
|
+
hermes,
|
|
16
|
+
omp,
|
|
17
|
+
cursor,
|
|
18
|
+
copilot,
|
|
19
|
+
openhands,
|
|
20
|
+
cline,
|
|
21
|
+
openclaw,
|
|
22
|
+
kiloCode,
|
|
23
|
+
cherryStudio,
|
|
24
|
+
codebuddy,
|
|
25
|
+
mimoCode,
|
|
26
|
+
qwenCode,
|
|
27
|
+
muse,
|
|
28
|
+
];
|
package/dist/harnesses/omp.js
CHANGED
|
@@ -3,8 +3,7 @@ import { KEY_ENV } from "../lib/config.js";
|
|
|
3
3
|
import { installed } from "../lib/files.js";
|
|
4
4
|
import { backup, home, readText, writePrivate } from "../lib/files.js";
|
|
5
5
|
export const omp = {
|
|
6
|
-
|
|
7
|
-
defaultModel: "glm-5.2",
|
|
6
|
+
backgroundSlot: true,
|
|
8
7
|
detect: () => installed("omp"),
|
|
9
8
|
id: "omp",
|
|
10
9
|
name: "omp (Oh My Pi)",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
export const openclaw = {
|
|
4
|
+
backgroundSlot: false,
|
|
5
|
+
detect: () => installed("openclaw"),
|
|
6
|
+
id: "openclaw",
|
|
7
|
+
name: "OpenClaw",
|
|
8
|
+
plan: (input) => {
|
|
9
|
+
const path = home(".openclaw", "openclaw.json");
|
|
10
|
+
return Promise.resolve({
|
|
11
|
+
changes: [
|
|
12
|
+
{
|
|
13
|
+
apply: async () => {
|
|
14
|
+
const current = await readJson(path);
|
|
15
|
+
const next = merge(current, {
|
|
16
|
+
agents: { defaults: { model: { primary: `boundless/${input.primary}` } } },
|
|
17
|
+
models: {
|
|
18
|
+
providers: {
|
|
19
|
+
boundless: {
|
|
20
|
+
api: "openai-completions",
|
|
21
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
22
|
+
baseUrl: `${input.gatewayUrl}/v1`,
|
|
23
|
+
models: input.catalog.map((model) => ({
|
|
24
|
+
contextWindow: model.contextTokens,
|
|
25
|
+
id: model.id,
|
|
26
|
+
maxTokens: 32_000,
|
|
27
|
+
name: model.name,
|
|
28
|
+
reasoning: model.supports.includes("reasoning"),
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const saved = await backup(path);
|
|
35
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
36
|
+
return { backup: saved };
|
|
37
|
+
},
|
|
38
|
+
path,
|
|
39
|
+
summary: `provider boundless with ${input.catalog.length} models, primary boundless/${input.primary}, key read from ${KEY_ENV}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
manual: [],
|
|
43
|
+
notes: [`OpenClaw reads the key from ${KEY_ENV} in your shell environment.`],
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
wire: "openai",
|
|
47
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { KEY_ENV } from "../lib/config.js";
|
|
3
|
-
import { installed } from "../lib/files.js";
|
|
4
|
-
import { backup, home, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
5
4
|
function modelEntry(model) {
|
|
6
5
|
const inputs = ["text", ...["image", "audio", "video"].filter((kind) => model.supports.includes(`${kind} input`))];
|
|
7
6
|
return {
|
|
@@ -12,55 +11,79 @@ function modelEntry(model) {
|
|
|
12
11
|
...(inputs.length > 1 ? { modalities: { input: inputs } } : {}),
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/** OpenCode and its forks share one config format: a provider block plus model/small_model. */
|
|
15
|
+
function opencodeFamily(family) {
|
|
16
|
+
const configPath = (input) => input.scope === "project"
|
|
17
|
+
? join(input.env.cwd, family.file)
|
|
18
|
+
: join(process.env.XDG_CONFIG_HOME ?? home(".config"), family.dir, family.file);
|
|
19
|
+
return {
|
|
20
|
+
backgroundSlot: true,
|
|
21
|
+
detect: () => installed(family.binary),
|
|
22
|
+
id: family.id,
|
|
23
|
+
name: family.name,
|
|
24
|
+
plan: (input) => {
|
|
25
|
+
const path = configPath(input);
|
|
26
|
+
const apiKey = input.scope === "project" && input.keyFile ? `{file:${input.keyFile}}` : `{env:${KEY_ENV}}`;
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
changes: [
|
|
29
|
+
{
|
|
30
|
+
apply: async () => {
|
|
31
|
+
const current = await readJson(path);
|
|
32
|
+
const next = merge(current, {
|
|
33
|
+
$schema: family.schema,
|
|
34
|
+
model: `boundless/${input.primary}`,
|
|
35
|
+
provider: {
|
|
36
|
+
boundless: {
|
|
37
|
+
models: Object.fromEntries(input.catalog.map((model) => [model.id, modelEntry(model)])),
|
|
38
|
+
name: "Boundless",
|
|
39
|
+
npm: "@ai-sdk/openai-compatible",
|
|
40
|
+
options: { apiKey, baseURL: `${input.gatewayUrl}/v1` },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
small_model: `boundless/${input.background}`,
|
|
44
|
+
});
|
|
45
|
+
const saved = await backup(path);
|
|
46
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
47
|
+
return { backup: saved };
|
|
48
|
+
},
|
|
49
|
+
path,
|
|
50
|
+
summary: `provider boundless with ${input.catalog.length} models, model boundless/${input.primary}, small_model boundless/${input.background}, apiKey ${apiKey}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
manual: [],
|
|
54
|
+
notes: family.file.endsWith(".jsonc") ? [`Comments in an existing ${family.file} are not preserved; the backup keeps the original.`] : [],
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
smoke: family.smoke,
|
|
58
|
+
wire: "openai",
|
|
59
|
+
};
|
|
20
60
|
}
|
|
21
|
-
export const opencode = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
export const opencode = opencodeFamily({
|
|
62
|
+
binary: "opencode",
|
|
63
|
+
dir: "opencode",
|
|
64
|
+
file: "opencode.json",
|
|
25
65
|
id: "opencode",
|
|
26
66
|
name: "OpenCode",
|
|
27
|
-
|
|
28
|
-
const path = configPath(input);
|
|
29
|
-
const apiKey = input.scope === "project" && input.keyFile ? `{file:${input.keyFile}}` : `{env:${KEY_ENV}}`;
|
|
30
|
-
return Promise.resolve({
|
|
31
|
-
changes: [
|
|
32
|
-
{
|
|
33
|
-
apply: async () => {
|
|
34
|
-
const current = await readJson(path);
|
|
35
|
-
const next = merge(current, {
|
|
36
|
-
$schema: "https://opencode.ai/config.json",
|
|
37
|
-
model: `boundless/${input.primary}`,
|
|
38
|
-
provider: {
|
|
39
|
-
boundless: {
|
|
40
|
-
models: Object.fromEntries(input.catalog.map((model) => [model.id, modelEntry(model)])),
|
|
41
|
-
name: "Boundless",
|
|
42
|
-
npm: "@ai-sdk/openai-compatible",
|
|
43
|
-
options: { apiKey, baseURL: `${input.gatewayUrl}/v1` },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
small_model: `boundless/${input.background}`,
|
|
47
|
-
});
|
|
48
|
-
const saved = await backup(path);
|
|
49
|
-
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
50
|
-
return { backup: saved };
|
|
51
|
-
},
|
|
52
|
-
path,
|
|
53
|
-
summary: `provider boundless with ${input.catalog.length} models, model boundless/${input.primary}, small_model boundless/${input.background}, apiKey ${apiKey}`,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
manual: [],
|
|
57
|
-
notes: [],
|
|
58
|
-
});
|
|
59
|
-
},
|
|
67
|
+
schema: "https://opencode.ai/config.json",
|
|
60
68
|
smoke: (input) => ({
|
|
61
69
|
args: ["run", "--model", `boundless/${input.primary}`, "Use the bash tool to run printf boundless, then report the output."],
|
|
62
70
|
command: "opencode",
|
|
63
71
|
expect: /boundless/i,
|
|
64
72
|
}),
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
});
|
|
74
|
+
export const kiloCode = opencodeFamily({
|
|
75
|
+
binary: "kilo",
|
|
76
|
+
dir: "kilo",
|
|
77
|
+
file: "kilo.jsonc",
|
|
78
|
+
id: "kilo-code",
|
|
79
|
+
name: "Kilo Code",
|
|
80
|
+
schema: "https://app.kilo.ai/config.json",
|
|
81
|
+
});
|
|
82
|
+
export const mimoCode = opencodeFamily({
|
|
83
|
+
binary: "mimo",
|
|
84
|
+
dir: "mimocode",
|
|
85
|
+
file: "mimocode.jsonc",
|
|
86
|
+
id: "mimo-code",
|
|
87
|
+
name: "MiMo Code",
|
|
88
|
+
schema: "https://mimo.xiaomi.com/mimocode/config.json",
|
|
89
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { parse, stringify } from "smol-toml";
|
|
3
|
+
import { backup, installed, readText, tilde, writePrivate } from "../lib/files.js";
|
|
4
|
+
export const openhands = {
|
|
5
|
+
backgroundSlot: false,
|
|
6
|
+
detect: () => installed("openhands"),
|
|
7
|
+
id: "openhands",
|
|
8
|
+
name: "OpenHands",
|
|
9
|
+
plan: (input) => {
|
|
10
|
+
const path = join(input.env.cwd, "config.toml");
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = parse((await readText(path)) ?? "");
|
|
16
|
+
const llm = (current.llm ?? {});
|
|
17
|
+
const next = {
|
|
18
|
+
...current,
|
|
19
|
+
llm: { ...llm, api_key: input.key, base_url: `${input.gatewayUrl}/v1`, model: `openai/${input.primary}` },
|
|
20
|
+
};
|
|
21
|
+
const saved = await backup(path);
|
|
22
|
+
await writePrivate(path, stringify(next));
|
|
23
|
+
return { backup: saved };
|
|
24
|
+
},
|
|
25
|
+
path,
|
|
26
|
+
summary: `[llm] model openai/${input.primary}, base_url ${input.gatewayUrl}/v1, api_key (key kept in this 0600 file)`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
manual: [],
|
|
30
|
+
notes: [
|
|
31
|
+
`OpenHands reads config.toml from the working directory, so it is written to ${tilde(path)} and holds the key itself; keep it out of git.`,
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
wire: "openai",
|
|
36
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
export const qwenCode = {
|
|
4
|
+
backgroundSlot: false,
|
|
5
|
+
detect: () => installed("qwen"),
|
|
6
|
+
id: "qwen-code",
|
|
7
|
+
name: "Qwen Code",
|
|
8
|
+
plan: (input) => {
|
|
9
|
+
const path = home(".qwen", "settings.json");
|
|
10
|
+
const baseUrl = `${input.gatewayUrl}/v1`;
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = await readJson(path);
|
|
16
|
+
const providers = current.modelProviders;
|
|
17
|
+
const existing = typeof providers === "object" && providers !== null && !Array.isArray(providers) && Array.isArray(providers.openai)
|
|
18
|
+
? providers.openai
|
|
19
|
+
: [];
|
|
20
|
+
const others = existing.filter((entry) => !(typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.baseUrl === baseUrl));
|
|
21
|
+
const ours = input.catalog.map((model) => ({ baseUrl, envKey: KEY_ENV, id: model.id, name: model.id }));
|
|
22
|
+
const next = merge(current, {
|
|
23
|
+
model: { name: input.primary },
|
|
24
|
+
modelProviders: { openai: [...others, ...ours] },
|
|
25
|
+
security: { auth: { selectedType: "openai" } },
|
|
26
|
+
});
|
|
27
|
+
const saved = await backup(path);
|
|
28
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
29
|
+
return { backup: saved };
|
|
30
|
+
},
|
|
31
|
+
path,
|
|
32
|
+
summary: `${input.catalog.length} models under modelProviders.openai, model ${input.primary}, key read from ${KEY_ENV}`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
manual: [],
|
|
36
|
+
notes: [`Qwen Code reads the key from ${KEY_ENV} in your shell environment (or ~/.qwen/.env).`],
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
wire: "openai",
|
|
40
|
+
};
|
package/dist/lib/catalog.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { CONSOLE, request, SetupError } from "./config.js";
|
|
2
|
+
/** One recommendation for every harness: the default model, and the model for cheap background slots. */
|
|
3
|
+
export const RECOMMENDED_MODEL = "deepseek-v4.1-flash";
|
|
4
|
+
export const RECOMMENDED_BACKGROUND = "dsv4";
|
|
2
5
|
export async function listModels(gatewayUrl, key) {
|
|
3
6
|
const [catalog, served] = await Promise.all([
|
|
4
7
|
request(`${CONSOLE}/api/catalog`),
|
|
@@ -24,6 +27,7 @@ export async function keyWorks(gatewayUrl, key) {
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
/** Exact USD, never rounded away: trailing zeros trimmed, up to twelve decimals. */
|
|
30
|
+
export const usdPerMillion = (amount) => `$${amount.toFixed(2)}`;
|
|
27
31
|
export function usd(amount) {
|
|
28
32
|
return `$${amount.toFixed(12).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
29
33
|
}
|
package/dist/lib/files.js
CHANGED
|
@@ -85,12 +85,41 @@ export function merge(base, patch) {
|
|
|
85
85
|
}
|
|
86
86
|
return patch;
|
|
87
87
|
}
|
|
88
|
+
// Drops line and block comments outside strings so a .jsonc file parses. Comments are not written back.
|
|
89
|
+
export function stripComments(text) {
|
|
90
|
+
let out = "";
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < text.length) {
|
|
93
|
+
const ch = text[i];
|
|
94
|
+
if (ch === '"') {
|
|
95
|
+
let j = i + 1;
|
|
96
|
+
while (j < text.length && text[j] !== '"') {
|
|
97
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
98
|
+
}
|
|
99
|
+
out += text.slice(i, j + 1);
|
|
100
|
+
i = j + 1;
|
|
101
|
+
}
|
|
102
|
+
else if (ch === "/" && text[i + 1] === "/") {
|
|
103
|
+
i = text.indexOf("\n", i);
|
|
104
|
+
i = i === -1 ? text.length : i;
|
|
105
|
+
}
|
|
106
|
+
else if (ch === "/" && text[i + 1] === "*") {
|
|
107
|
+
const end = text.indexOf("*/", i + 2);
|
|
108
|
+
i = end === -1 ? text.length : end + 2;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out += ch;
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
88
117
|
export async function readJson(path) {
|
|
89
118
|
const text = await readText(path);
|
|
90
119
|
if (text === null || text.trim() === "") {
|
|
91
120
|
return {};
|
|
92
121
|
}
|
|
93
|
-
const parsed = JSON.parse(text);
|
|
122
|
+
const parsed = JSON.parse(stripComments(text).replace(/,(\s*[}\]])/g, "$1"));
|
|
94
123
|
if (!isObject(parsed)) {
|
|
95
124
|
throw new Error(`${path} is not a JSON object`);
|
|
96
125
|
}
|
package/dist/ui/menu.js
CHANGED
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { BRAND, MUTED } from "./theme.js";
|
|
5
|
-
export function Select({ onCancel, onSelect, options, }) {
|
|
6
|
-
const [index, setIndex] = useState(0);
|
|
5
|
+
export function Select({ initial, onCancel, onSelect, options, }) {
|
|
6
|
+
const [index, setIndex] = useState(Math.max(options.findIndex((option) => option.value === initial), 0));
|
|
7
7
|
const move = (delta) => {
|
|
8
8
|
let next = index;
|
|
9
9
|
for (let step = 0; step < options.length; step += 1) {
|
|
@@ -46,6 +46,9 @@ export function MultiSelect({ initial, onCancel, onSubmit, options, }) {
|
|
|
46
46
|
else if (key.downArrow || input === "j") {
|
|
47
47
|
setIndex((index + 1) % options.length);
|
|
48
48
|
}
|
|
49
|
+
else if (input === "a") {
|
|
50
|
+
setChosen(chosen.size === options.length ? new Set() : new Set(options.map((option) => option.value)));
|
|
51
|
+
}
|
|
49
52
|
else if (input === " ") {
|
|
50
53
|
const value = options[index]?.value;
|
|
51
54
|
if (value !== undefined) {
|
package/dist/ui/table.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { usdPerMillion } from "../lib/catalog.js";
|
|
5
5
|
import { BRAND, MUTED } from "./theme.js";
|
|
6
6
|
const HEADER = ["ID", "Input /1M", "Output /1M", "Context", ""];
|
|
7
7
|
const cells = (model, recommended) => [
|
|
8
8
|
model.id,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
usdPerMillion(model.inputPerMillionTokens),
|
|
10
|
+
usdPerMillion(model.outputPerMillionTokens),
|
|
11
11
|
model.contextTokens.toLocaleString("en-US"),
|
|
12
12
|
model.id === recommended ? "recommended" : "",
|
|
13
13
|
];
|
|
14
|
-
export function ModelSelect({ models, onSelect, recommended, }) {
|
|
15
|
-
const start = Math.max(models.findIndex((model) => model.id === recommended), 0);
|
|
14
|
+
export function ModelSelect({ initial, models, onBack, onSelect, recommended, }) {
|
|
15
|
+
const start = Math.max(models.findIndex((model) => model.id === initial), models.findIndex((model) => model.id === recommended), 0);
|
|
16
16
|
const [index, setIndex] = useState(start);
|
|
17
17
|
useInput((input, key) => {
|
|
18
|
-
if (key.
|
|
18
|
+
if (key.escape) {
|
|
19
|
+
onBack();
|
|
20
|
+
}
|
|
21
|
+
else if (key.upArrow || input === "k") {
|
|
19
22
|
setIndex((index + models.length - 1) % models.length);
|
|
20
23
|
}
|
|
21
24
|
else if (key.downArrow || input === "j") {
|
package/package.json
CHANGED