@boundless.network/setup 0.1.1 → 0.1.3

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 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";
@@ -19,8 +19,7 @@ import { Frame, Muted, Para, Title } from "./ui/frame.js";
19
19
  import { MultiSelect, Select } from "./ui/menu.js";
20
20
  import { Spinner } from "./ui/spinner.js";
21
21
  import { LOGO, LOGO_COLUMNS, LOGO_ROWS } from "./ui/logo.js";
22
- import { ModelTable } from "./ui/table.js";
23
- import { TextInput } from "./ui/text-input.js";
22
+ import { ModelSelect } from "./ui/table.js";
24
23
  import { BAD, BRAND, MUTED, OK, WARN } from "./ui/theme.js";
25
24
  const NAV = "↑↓ navigate enter select";
26
25
  function message(error) {
@@ -77,7 +76,7 @@ export function App({ onFinish }) {
77
76
  smokes: wizard.smokes,
78
77
  tests: wizard.tests,
79
78
  });
80
- const hasBackgroundSlot = () => chosen().some((harness) => harness.backgroundDefault !== null);
79
+ const hasBackgroundSlot = () => chosen().some((harness) => harness.backgroundSlot);
81
80
  const chooseModel = (role, id) => {
82
81
  if (role === "primary") {
83
82
  wizard.primary = id;
@@ -173,9 +172,7 @@ export function App({ onFinish }) {
173
172
  setScreen({ kind: "model", role: "primary" });
174
173
  }, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
175
174
  case "model":
176
- 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 }));
177
- case "type-model":
178
- return (_jsx(TypeModel, { catalog: wizard.catalog, onCancel: () => setScreen({ kind: "model", role: screen.role }), onSubmit: (id) => chooseModel(screen.role, id) }));
175
+ return (_jsx(ModelPicker, { catalog: wizard.catalog, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
179
176
  case "review":
180
177
  return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
181
178
  wizard.plans = plans;
@@ -312,22 +309,8 @@ function Catalog({ onDone, onError, wizard }) {
312
309
  }, []);
313
310
  return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
314
311
  }
315
- function ModelPicker({ catalog, harnesses, onOther, onSelect, role, }) {
316
- const lead = harnesses.find((harness) => role === "primary" || harness.backgroundDefault !== null) ?? harnesses[0];
317
- const recommended = role === "primary" ? lead?.defaultModel : lead?.backgroundDefault;
318
- const ids = new Set(catalog.map((model) => model.id));
319
- const cheapest = [...catalog].sort((a, b) => a.outputPerMillionTokens - b.outputPerMillionTokens).map((model) => model.id);
320
- const picks = [recommended, ...cheapest].filter((id) => typeof id === "string" && ids.has(id));
321
- const options = [...new Set(picks)].slice(0, 4).map((id, index) => ({
322
- hint: index === 0 ? `recommended for ${lead?.name ?? "you"}` : undefined,
323
- label: id,
324
- value: id,
325
- }));
326
- 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" }] }) })] }));
327
- }
328
- function TypeModel({ catalog, onCancel, onSubmit }) {
329
- const [rejected, setRejected] = useState(null);
330
- 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] }));
312
+ function ModelPicker({ catalog, onSelect, role, }) {
313
+ 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(ModelSelect, { models: catalog, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
331
314
  }
332
315
  function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
333
316
  const [plans, setPlans] = useState(null);
@@ -413,7 +396,7 @@ function Test({ harnesses, onDone, wizard }) {
413
396
  const pairs = new Map();
414
397
  for (const harness of harnesses) {
415
398
  pairs.set(`${harness.wire}:${wizard.primary}`, { model: wizard.primary, wire: harness.wire });
416
- if (harness.backgroundDefault !== null) {
399
+ if (harness.backgroundSlot) {
417
400
  pairs.set(`${harness.wire}:${wizard.background}`, { model: wizard.background, wire: harness.wire });
418
401
  }
419
402
  }
@@ -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
- backgroundDefault: "dsv4",
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
+ };
@@ -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
- backgroundDefault: null,
9
- defaultModel: "qwen3.6",
8
+ backgroundSlot: false,
10
9
  detect: () => installed("codex"),
11
10
  id: "codex",
12
11
  name: "Codex CLI",
@@ -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
- backgroundDefault: null,
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
- backgroundDefault: null,
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
+ };
@@ -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
- backgroundDefault: null,
7
- defaultModel: "qwen3.6",
6
+ backgroundSlot: false,
8
7
  detect: () => installed("hermes"),
9
8
  id: "hermes",
10
9
  name: "Hermes",
@@ -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 { opencode } from "./opencode.js";
7
- export const HARNESSES = [claudeCode, codex, opencode, hermes, omp, cursor, copilot];
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
+ ];
@@ -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
- backgroundDefault: "dsv4",
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
- function configPath(input) {
16
- if (input.scope === "project") {
17
- return join(input.env.cwd, "opencode.json");
18
- }
19
- return join(process.env.XDG_CONFIG_HOME ?? home(".config"), "opencode", "opencode.json");
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
- backgroundDefault: "dsv4",
23
- defaultModel: "qwen3.6",
24
- detect: () => installed("opencode"),
61
+ export const opencode = opencodeFamily({
62
+ binary: "opencode",
63
+ dir: "opencode",
64
+ file: "opencode.json",
25
65
  id: "opencode",
26
66
  name: "OpenCode",
27
- plan: (input) => {
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
- wire: "openai",
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
+ };
@@ -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`),
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/table.js CHANGED
@@ -1,17 +1,40 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { useState } from "react";
3
4
  import { usd } from "../lib/catalog.js";
4
- import { MUTED } from "./theme.js";
5
- const columns = (model) => [
5
+ import { BRAND, MUTED } from "./theme.js";
6
+ const HEADER = ["ID", "Input /1M", "Output /1M", "Context", ""];
7
+ const cells = (model, recommended) => [
6
8
  model.id,
7
9
  usd(model.inputPerMillionTokens),
8
10
  usd(model.outputPerMillionTokens),
9
11
  model.contextTokens.toLocaleString("en-US"),
12
+ model.id === recommended ? "recommended" : "",
10
13
  ];
11
- export function ModelTable({ models }) {
12
- const header = ["ID", "Input /1M", "Output /1M", "Context"];
13
- const rows = models.map(columns);
14
- const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column]?.length ?? 0)));
15
- const line = (cells) => cells.map((cell, column) => (column === 0 ? cell.padEnd(widths[column] ?? 0) : cell.padStart(widths[column] ?? 0))).join(" ");
16
- return (_jsxs(Box, { alignItems: "center", flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: MUTED, children: line(header) }), rows.map((row) => (_jsx(Text, { children: line(row) }, row[0])))] }));
14
+ export function ModelSelect({ models, onSelect, recommended, }) {
15
+ const start = Math.max(models.findIndex((model) => model.id === recommended), 0);
16
+ const [index, setIndex] = useState(start);
17
+ useInput((input, key) => {
18
+ if (key.upArrow || input === "k") {
19
+ setIndex((index + models.length - 1) % models.length);
20
+ }
21
+ else if (key.downArrow || input === "j") {
22
+ setIndex((index + 1) % models.length);
23
+ }
24
+ else if (key.return) {
25
+ const chosen = models[index];
26
+ if (chosen) {
27
+ onSelect(chosen.id);
28
+ }
29
+ }
30
+ });
31
+ const rows = models.map((model) => cells(model, recommended));
32
+ const widths = HEADER.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column]?.length ?? 0)));
33
+ const line = (row) => row
34
+ .map((cell, column) => (column === 0 || column === 4 ? cell.padEnd(widths[column] ?? 0) : cell.padStart(widths[column] ?? 0)))
35
+ .join(" ");
36
+ return (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: MUTED, children: ` ${line(HEADER)}` }), rows.map((row, position) => {
37
+ const active = position === index;
38
+ return (_jsxs(Text, { bold: active, color: active ? BRAND : undefined, children: [active ? "▸ " : " ", line(row)] }, row[0]));
39
+ })] }) }));
17
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boundless.network/setup",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Set up Boundless as the inference provider for your coding harness. Run: npx -y @boundless.network/setup@latest",
5
5
  "license": "MIT",
6
6
  "type": "module",