@boundless.network/setup 0.1.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 +42 -0
- package/dist/app.js +471 -0
- package/dist/cli.js +37 -0
- package/dist/harnesses/claude-code.js +55 -0
- package/dist/harnesses/codex.js +61 -0
- package/dist/harnesses/gui.js +43 -0
- package/dist/harnesses/hermes.js +46 -0
- package/dist/harnesses/index.js +7 -0
- package/dist/harnesses/omp.js +41 -0
- package/dist/harnesses/opencode.js +66 -0
- package/dist/harnesses/types.js +1 -0
- package/dist/lib/browser.js +21 -0
- package/dist/lib/catalog.js +29 -0
- package/dist/lib/config.js +42 -0
- package/dist/lib/env.js +35 -0
- package/dist/lib/exchange.js +55 -0
- package/dist/lib/files.js +116 -0
- package/dist/lib/oauth.js +89 -0
- package/dist/lib/smoke.js +36 -0
- package/dist/lib/store.js +157 -0
- package/dist/lib/test.js +56 -0
- package/dist/summary.js +14 -0
- package/dist/ui/facts.js +19 -0
- package/dist/ui/frame.js +21 -0
- package/dist/ui/menu.js +74 -0
- package/dist/ui/spinner.js +13 -0
- package/dist/ui/table.js +17 -0
- package/dist/ui/text-input.js +22 -0
- package/dist/ui/theme.js +5 -0
- package/dist/version.js +3 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @boundless.network/setup
|
|
2
|
+
|
|
3
|
+
The Boundless setup wizard for your terminal.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx -y @boundless.network/setup@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It does what [boundless.md](https://boundless.md) asks a coding agent to do, as a full-screen terminal app:
|
|
10
|
+
|
|
11
|
+
1. Inspects the machine: OS, shell, remote session, installed harnesses, an existing key.
|
|
12
|
+
2. Signs you in through your browser with the OAuth device grant, or shows a code when there is no browser.
|
|
13
|
+
3. Exchanges that approval for one inference key on your account. The secret is never printed.
|
|
14
|
+
4. Stores the key in the macOS Keychain, the Linux Secret Service, or a private 0600 file, and exports `BOUNDLESS_API_KEY` from your shell profile without ever writing the key into the profile.
|
|
15
|
+
5. Lets you pick a default and a background model from the live catalog, with prices.
|
|
16
|
+
6. Backs up and merges configuration for Claude Code, Codex CLI, OpenCode, Hermes and omp, and prints the steps for Cursor and GitHub Copilot.
|
|
17
|
+
7. Sends one small request per model and wire format, with the exact cost, and optionally runs each installed harness once from a scratch directory.
|
|
18
|
+
|
|
19
|
+
Project-only scope keeps the key in `.boundless/api-key` (git-ignored) and writes project-level configuration where the harness supports it.
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install
|
|
25
|
+
npm run build
|
|
26
|
+
BOUNDLESS_CONSOLE=http://localhost:3000 node dist/cli.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`BOUNDLESS_CONSOLE` points the wizard at another console; it reads the OAuth client, issuer and gateway from `GET /api/setup` there. Plain HTTP is accepted for loopback only.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Publishing
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm version patch
|
|
39
|
+
npm publish
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package is public under the `@boundless.network` npm organisation. `npm publish` runs `prepublishOnly`, which builds `dist/`.
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
// Async screens run their task exactly once, on mount; the wizard object is mutable and shared.
|
|
5
|
+
import { HARNESSES } from "./harnesses/index.js";
|
|
6
|
+
import { openBrowser } from "./lib/browser.js";
|
|
7
|
+
import { keyWorks, listModels } from "./lib/catalog.js";
|
|
8
|
+
import { CONSOLE, KEY_ENV, KEYS_URL, discoverSetup, SetupError } from "./lib/config.js";
|
|
9
|
+
import { inspectEnvironment } from "./lib/env.js";
|
|
10
|
+
import { exchangeForKey } from "./lib/exchange.js";
|
|
11
|
+
import { tilde } from "./lib/files.js";
|
|
12
|
+
import { approvalUrl, startDeviceGrant, waitForAccessToken } from "./lib/oauth.js";
|
|
13
|
+
import { runSmoke } from "./lib/smoke.js";
|
|
14
|
+
import { availableStores, exposeInShell, findExistingKey, projectKeyFile, saveKey, saveProjectKey, } from "./lib/store.js";
|
|
15
|
+
import { testRequest } from "./lib/test.js";
|
|
16
|
+
import { headline } from "./summary.js";
|
|
17
|
+
import { Facts } from "./ui/facts.js";
|
|
18
|
+
import { Frame, Muted, Para, Title } from "./ui/frame.js";
|
|
19
|
+
import { MultiSelect, Select } from "./ui/menu.js";
|
|
20
|
+
import { Spinner } from "./ui/spinner.js";
|
|
21
|
+
import { ModelTable } from "./ui/table.js";
|
|
22
|
+
import { TextInput } from "./ui/text-input.js";
|
|
23
|
+
import { BAD, BRAND, MUTED, OK, WARN } from "./ui/theme.js";
|
|
24
|
+
const NAV = "↑↓ navigate enter select";
|
|
25
|
+
function message(error) {
|
|
26
|
+
return error instanceof Error ? error.message : String(error);
|
|
27
|
+
}
|
|
28
|
+
export function App({ onFinish }) {
|
|
29
|
+
const { exit } = useApp();
|
|
30
|
+
const [screen, setScreen] = useState({ kind: "boot" });
|
|
31
|
+
const wizard = useRef({
|
|
32
|
+
background: "",
|
|
33
|
+
backups: [],
|
|
34
|
+
catalog: [],
|
|
35
|
+
changed: [],
|
|
36
|
+
config: null,
|
|
37
|
+
installed: new Set(),
|
|
38
|
+
env: null,
|
|
39
|
+
existing: null,
|
|
40
|
+
harnesses: [],
|
|
41
|
+
key: null,
|
|
42
|
+
keyFile: null,
|
|
43
|
+
keyName: null,
|
|
44
|
+
keyStore: "",
|
|
45
|
+
plans: [],
|
|
46
|
+
primary: "",
|
|
47
|
+
scope: "user",
|
|
48
|
+
smokes: [],
|
|
49
|
+
store: null,
|
|
50
|
+
stores: [],
|
|
51
|
+
tests: [],
|
|
52
|
+
}).current;
|
|
53
|
+
const fail = (error, retry) => setScreen({ kind: "error", message: message(error), retry });
|
|
54
|
+
const chosen = () => HARNESSES.filter((harness) => wizard.harnesses.includes(harness.id));
|
|
55
|
+
const planInput = () => ({
|
|
56
|
+
background: wizard.background,
|
|
57
|
+
catalog: wizard.catalog,
|
|
58
|
+
env: wizard.env,
|
|
59
|
+
gatewayUrl: wizard.config.gatewayUrl,
|
|
60
|
+
key: wizard.key,
|
|
61
|
+
keyFile: wizard.keyFile,
|
|
62
|
+
primary: wizard.primary,
|
|
63
|
+
scope: wizard.scope,
|
|
64
|
+
store: wizard.store,
|
|
65
|
+
});
|
|
66
|
+
const summary = () => ({
|
|
67
|
+
background: wizard.background,
|
|
68
|
+
backups: wizard.backups,
|
|
69
|
+
changed: wizard.changed,
|
|
70
|
+
harnesses: chosen().map(({ id, name }) => ({ id, name })),
|
|
71
|
+
keyName: wizard.keyName,
|
|
72
|
+
keyStore: wizard.keyStore,
|
|
73
|
+
manual: wizard.plans.filter(({ plan }) => plan.manual.length > 0).map(({ harness, plan }) => ({ name: harness.name, steps: plan.manual })),
|
|
74
|
+
primary: wizard.primary,
|
|
75
|
+
scope: wizard.scope,
|
|
76
|
+
smokes: wizard.smokes,
|
|
77
|
+
tests: wizard.tests,
|
|
78
|
+
});
|
|
79
|
+
const hasBackgroundSlot = () => chosen().some((harness) => harness.backgroundDefault !== null);
|
|
80
|
+
const chooseModel = (role, id) => {
|
|
81
|
+
if (role === "primary") {
|
|
82
|
+
wizard.primary = id;
|
|
83
|
+
if (hasBackgroundSlot()) {
|
|
84
|
+
setScreen({ kind: "model", role: "background" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
wizard.background = id;
|
|
89
|
+
setScreen({ kind: "review" });
|
|
90
|
+
};
|
|
91
|
+
const finish = () => {
|
|
92
|
+
onFinish(summary());
|
|
93
|
+
exit();
|
|
94
|
+
};
|
|
95
|
+
switch (screen.kind) {
|
|
96
|
+
case "boot":
|
|
97
|
+
return (_jsx(Boot, { onDone: () => setScreen({ kind: "welcome" }), onError: (error) => fail(error, null), wizard: wizard }));
|
|
98
|
+
case "welcome":
|
|
99
|
+
return (_jsx(Welcome, { onSelect: (choice) => {
|
|
100
|
+
if (choice === "continue") {
|
|
101
|
+
setScreen(wizard.existing?.valid ? { kind: "existing" } : { kind: "signin" });
|
|
102
|
+
}
|
|
103
|
+
else if (choice === "cancel") {
|
|
104
|
+
exit();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
setScreen({ kind: "info", topic: choice });
|
|
108
|
+
}
|
|
109
|
+
}, wizard: wizard }));
|
|
110
|
+
case "info":
|
|
111
|
+
return _jsx(Info, { onBack: () => setScreen({ kind: "welcome" }), topic: screen.topic });
|
|
112
|
+
case "existing":
|
|
113
|
+
return (_jsx(Existing, { onSelect: (choice) => {
|
|
114
|
+
if (choice === "reuse") {
|
|
115
|
+
wizard.key = wizard.existing?.key ?? null;
|
|
116
|
+
wizard.keyStore = wizard.existing?.source ?? "";
|
|
117
|
+
setScreen({ kind: "harnesses" });
|
|
118
|
+
}
|
|
119
|
+
else if (choice === "new") {
|
|
120
|
+
setScreen({ kind: "signin" });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
exit();
|
|
124
|
+
}
|
|
125
|
+
}, source: wizard.existing?.source ?? "" }));
|
|
126
|
+
case "signin":
|
|
127
|
+
return (_jsx(SignIn, { config: wizard.config, env: wizard.env, onCancel: () => setScreen({ kind: "welcome" }), onError: (error) => fail(error, { kind: "signin" }), onToken: (token) => setScreen({ kind: "exchange", token }) }));
|
|
128
|
+
case "exchange":
|
|
129
|
+
return (_jsx(Exchange, { onDone: (key, name) => {
|
|
130
|
+
wizard.key = key;
|
|
131
|
+
wizard.keyName = name;
|
|
132
|
+
setScreen({ kind: "harnesses" });
|
|
133
|
+
}, onError: (error) => fail(error, error instanceof SetupError && error.code === "setup_recovery_required" ? null : screen), token: screen.token }));
|
|
134
|
+
case "error":
|
|
135
|
+
return (_jsx(ErrorScreen, { message: screen.message, onSelect: (choice) => (choice === "retry" && screen.retry ? setScreen(screen.retry) : exit()), retry: screen.retry !== null }));
|
|
136
|
+
case "harnesses":
|
|
137
|
+
return (_jsx(Harnesses, { initial: wizard.harnesses.length > 0 ? wizard.harnesses : [...wizard.installed], installed: wizard.installed, onSubmit: (ids) => {
|
|
138
|
+
wizard.harnesses = ids;
|
|
139
|
+
setScreen({ kind: "scope" });
|
|
140
|
+
} }));
|
|
141
|
+
case "scope":
|
|
142
|
+
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Where should this apply?" }), _jsx(Select, { onSelect: (scope) => {
|
|
143
|
+
wizard.scope = scope;
|
|
144
|
+
if (scope === "project") {
|
|
145
|
+
wizard.store = null;
|
|
146
|
+
wizard.keyFile = projectKeyFile(wizard.env.cwd);
|
|
147
|
+
wizard.keyStore = tilde(wizard.keyFile);
|
|
148
|
+
setScreen({ kind: "catalog" });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
setScreen({ kind: "store" });
|
|
152
|
+
}
|
|
153
|
+
}, options: [
|
|
154
|
+
{ hint: "recommended", label: "My user account", value: "user" },
|
|
155
|
+
{ hint: tilde(wizard.env.cwd), label: "Only this project", value: "project" },
|
|
156
|
+
] })] }));
|
|
157
|
+
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) => {
|
|
159
|
+
const store = wizard.stores.find((candidate) => candidate.kind === kind) ?? null;
|
|
160
|
+
wizard.store = store;
|
|
161
|
+
wizard.keyFile = store?.path ?? null;
|
|
162
|
+
wizard.keyStore = store?.label ?? "";
|
|
163
|
+
setScreen({ kind: "catalog" });
|
|
164
|
+
}, options: wizard.stores.map((store, index) => ({
|
|
165
|
+
hint: index === 0 ? "recommended" : undefined,
|
|
166
|
+
label: store.label,
|
|
167
|
+
value: store.kind,
|
|
168
|
+
})) })] }));
|
|
169
|
+
case "catalog":
|
|
170
|
+
return (_jsx(Catalog, { onDone: (catalog) => {
|
|
171
|
+
wizard.catalog = catalog;
|
|
172
|
+
setScreen({ kind: "model", role: "primary" });
|
|
173
|
+
}, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
|
|
174
|
+
case "model":
|
|
175
|
+
return (_jsx(ModelPicker, { catalog: wizard.catalog, harnesses: chosen(), onOther: () => setScreen({ kind: "type-model", role: screen.role }), onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
|
|
176
|
+
case "type-model":
|
|
177
|
+
return (_jsx(TypeModel, { catalog: wizard.catalog, onCancel: () => setScreen({ kind: "model", role: screen.role }), onSubmit: (id) => chooseModel(screen.role, id) }));
|
|
178
|
+
case "review":
|
|
179
|
+
return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
|
|
180
|
+
wizard.plans = plans;
|
|
181
|
+
}, onProceed: () => setScreen({ kind: "apply" }), wizard: wizard }));
|
|
182
|
+
case "apply":
|
|
183
|
+
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 }));
|
|
184
|
+
case "manual":
|
|
185
|
+
return _jsx(Manual, { onContinue: () => setScreen({ kind: "test" }), plans: wizard.plans });
|
|
186
|
+
case "test":
|
|
187
|
+
return (_jsx(Test, { harnesses: chosen(), onDone: (tests) => {
|
|
188
|
+
wizard.tests = tests;
|
|
189
|
+
setScreen({ kind: "tested" });
|
|
190
|
+
}, wizard: wizard }));
|
|
191
|
+
case "tested":
|
|
192
|
+
return (_jsx(Tested, { harnesses: chosen(), onSelect: (choice) => setScreen(choice === "smoke" ? { kind: "smoke" } : { kind: "done" }), tests: wizard.tests }));
|
|
193
|
+
case "smoke":
|
|
194
|
+
return (_jsx(Smoke, { harnesses: chosen(), input: planInput(), onDone: (smokes) => {
|
|
195
|
+
wizard.smokes = smokes;
|
|
196
|
+
setScreen({ kind: "done" });
|
|
197
|
+
} }));
|
|
198
|
+
case "done":
|
|
199
|
+
return _jsx(Done, { onExit: finish, summary: summary() });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function Boot({ onDone, onError, wizard }) {
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
(async () => {
|
|
205
|
+
const [env, config] = await Promise.all([inspectEnvironment(), discoverSetup()]);
|
|
206
|
+
wizard.env = env;
|
|
207
|
+
wizard.config = config;
|
|
208
|
+
const detections = await Promise.all(HARNESSES.map(async (harness) => [harness.id, await harness.detect(env)]));
|
|
209
|
+
wizard.installed = new Set(detections.filter(([, found]) => found).map(([id]) => id));
|
|
210
|
+
wizard.stores = await availableStores(env);
|
|
211
|
+
const existing = await findExistingKey(env);
|
|
212
|
+
wizard.existing = existing ? { ...existing, valid: await keyWorks(config.gatewayUrl, existing.key) } : null;
|
|
213
|
+
onDone();
|
|
214
|
+
})().catch(onError);
|
|
215
|
+
}, []);
|
|
216
|
+
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Inspecting this machine" }) }));
|
|
217
|
+
}
|
|
218
|
+
function Welcome({ onSelect, wizard }) {
|
|
219
|
+
const env = wizard.env;
|
|
220
|
+
const installed = HARNESSES.filter((harness) => wizard.installed.has(harness.id)).map((harness) => harness.name);
|
|
221
|
+
const facts = [
|
|
222
|
+
{ label: "Directory", value: tilde(env.cwd) },
|
|
223
|
+
{ label: "System", value: `${env.osLabel} (${env.arch})` },
|
|
224
|
+
{ label: "Shell", state: env.shell === "other" ? "warn" : "ok", value: env.shellLabel },
|
|
225
|
+
{ label: "Harnesses", state: installed.length > 0 ? "ok" : "warn", value: installed.length > 0 ? installed.join(", ") : "none detected" },
|
|
226
|
+
{
|
|
227
|
+
label: "Key",
|
|
228
|
+
state: wizard.existing ? (wizard.existing.valid ? "ok" : "warn") : "none",
|
|
229
|
+
value: wizard.existing ? `${wizard.existing.valid ? "found" : "found but rejected"} (${wizard.existing.source})` : "none configured yet",
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
if (env.remote) {
|
|
233
|
+
facts.splice(2, 0, { label: "Remote", state: "warn", value: env.remote });
|
|
234
|
+
}
|
|
235
|
+
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Boundless Setup" }), _jsxs(Muted, { center: true, children: ["Signs you in through your browser, creates an inference key on your account,", "\n", "configures your coding harnesses, and sends one small test request.", "\n", "The key is never printed. Nothing is purchased."] }), _jsx(Facts, { facts: facts }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: [
|
|
236
|
+
{ label: "Continue", value: "continue" },
|
|
237
|
+
{ label: "What this does", value: "what" },
|
|
238
|
+
{ label: "Privacy & data", value: "privacy" },
|
|
239
|
+
{ label: "Cancel", value: "cancel" },
|
|
240
|
+
] }) })] }));
|
|
241
|
+
}
|
|
242
|
+
function Info({ onBack, topic }) {
|
|
243
|
+
useInput((_, key) => {
|
|
244
|
+
if (key.return || key.escape) {
|
|
245
|
+
onBack();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
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
|
+
}
|
|
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: [
|
|
252
|
+
{ hint: "recommended", label: "Reuse it", value: "reuse" },
|
|
253
|
+
{ label: "Sign in and create a separate key", value: "new" },
|
|
254
|
+
{ label: "Cancel", value: "cancel" },
|
|
255
|
+
] }) })] }));
|
|
256
|
+
}
|
|
257
|
+
function SignIn({ config, env, onCancel, onError, onToken, }) {
|
|
258
|
+
const [grant, setGrant] = useState(null);
|
|
259
|
+
const [opened, setOpened] = useState(null);
|
|
260
|
+
const controller = useRef(new AbortController()).current;
|
|
261
|
+
useInput((_, key) => {
|
|
262
|
+
if (key.escape) {
|
|
263
|
+
controller.abort();
|
|
264
|
+
onCancel();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
(async () => {
|
|
269
|
+
const started = await startDeviceGrant(config);
|
|
270
|
+
setGrant(started);
|
|
271
|
+
const shown = env.remote ? false : await openBrowser(approvalUrl(started));
|
|
272
|
+
setOpened(shown);
|
|
273
|
+
const token = await waitForAccessToken(config, started, controller.signal);
|
|
274
|
+
onToken(token);
|
|
275
|
+
})().catch((error) => {
|
|
276
|
+
if (!(error instanceof SetupError && error.code === "cancelled")) {
|
|
277
|
+
onError(error);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}, []);
|
|
281
|
+
const fallback = grant && opened === false;
|
|
282
|
+
return (_jsxs(Frame, { hints: "esc cancel", children: [_jsx(Title, { children: "Sign in through your browser" }), grant === null ? (_jsx(Spinner, { label: "Starting browser sign-in" })) : (_jsxs(_Fragment, { children: [fallback ? (_jsxs(_Fragment, { children: [_jsx(Para, { children: env.remote ? `Remote environment (${env.remote}). Open this on any device:` : "Your browser could not be opened. Open this on any device:" }), _jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { color: BRAND, children: grant.verificationUri }) }), _jsxs(Box, { justifyContent: "center", marginBottom: 1, children: [_jsx(Text, { children: "and enter code " }), _jsx(Text, { bold: true, color: BRAND, children: grant.userCode })] })] })) : (_jsx(Para, { children: "Sign in (signup works too) and approve \"Boundless local setup\". The browser tab has the code already applied." })), _jsx(Spinner, { label: "Waiting for approval" }), _jsxs(Muted, { center: true, children: ["Expires ", new Date(grant.expiresAt).toLocaleTimeString()] })] }))] }));
|
|
283
|
+
}
|
|
284
|
+
function Exchange({ onDone, onError, token }) {
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
exchangeForKey(token, controller.signal)
|
|
288
|
+
.then((issued) => onDone(issued.key, issued.name))
|
|
289
|
+
.catch(onError);
|
|
290
|
+
return () => controller.abort();
|
|
291
|
+
}, []);
|
|
292
|
+
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Creating your inference key" }) }));
|
|
293
|
+
}
|
|
294
|
+
function ErrorScreen({ message: text, onSelect, retry }) {
|
|
295
|
+
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
|
+
}
|
|
297
|
+
function Harnesses({ initial, installed, onSubmit }) {
|
|
298
|
+
const [empty, setEmpty] = useState(false);
|
|
299
|
+
const options = HARNESSES.map((harness) => ({
|
|
300
|
+
hint: installed.has(harness.id) ? "installed" : "not found",
|
|
301
|
+
label: harness.name,
|
|
302
|
+
value: harness.id,
|
|
303
|
+
}));
|
|
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] }));
|
|
305
|
+
}
|
|
306
|
+
function Catalog({ onDone, onError, wizard }) {
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
listModels(wizard.config.gatewayUrl, wizard.key).then(onDone).catch(onError);
|
|
309
|
+
}, []);
|
|
310
|
+
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
|
|
311
|
+
}
|
|
312
|
+
function ModelPicker({ catalog, harnesses, onOther, onSelect, role, }) {
|
|
313
|
+
const lead = harnesses.find((harness) => role === "primary" || harness.backgroundDefault !== null) ?? harnesses[0];
|
|
314
|
+
const recommended = role === "primary" ? lead?.defaultModel : lead?.backgroundDefault;
|
|
315
|
+
const ids = new Set(catalog.map((model) => model.id));
|
|
316
|
+
const cheapest = [...catalog].sort((a, b) => a.outputPerMillionTokens - b.outputPerMillionTokens).map((model) => model.id);
|
|
317
|
+
const picks = [recommended, ...cheapest].filter((id) => typeof id === "string" && ids.has(id));
|
|
318
|
+
const options = [...new Set(picks)].slice(0, 4).map((id, index) => ({
|
|
319
|
+
hint: index === 0 ? `recommended for ${lead?.name ?? "you"}` : undefined,
|
|
320
|
+
label: id,
|
|
321
|
+
value: id,
|
|
322
|
+
}));
|
|
323
|
+
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."] }), _jsx(ModelTable, { models: catalog }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: (id) => (id === "__other" ? onOther() : onSelect(id)), options: [...options, { label: "Another model from the table", value: "__other" }] }) })] }));
|
|
324
|
+
}
|
|
325
|
+
function TypeModel({ catalog, onCancel, onSubmit }) {
|
|
326
|
+
const [rejected, setRejected] = useState(null);
|
|
327
|
+
return (_jsxs(Frame, { hints: "enter confirm esc back", children: [_jsx(Title, { children: "Type a model ID" }), _jsx(ModelTable, { models: catalog }), _jsx(Box, { justifyContent: "center", children: _jsx(TextInput, { onCancel: onCancel, onSubmit: (id) => (catalog.some((model) => model.id === id) ? onSubmit(id) : setRejected(id)), placeholder: "model id" }) }), rejected ? (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { color: WARN, children: [rejected, " is not served right now."] }) })) : null] }));
|
|
328
|
+
}
|
|
329
|
+
function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
330
|
+
const [plans, setPlans] = useState(null);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
Promise.all(harnesses.map(async (harness) => ({ harness, plan: await harness.plan(input) }))).then((planned) => {
|
|
333
|
+
onPlans(planned);
|
|
334
|
+
setPlans(planned);
|
|
335
|
+
});
|
|
336
|
+
}, []);
|
|
337
|
+
if (plans === null) {
|
|
338
|
+
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Planning changes" }) }));
|
|
339
|
+
}
|
|
340
|
+
const env = wizard.env;
|
|
341
|
+
const exposure = wizard.scope === "project"
|
|
342
|
+
? "private project file, ignored by git"
|
|
343
|
+
: env.os === "windows"
|
|
344
|
+
? "user environment variable"
|
|
345
|
+
: `exported as ${KEY_ENV} by your shell profile`;
|
|
346
|
+
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: "Review" }), _jsx(Facts, { facts: [
|
|
347
|
+
{ label: "Key", value: `${wizard.keyStore} · ${exposure}` },
|
|
348
|
+
{ label: "Model", value: wizard.primary },
|
|
349
|
+
...(wizard.background !== wizard.primary ? [{ label: "Background", value: wizard.background }] : []),
|
|
350
|
+
] }), _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: [
|
|
351
|
+
{ label: "Apply", value: "apply" },
|
|
352
|
+
{ label: "Cancel", value: "cancel" },
|
|
353
|
+
] }) })] }));
|
|
354
|
+
}
|
|
355
|
+
function Apply({ onDone, onError, wizard }) {
|
|
356
|
+
const [step, setStep] = useState("Storing the key");
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
(async () => {
|
|
359
|
+
const env = wizard.env;
|
|
360
|
+
const key = wizard.key;
|
|
361
|
+
const changed = [];
|
|
362
|
+
const backups = [];
|
|
363
|
+
if (wizard.scope === "project") {
|
|
364
|
+
const { ignored, path } = await saveProjectKey(env.cwd, key);
|
|
365
|
+
changed.push(tilde(path));
|
|
366
|
+
if (ignored) {
|
|
367
|
+
changed.push(`${tilde(env.cwd)}/.gitignore (.boundless/ added)`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (wizard.store && wizard.existing?.key !== key) {
|
|
371
|
+
await saveKey(wizard.store, key);
|
|
372
|
+
changed.push(wizard.store.path ? tilde(wizard.store.path) : wizard.store.label);
|
|
373
|
+
}
|
|
374
|
+
if (wizard.scope === "user" && wizard.store) {
|
|
375
|
+
setStep("Updating your shell profile");
|
|
376
|
+
changed.push(...(await exposeInShell(env, wizard.store)));
|
|
377
|
+
}
|
|
378
|
+
for (const { harness, plan } of wizard.plans) {
|
|
379
|
+
setStep(`Configuring ${harness.name}`);
|
|
380
|
+
for (const change of plan.changes) {
|
|
381
|
+
const { backup } = await change.apply();
|
|
382
|
+
changed.push(tilde(change.path));
|
|
383
|
+
if (backup) {
|
|
384
|
+
backups.push(tilde(backup));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
wizard.changed = changed;
|
|
389
|
+
wizard.backups = backups;
|
|
390
|
+
onDone();
|
|
391
|
+
})().catch(onError);
|
|
392
|
+
}, []);
|
|
393
|
+
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: step }) }));
|
|
394
|
+
}
|
|
395
|
+
function Manual({ onContinue, plans }) {
|
|
396
|
+
useInput((_, key) => {
|
|
397
|
+
if (key.return) {
|
|
398
|
+
onContinue();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return (_jsxs(Frame, { hints: "enter continue", children: [_jsx(Title, { children: "Finish these by hand" }), plans
|
|
402
|
+
.filter(({ plan }) => plan.manual.length > 0)
|
|
403
|
+
.map(({ harness, plan }) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: harness.name }), plan.manual.map((line, index) => (_jsxs(Text, { children: [" ", index + 1, ". ", line] }, line)))] }, harness.id)))] }));
|
|
404
|
+
}
|
|
405
|
+
function Test({ harnesses, onDone, wizard }) {
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
(async () => {
|
|
408
|
+
const gateway = wizard.config.gatewayUrl;
|
|
409
|
+
const key = wizard.key;
|
|
410
|
+
const pairs = new Map();
|
|
411
|
+
for (const harness of harnesses) {
|
|
412
|
+
pairs.set(`${harness.wire}:${wizard.primary}`, { model: wizard.primary, wire: harness.wire });
|
|
413
|
+
if (harness.backgroundDefault !== null) {
|
|
414
|
+
pairs.set(`${harness.wire}:${wizard.background}`, { model: wizard.background, wire: harness.wire });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const results = [];
|
|
418
|
+
for (const { model, wire } of pairs.values()) {
|
|
419
|
+
results.push(await testRequest(gateway, key, wire, model, wizard.catalog));
|
|
420
|
+
}
|
|
421
|
+
onDone(results);
|
|
422
|
+
})();
|
|
423
|
+
}, []);
|
|
424
|
+
return (_jsxs(Frame, { hints: "", children: [_jsx(Spinner, { label: "Sending test requests" }), _jsx(Muted, { center: true, children: "One small request per model and wire format. This uses account credits." })] }));
|
|
425
|
+
}
|
|
426
|
+
function TestFacts({ tests }) {
|
|
427
|
+
return (_jsx(Facts, { facts: tests.map((test) => test.kind === "ok"
|
|
428
|
+
? {
|
|
429
|
+
label: `${test.model} (${test.wire})`,
|
|
430
|
+
value: `"${test.reply.slice(0, 24)}" · ${test.inputTokens} in / ${test.outputTokens} out${test.cost ? ` · ${test.cost}` : ""}`,
|
|
431
|
+
}
|
|
432
|
+
: { label: `${test.model} (${test.wire})`, state: "bad", value: `${test.reason}: ${test.message}` }) }));
|
|
433
|
+
}
|
|
434
|
+
function Tested({ harnesses, onSelect, tests }) {
|
|
435
|
+
const smokeable = harnesses.filter((harness) => harness.smoke);
|
|
436
|
+
const allOk = tests.every((test) => test.kind === "ok");
|
|
437
|
+
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: allOk ? "The API test passed" : "A test failed" }), _jsx(TestFacts, { tests: tests }), smokeable.length > 0 ? (_jsx(Muted, { center: true, children: "Optionally run each installed harness once from a scratch directory to prove the configuration end to end. This also uses credits." })) : null, _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onSelect, options: smokeable.length > 0 && allOk
|
|
438
|
+
? [
|
|
439
|
+
{ label: `Run ${smokeable.map((harness) => harness.name).join(", ")}`, value: "smoke" },
|
|
440
|
+
{ label: "Finish", value: "finish" },
|
|
441
|
+
]
|
|
442
|
+
: [{ label: "Finish", value: "finish" }] }) })] }));
|
|
443
|
+
}
|
|
444
|
+
function Smoke({ harnesses, input, onDone }) {
|
|
445
|
+
const [current, setCurrent] = useState("");
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
(async () => {
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const harness of harnesses) {
|
|
450
|
+
if (!harness.smoke) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
setCurrent(harness.name);
|
|
454
|
+
results.push({ name: harness.name, result: await runSmoke(harness.smoke(input), input.key) });
|
|
455
|
+
}
|
|
456
|
+
onDone(results);
|
|
457
|
+
})();
|
|
458
|
+
}, []);
|
|
459
|
+
return (_jsxs(Frame, { hints: "", children: [_jsx(Spinner, { label: `Running ${current}` }), _jsx(Muted, { center: true, children: "Up to three minutes per harness." })] }));
|
|
460
|
+
}
|
|
461
|
+
function Done({ onExit, summary }) {
|
|
462
|
+
const ok = headline(summary).startsWith("Success");
|
|
463
|
+
return (_jsxs(Frame, { hints: "enter exit", children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: ok ? OK : WARN, children: headline(summary) }) }), _jsx(Facts, { facts: [
|
|
464
|
+
{ label: "Harnesses", value: summary.harnesses.map((harness) => harness.name).join(", ") },
|
|
465
|
+
{ label: "Model", value: summary.primary },
|
|
466
|
+
...(summary.background !== summary.primary ? [{ label: "Background", value: summary.background }] : []),
|
|
467
|
+
{ label: "Scope", value: summary.scope === "user" ? "user account" : "this project" },
|
|
468
|
+
{ label: "Key", value: summary.keyName ? `${summary.keyName} in ${summary.keyStore}` : summary.keyStore },
|
|
469
|
+
...summary.smokes.map(({ name, result }) => ({ label: name, state: result.kind === "ok" ? "ok" : "bad", value: result.kind === "ok" ? "ran a real tool call" : result.output.split("\n").at(-1) ?? "failed" })),
|
|
470
|
+
] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: MUTED, children: "Changed" }), summary.changed.map((path) => (_jsxs(Text, { children: [" ", path] }, path)))] }), summary.backups.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: MUTED, children: "Backups (restore by moving one back over its original)" }), summary.backups.map((path) => (_jsxs(Text, { children: [" ", path] }, path)))] })) : null, _jsxs(Muted, { children: ["Open a new terminal so the shell picks up ", KEY_ENV, "; already-running apps need a restart. Manage or revoke the key at ", KEYS_URL, "."] }), _jsx(Box, { justifyContent: "center", children: _jsx(Select, { onSelect: onExit, options: [{ label: "Exit", value: "exit" }] }) })] }));
|
|
471
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { App } from "./app.js";
|
|
5
|
+
import { CONSOLE, KEYS_URL } from "./lib/config.js";
|
|
6
|
+
import { headline } from "./summary.js";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
9
|
+
process.stdout.write(`${VERSION}\n`);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
if (!(process.stdout.isTTY && process.stdin.isTTY)) {
|
|
13
|
+
process.stderr.write(`boundless-setup needs an interactive terminal. Run it directly, or paste "curl -fsSL https://boundless.md" into your agent.\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
let summary = null;
|
|
17
|
+
const app = render(_jsx(App, { onFinish: (finished) => (summary = finished) }), { alternateScreen: true, exitOnCtrlC: true });
|
|
18
|
+
await app.waitUntilExit();
|
|
19
|
+
if (summary !== null) {
|
|
20
|
+
const done = summary;
|
|
21
|
+
const lines = [
|
|
22
|
+
headline(done),
|
|
23
|
+
`Harnesses: ${done.harnesses.map((harness) => harness.name).join(", ")}`,
|
|
24
|
+
`Model: ${done.primary}${done.background !== done.primary ? ` (background ${done.background})` : ""}`,
|
|
25
|
+
`Key: ${done.keyName ? `${done.keyName} in ` : ""}${done.keyStore}`,
|
|
26
|
+
...done.changed.map((path) => `Changed: ${path}`),
|
|
27
|
+
...done.backups.map((path) => `Backup: ${path}`),
|
|
28
|
+
...done.tests.map((test) => test.kind === "ok"
|
|
29
|
+
? `Test ${test.model} (${test.wire}): ok, ${test.inputTokens} in / ${test.outputTokens} out${test.cost ? `, ${test.cost}` : ""}`
|
|
30
|
+
: `Test ${test.model} (${test.wire}): ${test.reason} — ${test.message}`),
|
|
31
|
+
...done.smokes.map(({ name, result }) => `${name}: ${result.kind === "ok" ? "ran a real tool call" : `failed — ${result.output.split("\n").at(-1)}`}`),
|
|
32
|
+
...done.manual.flatMap(({ name, steps }) => [`${name} (by hand):`, ...steps.map((step, index) => ` ${index + 1}. ${step}`)]),
|
|
33
|
+
"Open a new terminal so BOUNDLESS_API_KEY is available; restart running apps.",
|
|
34
|
+
`Console ${CONSOLE}/ · keys ${KEYS_URL}`,
|
|
35
|
+
];
|
|
36
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { claudeHome } from "../lib/env.js";
|
|
3
|
+
import { installed } from "../lib/files.js";
|
|
4
|
+
import { backup, merge, readJson, tilde, writePrivate } from "../lib/files.js";
|
|
5
|
+
function settingsPath(input) {
|
|
6
|
+
return input.scope === "user" ? join(claudeHome(), "settings.json") : join(input.env.cwd, ".claude", "settings.local.json");
|
|
7
|
+
}
|
|
8
|
+
export const claudeCode = {
|
|
9
|
+
backgroundDefault: "dsv4",
|
|
10
|
+
defaultModel: "glm-5.2",
|
|
11
|
+
detect: () => installed("claude"),
|
|
12
|
+
id: "claude-code",
|
|
13
|
+
name: "Claude Code",
|
|
14
|
+
plan: (input) => {
|
|
15
|
+
const path = settingsPath(input);
|
|
16
|
+
return Promise.resolve({
|
|
17
|
+
changes: [
|
|
18
|
+
{
|
|
19
|
+
apply: async () => {
|
|
20
|
+
const current = await readJson(path);
|
|
21
|
+
const next = merge(current, {
|
|
22
|
+
env: {
|
|
23
|
+
ANTHROPIC_AUTH_TOKEN: input.key,
|
|
24
|
+
ANTHROPIC_BASE_URL: input.gatewayUrl,
|
|
25
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: input.background,
|
|
26
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: input.primary,
|
|
27
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: input.primary,
|
|
28
|
+
ANTHROPIC_MODEL: input.primary,
|
|
29
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
30
|
+
CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT: "1",
|
|
31
|
+
},
|
|
32
|
+
model: input.primary,
|
|
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: `model ${input.primary}, background ${input.background}, ANTHROPIC_BASE_URL → ${input.gatewayUrl}, ANTHROPIC_AUTH_TOKEN (key kept in this 0600 file)`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
manual: [],
|
|
43
|
+
notes: [
|
|
44
|
+
"Exit any running Claude Code before this file is written; start a fresh session afterwards.",
|
|
45
|
+
`The credential in ${tilde(path)} replaces your claude.ai login while set. Remove ANTHROPIC_AUTH_TOKEN to go back.`,
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
smoke: (input) => ({
|
|
50
|
+
args: ["--model", input.primary, "-p", "Use the Bash tool to run printf boundless, then report the output."],
|
|
51
|
+
command: "claude",
|
|
52
|
+
expect: /boundless/i,
|
|
53
|
+
}),
|
|
54
|
+
wire: "anthropic",
|
|
55
|
+
};
|