omnexx 0.6.0 → 0.7.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.
Files changed (44) hide show
  1. package/README.md +53 -4
  2. package/dist/{chat-server-NB3GPOMD.js → chat-server-X2QSACYD.js} +48 -8
  3. package/dist/chat-server-X2QSACYD.js.map +1 -0
  4. package/dist/{chat-settings-D3WKVSFU.js → chat-settings-DZCYNQF5.js} +39 -5
  5. package/dist/{chat-settings-D3WKVSFU.js.map → chat-settings-DZCYNQF5.js.map} +1 -1
  6. package/dist/{chunk-PY47EEHG.js → chunk-2TKRLH7N.js} +3 -3
  7. package/dist/{chunk-AOIUX7TJ.js → chunk-FC5LAF6U.js} +6 -4
  8. package/dist/{chunk-AOIUX7TJ.js.map → chunk-FC5LAF6U.js.map} +1 -1
  9. package/dist/{chunk-UZ6WXVKI.js → chunk-JHJW7UQV.js} +6 -4
  10. package/dist/{chunk-UZ6WXVKI.js.map → chunk-JHJW7UQV.js.map} +1 -1
  11. package/dist/{chunk-5T4EFIY2.js → chunk-M23TQTS6.js} +2 -2
  12. package/dist/{chunk-N2WJ2YTX.js → chunk-NPXKRK6O.js} +40 -4
  13. package/dist/chunk-NPXKRK6O.js.map +1 -0
  14. package/dist/chunk-UX4MF6V4.js +70 -0
  15. package/dist/chunk-UX4MF6V4.js.map +1 -0
  16. package/dist/{chunk-M64SDRL3.js → chunk-ZA7YDO2B.js} +51 -10
  17. package/dist/chunk-ZA7YDO2B.js.map +1 -0
  18. package/dist/{chunk-55VJCO7L.js → chunk-ZCBVW3DZ.js} +32 -5
  19. package/dist/chunk-ZCBVW3DZ.js.map +1 -0
  20. package/dist/{chunk-S3TUDM5B.js → chunk-ZGB5IZJR.js} +7 -66
  21. package/dist/chunk-ZGB5IZJR.js.map +1 -0
  22. package/dist/cli.js +13 -11
  23. package/dist/cli.js.map +1 -1
  24. package/dist/discovery-2LUVI2SX.js +11 -0
  25. package/dist/{model-picker-7NLP3QO6.js → model-picker-RVX6A2E3.js} +6 -5
  26. package/dist/{ollama-V6LN6HAR.js → ollama-ELPMF6WY.js} +5 -4
  27. package/dist/{ollama-V6LN6HAR.js.map → ollama-ELPMF6WY.js.map} +1 -1
  28. package/dist/{print-OUSVBDAA.js → print-KGSMCQ2Y.js} +5 -4
  29. package/dist/{print-OUSVBDAA.js.map → print-KGSMCQ2Y.js.map} +1 -1
  30. package/dist/session-OKHTHELI.js +27 -0
  31. package/dist/session-OKHTHELI.js.map +1 -0
  32. package/dist/{start-ZJQLZBS6.js → start-LAVPSRH2.js} +10 -9
  33. package/dist/{start-ZJQLZBS6.js.map → start-LAVPSRH2.js.map} +1 -1
  34. package/package.json +1 -1
  35. package/dist/chat-server-NB3GPOMD.js.map +0 -1
  36. package/dist/chunk-55VJCO7L.js.map +0 -1
  37. package/dist/chunk-M64SDRL3.js.map +0 -1
  38. package/dist/chunk-N2WJ2YTX.js.map +0 -1
  39. package/dist/chunk-S3TUDM5B.js.map +0 -1
  40. package/dist/session-YXEM22IA.js +0 -26
  41. /package/dist/{chunk-PY47EEHG.js.map → chunk-2TKRLH7N.js.map} +0 -0
  42. /package/dist/{chunk-5T4EFIY2.js.map → chunk-M23TQTS6.js.map} +0 -0
  43. /package/dist/{model-picker-7NLP3QO6.js.map → discovery-2LUVI2SX.js.map} +0 -0
  44. /package/dist/{session-YXEM22IA.js.map → model-picker-RVX6A2E3.js.map} +0 -0
package/README.md CHANGED
@@ -1,10 +1,59 @@
1
- # omnexx
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/yaegerbomb42/omnexx/main/docs/assets/logo.png" alt="" width="120">
3
+ </p>
2
4
 
3
- **Set a goal, walk away, come back to commits.**
5
+ <h1 align="center">omnexx</h1>
4
6
 
5
- Omnexx is a personal coding agent harness: one agent works on one huge task in one repo for about 24 hours with no one steering it. Your tests, typecheck and lint decide what's kept; every kept step is a small git commit on its own branch, and you get a report in the morning.
7
+ <p align="center"><b>Set a goal, walk away, come back to commits.</b><br>
8
+ A coding agent that works on your repo for hours on its own, keeps only what passes your tests,
9
+ and runs on any model, including free local ones.</p>
6
10
 
7
- > **Alpha.** A personal tool, not a product yet. It spends your own API credits.
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/omnexx"><img src="https://img.shields.io/npm/v/omnexx?color=36c5f0&label=npm" alt="npm"></a>
13
+ <a href="https://marketplace.visualstudio.com/items?itemName=yaegerbomb42.omnexx"><img src="https://img.shields.io/visual-studio-marketplace/v/yaegerbomb42.omnexx?color=a78bfa&label=VS%20Code" alt="VS Code"></a>
14
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3fb950" alt="MIT"></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <img src="https://raw.githubusercontent.com/yaegerbomb42/omnexx/main/docs/assets/demo.gif" alt="omnexx demo: install, the VS Code panel, and a real unattended run" width="820">
19
+ </p>
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ brew install yaegerbomb42/omnexx/omnexx # macOS / Linux
25
+ npm i -g omnexx # anywhere with Node 22+
26
+ curl -fsSL https://omnexx.org/install.sh | sh # one-liner
27
+ ```
28
+
29
+ Then `omnexx --ollama` to start free on a local model, or just `omnexx` and paste an API key.
30
+ In VS Code or Cursor, install the [omnexx extension](https://marketplace.visualstudio.com/items?itemName=yaegerbomb42.omnexx)
31
+ for the chat panel.
32
+
33
+ ## Real numbers
34
+
35
+ From an unattended run that built a Markdown notes app from a single goal, on free models:
36
+
37
+ | Tasks done without help | Spent | Prompt-cache hits | Autonomous work |
38
+ | :---------------------: | :-------: | :---------------: | :-------------: |
39
+ | **12 / 14** | **$0.00** | **86%** | **2.6 h** |
40
+
41
+ Every kept step is a small commit that passed your tests, typecheck and lint; everything else is
42
+ rolled back. A public benchmark against other harnesses is in progress.
43
+
44
+ ## The VS Code panel
45
+
46
+ <p>
47
+ <img src="https://raw.githubusercontent.com/yaegerbomb42/omnexx/main/docs/assets/panel.png" alt="omnexx chat panel mid-task: stats, step timeline, conversation and live log" width="380">
48
+ <img src="https://raw.githubusercontent.com/yaegerbomb42/omnexx/main/docs/assets/model-picker.png" alt="omnexx model picker" width="380">
49
+ </p>
50
+
51
+ Chat, plan, or run for hours from the sidebar. Watch the step timeline, tokens, cost and a live
52
+ log; approve or auto-approve guarded actions; switch models or add any of 30+ providers in one
53
+ click.
54
+
55
+ > **Alpha.** Built in the open; expect rough edges. It spends your own API credits unless you
56
+ > use a free or local model.
8
57
 
9
58
  ## Quickstart
10
59
 
@@ -8,7 +8,9 @@ import {
8
8
  import "./chunk-2H7UOFLK.js";
9
9
 
10
10
  // src/cli/chat-server.ts
11
+ import { readFile } from "fs/promises";
11
12
  import { createInterface } from "readline";
13
+ var MAX_FRAME_BYTES = 4 * 1024 * 1024;
12
14
  var MODES = /* @__PURE__ */ new Set(["chat", "plan", "run"]);
13
15
  var EMIT_EVERY_MS = 40;
14
16
  function parseCommand(line) {
@@ -28,7 +30,7 @@ function text(c, key) {
28
30
  return v;
29
31
  }
30
32
  async function serveChat(io, runCli, opts) {
31
- const { Session } = await import("./session-YXEM22IA.js");
33
+ const { Session } = await import("./session-OKHTHELI.js");
32
34
  const session = new Session(io, runCli);
33
35
  const write = (msg) => {
34
36
  println(io.stdout, JSON.stringify(msg));
@@ -37,6 +39,22 @@ async function serveChat(io, runCli, opts) {
37
39
  write({ type: "error", message: describeError(err) });
38
40
  };
39
41
  let sentUpTo = -1;
42
+ let sentFrame = 0;
43
+ const sendFrame = async () => {
44
+ const f = session.browserFrame;
45
+ if (!f || f.seq === sentFrame) return;
46
+ sentFrame = f.seq;
47
+ const data = await readFile(f.path).catch(() => void 0);
48
+ if (!data || data.length > MAX_FRAME_BYTES) return;
49
+ write({
50
+ type: "browserFrame",
51
+ seq: f.seq,
52
+ mime: f.mime,
53
+ url: f.url,
54
+ action: f.action,
55
+ data: data.toString("base64")
56
+ });
57
+ };
40
58
  let clearedAt = -1;
41
59
  let timer;
42
60
  const emit = () => {
@@ -65,16 +83,24 @@ async function serveChat(io, runCli, opts) {
65
83
  toolErrors: t.toolErrors,
66
84
  compactions: t.compactions
67
85
  } : null,
68
- run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null
86
+ run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null,
87
+ helpers: session.helpers,
88
+ browser: session.browserFrame ? {
89
+ seq: session.browserFrame.seq,
90
+ url: session.browserFrame.url,
91
+ action: session.browserFrame.action,
92
+ at: session.browserFrame.at
93
+ } : null
69
94
  };
70
95
  write(state);
96
+ void sendFrame();
71
97
  };
72
98
  const schedule = () => {
73
99
  timer ??= setTimeout(emit, EMIT_EVERY_MS);
74
100
  };
75
101
  session.onChange(schedule);
76
102
  const sendModels = async () => {
77
- const { loadModelChoices } = await import("./model-picker-7NLP3QO6.js");
103
+ const { loadModelChoices } = await import("./model-picker-RVX6A2E3.js");
78
104
  const { items, unreachable } = await loadModelChoices(io);
79
105
  write({
80
106
  type: "models",
@@ -84,7 +110,7 @@ async function serveChat(io, runCli, opts) {
84
110
  });
85
111
  };
86
112
  const sendSettings = async () => {
87
- const { providerSettings } = await import("./chat-settings-D3WKVSFU.js");
113
+ const { providerSettings } = await import("./chat-settings-DZCYNQF5.js");
88
114
  write({ type: "settings", ...await providerSettings(io) });
89
115
  };
90
116
  const providersChanged = async (message) => {
@@ -92,8 +118,13 @@ async function serveChat(io, runCli, opts) {
92
118
  await sendSettings();
93
119
  await sendModels();
94
120
  };
121
+ const inflight = /* @__PURE__ */ new Set();
95
122
  const run = (p) => {
96
- p.catch(fail).finally(schedule);
123
+ const tracked = p.catch(fail).finally(() => {
124
+ inflight.delete(tracked);
125
+ schedule();
126
+ });
127
+ inflight.add(tracked);
97
128
  };
98
129
  const handle = (c) => {
99
130
  switch (c.type) {
@@ -145,7 +176,7 @@ async function serveChat(io, runCli, opts) {
145
176
  return typeof v === "string" ? v : void 0;
146
177
  };
147
178
  run(
148
- import("./chat-settings-D3WKVSFU.js").then(async ({ addProvider }) => {
179
+ import("./chat-settings-DZCYNQF5.js").then(async ({ addProvider }) => {
149
180
  await providersChanged(
150
181
  await addProvider(io, {
151
182
  provider: str("provider"),
@@ -157,9 +188,16 @@ async function serveChat(io, runCli, opts) {
157
188
  );
158
189
  return;
159
190
  }
191
+ case "testProvider":
192
+ run(
193
+ import("./chat-settings-DZCYNQF5.js").then(async ({ testProvider }) => {
194
+ write({ type: "providerTest", ...await testProvider(io, text(c, "name")) });
195
+ })
196
+ );
197
+ return;
160
198
  case "removeProvider":
161
199
  run(
162
- import("./chat-settings-D3WKVSFU.js").then(async ({ removeProvider }) => {
200
+ import("./chat-settings-DZCYNQF5.js").then(async ({ removeProvider }) => {
163
201
  await providersChanged(await removeProvider(io, text(c, "name")));
164
202
  })
165
203
  );
@@ -171,6 +209,7 @@ async function serveChat(io, runCli, opts) {
171
209
  }
172
210
  case "sync":
173
211
  sentUpTo = clearedAt;
212
+ sentFrame = 0;
174
213
  write({ type: "ready", version: opts.version });
175
214
  emit();
176
215
  return;
@@ -193,6 +232,7 @@ async function serveChat(io, runCli, opts) {
193
232
  }
194
233
  }
195
234
  if (session.chatBusy) await session.command("stop");
235
+ await Promise.allSettled([...inflight]);
196
236
  if (timer) {
197
237
  clearTimeout(timer);
198
238
  emit();
@@ -202,4 +242,4 @@ async function serveChat(io, runCli, opts) {
202
242
  export {
203
243
  serveChat
204
244
  };
205
- //# sourceMappingURL=chat-server-NB3GPOMD.js.map
245
+ //# sourceMappingURL=chat-server-X2QSACYD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/chat-server.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { createInterface } from 'node:readline';\nimport { describeError } from '../errors.js';\nimport type { Telemetry } from '../telemetry/aggregate.js';\nimport type { TodoItem } from '../tools/todo.js';\nimport type { Entry, Helper, Mode, RunCli } from '../tui/session.js';\nimport { println, type CliIO } from './io.js';\n\n/**\n * `omnexx chat-server`: the interactive session behind a JSON-lines protocol on stdin/stdout, for\n * editor panels (the VS Code chat). It drives the same Session as the terminal UI, so slash\n * commands, approvals, model switching and plan mode behave the same way.\n *\n * In (one JSON object per line): send {text} · answer {yes} · stop · pause · resume · mode {mode} ·\n * yolo {on} ·\n * model {ref} · models · connect {value, name?} · undo · clear · compact · sync (resend all) ·\n * settings · addProvider {provider?, key?, url?} · removeProvider {name} · testProvider {name}.\n * Out: ready {version} · state (below; `entries` holds only lines not sent before) · models\n * {items, unreachable, current} · settings {catalog, connected} · providerTest {name, ok, models,\n * detail} · error {message}.\n */\n\nexport interface ChatUsage {\n /** When the chat's first event happened (epoch ms), for an uptime clock. */\n startedAt: number | null;\n tokens: Telemetry['tokens'];\n usd: number;\n turns: number;\n toolCalls: number;\n toolErrors: number;\n compactions: number;\n}\n\nexport interface ChatState {\n type: 'state';\n entries: Entry[];\n /** The reply as it streams in; empty once it lands in `entries`. */\n live: string;\n todos: TodoItem[];\n /** A question waiting for allow / deny, or null. */\n pending: string | null;\n mode: Mode;\n busy: boolean;\n paused: boolean;\n autoApprove: boolean;\n model: string | null;\n context: { used: number; limit: number } | null;\n usage: ChatUsage | null;\n run: { id: string; alive: boolean; phase?: string; done?: number; tasks?: number } | null;\n /** Helpers (subagents) for the current message. */\n helpers: Helper[];\n /** The latest browser frame's number and page; the image comes as a `browserFrame` message. */\n browser: { seq: number; url: string; action: string; at: number } | null;\n}\n\n/** Frames larger than this are skipped rather than pushed through the pipe. */\nconst MAX_FRAME_BYTES = 4 * 1024 * 1024;\n\nconst MODES = new Set<Mode>(['chat', 'plan', 'run']);\n/** Coalesce bursts of changes (a streaming reply fires one per token) into one state line. */\nconst EMIT_EVERY_MS = 40;\n\ntype Command = { type: string } & Record<string, unknown>;\n\nfunction parseCommand(line: string): Command {\n let data: unknown;\n try {\n data = JSON.parse(line);\n } catch {\n throw new Error('expected one JSON object per line');\n }\n if (typeof data !== 'object' || data === null || typeof (data as Command).type !== 'string')\n throw new Error('expected an object with a \"type\"');\n return data as Command;\n}\n\nfunction text(c: Command, key: string): string {\n const v = c[key];\n if (typeof v !== 'string' || !v.trim()) throw new Error(`\"${c.type}\" needs a \"${key}\" string`);\n return v;\n}\n\nexport async function serveChat(\n io: CliIO,\n runCli: RunCli,\n opts: { version: string },\n): Promise<number> {\n const { Session } = await import('../tui/session.js');\n const session = new Session(io, runCli);\n const write = (msg: object): void => {\n println(io.stdout, JSON.stringify(msg));\n };\n const fail = (err: unknown): void => {\n write({ type: 'error', message: describeError(err) });\n };\n\n let sentUpTo = -1;\n let sentFrame = 0;\n /** Send the browser's latest screenshot once per new frame. */\n const sendFrame = async (): Promise<void> => {\n const f = session.browserFrame;\n if (!f || f.seq === sentFrame) return;\n sentFrame = f.seq;\n const data = await readFile(f.path).catch(() => undefined);\n if (!data || data.length > MAX_FRAME_BYTES) return;\n write({\n type: 'browserFrame',\n seq: f.seq,\n mime: f.mime,\n url: f.url,\n action: f.action,\n data: data.toString('base64'),\n });\n };\n /** The last entry before the latest new chat; a reloaded panel starts after it. */\n let clearedAt = -1;\n let timer: NodeJS.Timeout | undefined;\n const emit = (): void => {\n timer = undefined;\n const fresh = session.entries.filter((e) => e.id > sentUpTo);\n sentUpTo = session.entries.at(-1)?.id ?? sentUpTo;\n const t = session.chatTelemetry;\n const state: ChatState = {\n type: 'state',\n entries: fresh,\n live: session.live,\n todos: session.todos,\n pending: session.pending?.question ?? null,\n mode: session.mode,\n busy: session.busy !== undefined || session.chatBusy,\n paused: session.chatPaused,\n autoApprove: session.autoApprove,\n model: session.chatModelName ?? null,\n context: session.context ?? null,\n usage: t\n ? {\n startedAt: t.startedAt ?? null,\n tokens: t.tokens,\n usd: t.usd,\n turns: t.turns,\n toolCalls: t.toolCalls,\n toolErrors: t.toolErrors,\n compactions: t.compactions,\n }\n : null,\n run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null,\n helpers: session.helpers,\n browser: session.browserFrame\n ? {\n seq: session.browserFrame.seq,\n url: session.browserFrame.url,\n action: session.browserFrame.action,\n at: session.browserFrame.at,\n }\n : null,\n };\n write(state);\n void sendFrame();\n };\n const schedule = (): void => {\n timer ??= setTimeout(emit, EMIT_EVERY_MS);\n };\n session.onChange(schedule);\n\n const sendModels = async (): Promise<void> => {\n const { loadModelChoices } = await import('../tui/model-picker.js');\n const { items, unreachable } = await loadModelChoices(io);\n write({\n type: 'models',\n items: items.filter((i) => i.kind === 'model'),\n unreachable,\n current: session.chatModelName ?? null,\n });\n };\n const sendSettings = async (): Promise<void> => {\n const { providerSettings } = await import('./chat-settings.js');\n write({ type: 'settings', ...(await providerSettings(io)) });\n };\n /** After a provider change: say what happened, then refresh the settings page and models. */\n const providersChanged = async (message: string): Promise<void> => {\n session.push('system', message);\n await sendSettings();\n await sendModels();\n };\n\n /** Commands still running, so a closed pipe lets them finish before the process exits. */\n const inflight = new Set<Promise<unknown>>();\n const run = (p: Promise<unknown>): void => {\n const tracked = p.catch(fail).finally(() => {\n inflight.delete(tracked);\n schedule();\n });\n inflight.add(tracked);\n };\n\n const handle = (c: Command): void => {\n switch (c.type) {\n case 'send':\n run(session.submit(text(c, 'text')));\n return;\n case 'answer':\n if (!session.pending) throw new Error('nothing is waiting for an answer');\n run(session.submit(c.yes === true ? 'y' : 'n'));\n return;\n case 'stop':\n case 'pause':\n case 'resume':\n case 'undo':\n case 'compact':\n run(session.command(c.type));\n return;\n case 'clear':\n run(\n session.command('clear').then(() => {\n // A new chat starts the panel over: nothing from before the clear is sent again.\n clearedAt = session.entries.at(-1)?.id ?? clearedAt;\n sentUpTo = clearedAt;\n write({ type: 'ready', version: opts.version });\n emit();\n }),\n );\n return;\n case 'mode': {\n const mode = c.mode as Mode;\n if (!MODES.has(mode)) throw new Error('mode is one of chat, plan, run');\n if (session.mode !== mode) session.setMode(mode);\n return;\n }\n case 'yolo':\n // /yolo toggles; only send it when the state differs from what was asked.\n if ((c.on === true) !== session.autoApprove) run(session.submit('/yolo'));\n return;\n case 'model':\n run(session.switchModel(text(c, 'ref')));\n return;\n case 'models':\n run(sendModels());\n return;\n case 'settings':\n run(sendSettings());\n return;\n case 'addProvider': {\n const str = (k: string): string | undefined => {\n const v = c[k];\n return typeof v === 'string' ? v : undefined;\n };\n run(\n import('./chat-settings.js').then(async ({ addProvider }) => {\n await providersChanged(\n await addProvider(io, {\n provider: str('provider'),\n key: str('key'),\n url: str('url'),\n }),\n );\n }),\n );\n return;\n }\n case 'testProvider':\n run(\n import('./chat-settings.js').then(async ({ testProvider }) => {\n write({ type: 'providerTest', ...(await testProvider(io, text(c, 'name'))) });\n }),\n );\n return;\n case 'removeProvider':\n run(\n import('./chat-settings.js').then(async ({ removeProvider }) => {\n await providersChanged(await removeProvider(io, text(c, 'name')));\n }),\n );\n return;\n case 'connect': {\n const name = typeof c.name === 'string' && c.name.trim() ? ` --name ${c.name.trim()}` : '';\n run(session.submit(`/model ${text(c, 'value')}${name}`));\n return;\n }\n case 'sync':\n // A reloaded panel: start it over with the conversation since the last new chat.\n sentUpTo = clearedAt;\n sentFrame = 0;\n write({ type: 'ready', version: opts.version });\n emit();\n return;\n default:\n throw new Error(`unknown command \"${c.type}\"`);\n }\n };\n\n await session.loadCommands();\n await session.continueChat().catch(fail);\n await session.greet(false).catch(fail);\n write({ type: 'ready', version: opts.version });\n emit();\n\n const lines = createInterface({ input: io.stdin, crlfDelay: Infinity });\n for await (const line of lines) {\n if (!line.trim()) continue;\n try {\n handle(parseCommand(line));\n } catch (err) {\n fail(err);\n }\n }\n // The editor closed the pipe: end any turn in progress, let running commands finish, then\n // flush the last state.\n if (session.chatBusy) await session.command('stop');\n await Promise.allSettled([...inflight]);\n if (timer) {\n clearTimeout(timer);\n emit();\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  connect,
4
4
  describeConnect
5
- } from "./chunk-AOIUX7TJ.js";
5
+ } from "./chunk-FC5LAF6U.js";
6
6
  import {
7
7
  PROVIDER_TEMPLATES,
8
8
  removeEndpointBlock
9
- } from "./chunk-5T4EFIY2.js";
9
+ } from "./chunk-M23TQTS6.js";
10
10
  import "./chunk-7PC4SSYS.js";
11
11
  import {
12
12
  clearKey,
@@ -15,10 +15,11 @@ import {
15
15
  maskKey,
16
16
  resolvePaths,
17
17
  userConfigFile
18
- } from "./chunk-S3TUDM5B.js";
18
+ } from "./chunk-ZGB5IZJR.js";
19
19
  import {
20
20
  UsageError
21
21
  } from "./chunk-7PNGHZYH.js";
22
+ import "./chunk-UX4MF6V4.js";
22
23
  import "./chunk-2H7UOFLK.js";
23
24
 
24
25
  // src/cli/chat-settings.ts
@@ -101,10 +102,43 @@ async function removeProvider(io, name) {
101
102
  return `${lower} had nothing to remove (a key from an environment variable stays set)`;
102
103
  return `removed ${lower}${removedKey ? " and its stored key" : ""}`;
103
104
  }
105
+ var TEST_TIMEOUT_MS = 8e3;
106
+ async function testProvider(io, name) {
107
+ const lower = name.trim().toLowerCase();
108
+ if (!NAME.test(lower)) throw new UsageError(`"${name}" is not a provider name`);
109
+ const paths = resolvePaths(io.env);
110
+ if (lower === "anthropic") {
111
+ const found = await findProviderKey(paths, io.env, "anthropic");
112
+ return {
113
+ name: lower,
114
+ ok: found !== void 0,
115
+ models: found ? 3 : 0,
116
+ detail: found ? "key set (opus, sonnet, haiku)" : "no key set"
117
+ };
118
+ }
119
+ const { config } = await loadConfig({ cwd: io.cwd, env: io.env });
120
+ const ep = config.providers.endpoints[lower];
121
+ if (!ep) return { name: lower, ok: false, models: 0, detail: "not set up" };
122
+ const key = (await findProviderKey(paths, io.env, lower, ep.api_key_env))?.key;
123
+ const { discoverModels } = await import("./discovery-2LUVI2SX.js");
124
+ const models = await discoverModels(
125
+ { baseUrl: ep.base_url, ...key ? { apiKey: key } : {} },
126
+ { timeoutMs: TEST_TIMEOUT_MS, ...io.fetch ? { fetch: io.fetch } : {} }
127
+ );
128
+ if (models.length)
129
+ return { name: lower, ok: true, models: models.length, detail: `${models.length} models` };
130
+ return {
131
+ name: lower,
132
+ ok: false,
133
+ models: 0,
134
+ detail: key || ep.free ? "couldn't reach it or it listed no models" : "no key set"
135
+ };
136
+ }
104
137
  export {
105
138
  addProvider,
106
139
  providerCatalog,
107
140
  providerSettings,
108
- removeProvider
141
+ removeProvider,
142
+ testProvider
109
143
  };
110
- //# sourceMappingURL=chat-settings-D3WKVSFU.js.map
144
+ //# sourceMappingURL=chat-settings-DZCYNQF5.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/chat-settings.ts"],"sourcesContent":["import { clearKey, findProviderKey, maskKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { PROVIDER_TEMPLATES, removeEndpointBlock } from './commands/providers.js';\nimport { connect, describeConnect } from './connect.js';\nimport type { CliIO } from './io.js';\n\n/**\n * Provider settings for editor panels (`omnexx chat-server`): what can be added, what is set up,\n * and adding or removing one. Keys go to the 0600 credentials file and are only ever shown masked.\n */\n\nexport interface CatalogProvider {\n name: string;\n label: string;\n baseUrl: string;\n /** A local server the person runs themselves: no key. */\n local: boolean;\n /** Where to get a key. */\n signup?: string;\n /** How its keys start, for a placeholder. */\n keyPrefix?: string;\n}\n\nexport interface ConnectedProvider {\n name: string;\n label: string;\n baseUrl: string;\n local: boolean;\n free: boolean;\n /** The key, masked (`sk-…wxyz`), or null when none is set. */\n key: string | null;\n /** Where the key comes from: an environment variable or the credentials file. */\n keySource: string | null;\n}\n\nexport interface ProviderSettings {\n catalog: CatalogProvider[];\n connected: ConnectedProvider[];\n}\n\nconst ANTHROPIC: CatalogProvider = {\n name: 'anthropic',\n label: 'Anthropic (Claude)',\n baseUrl: 'https://api.anthropic.com',\n local: false,\n signup: 'https://console.anthropic.com/settings/keys',\n keyPrefix: 'sk-ant-',\n};\n\nconst NAME = /^[a-z][a-z0-9_-]{0,39}$/;\n\nexport function providerCatalog(): CatalogProvider[] {\n const listed = Object.entries(PROVIDER_TEMPLATES)\n .filter(([name]) => name !== 'custom')\n .map(([name, t]) => ({\n name,\n label: t.label ?? name,\n baseUrl: t.baseUrl,\n local: t.local === true,\n ...(t.signup ? { signup: t.signup } : {}),\n ...(t.keyPrefix ? { keyPrefix: t.keyPrefix } : {}),\n }));\n return [ANTHROPIC, ...listed];\n}\n\nexport async function providerSettings(io: CliIO): Promise<ProviderSettings> {\n const paths = resolvePaths(io.env);\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const catalog = providerCatalog();\n const labelOf = (name: string): string => catalog.find((c) => c.name === name)?.label ?? name;\n const connected: ConnectedProvider[] = [];\n const anthropic = await findProviderKey(paths, io.env, 'anthropic');\n if (anthropic)\n connected.push({\n name: 'anthropic',\n label: ANTHROPIC.label,\n baseUrl: ANTHROPIC.baseUrl,\n local: false,\n free: false,\n key: maskKey(anthropic.key),\n keySource: anthropic.source,\n });\n for (const [name, ep] of Object.entries(config.providers.endpoints)) {\n const found = await findProviderKey(paths, io.env, name, ep.api_key_env);\n connected.push({\n name,\n label: labelOf(name),\n baseUrl: ep.base_url,\n local:\n PROVIDER_TEMPLATES[name]?.local === true ||\n /\\/\\/(localhost|127\\.0\\.0\\.1)[:/]/.test(ep.base_url),\n free: ep.free,\n key: found ? maskKey(found.key) : null,\n keySource: found?.source ?? null,\n });\n }\n return { catalog, connected };\n}\n\nexport interface AddProvider {\n /** A catalog name, or a new name for a custom URL. */\n provider?: string | undefined;\n key?: string | undefined;\n /** For a custom or local OpenAI-compatible server. */\n url?: string | undefined;\n}\n\n/** Connect a provider from the settings form; returns what to tell the person. */\nexport async function addProvider(io: CliIO, a: AddProvider): Promise<string> {\n const url = a.url?.trim();\n const provider = a.provider?.trim().toLowerCase();\n const key = a.key?.trim() ? a.key.trim() : undefined;\n if (url && !/^https?:\\/\\/\\S+$/i.test(url))\n throw new UsageError('the URL must start with http:// or https://');\n if (provider && !NAME.test(provider))\n throw new UsageError(\n `\"${provider}\" is not a usable name`,\n 'lowercase letters, digits, _ and -',\n );\n if (!url && !provider && !key) throw new UsageError('pick a provider or enter a URL');\n // A catalog provider is connected by name; a URL gets the name it was given, if any.\n const target = url ?? provider ?? key ?? '';\n const r = await connect(io, target, url ? key : target === key ? undefined : key, {\n ...(url && provider ? { name: provider } : {}),\n });\n return describeConnect(r);\n}\n\n/** Remove a provider: its endpoint block and its stored key. Environment variables stay. */\nexport async function removeProvider(io: CliIO, name: string): Promise<string> {\n const lower = name.trim().toLowerCase();\n if (!NAME.test(lower)) throw new UsageError(`\"${name}\" is not a provider name`);\n const paths = resolvePaths(io.env);\n const removedBlock =\n lower === 'anthropic' ? false : await removeEndpointBlock(userConfigFile(paths), lower);\n const removedKey = await clearKey(paths, lower);\n if (!removedBlock && !removedKey)\n return `${lower} had nothing to remove (a key from an environment variable stays set)`;\n return `removed ${lower}${removedKey ? ' and its stored key' : ''}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAM,YAA6B;AAAA,EACjC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,WAAW;AACb;AAEA,IAAM,OAAO;AAEN,SAAS,kBAAqC;AACnD,QAAM,SAAS,OAAO,QAAQ,kBAAkB,EAC7C,OAAO,CAAC,CAAC,IAAI,MAAM,SAAS,QAAQ,EACpC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO;AAAA,IACnB;AAAA,IACA,OAAO,EAAE,SAAS;AAAA,IAClB,SAAS,EAAE;AAAA,IACX,OAAO,EAAE,UAAU;AAAA,IACnB,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IACvC,GAAI,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,IAAI,CAAC;AAAA,EAClD,EAAE;AACJ,SAAO,CAAC,WAAW,GAAG,MAAM;AAC9B;AAEA,eAAsB,iBAAiB,IAAsC;AAC3E,QAAM,QAAQ,aAAa,GAAG,GAAG;AACjC,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,EAAE,KAAK,GAAG,KAAK,KAAK,GAAG,IAAI,CAAC;AAChE,QAAM,UAAU,gBAAgB;AAChC,QAAM,UAAU,CAAC,SAAyB,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,SAAS;AACzF,QAAM,YAAiC,CAAC;AACxC,QAAM,YAAY,MAAM,gBAAgB,OAAO,GAAG,KAAK,WAAW;AAClE,MAAI;AACF,cAAU,KAAK;AAAA,MACb,MAAM;AAAA,MACN,OAAO,UAAU;AAAA,MACjB,SAAS,UAAU;AAAA,MACnB,OAAO;AAAA,MACP,MAAM;AAAA,MACN,KAAK,QAAQ,UAAU,GAAG;AAAA,MAC1B,WAAW,UAAU;AAAA,IACvB,CAAC;AACH,aAAW,CAAC,MAAM,EAAE,KAAK,OAAO,QAAQ,OAAO,UAAU,SAAS,GAAG;AACnE,UAAM,QAAQ,MAAM,gBAAgB,OAAO,GAAG,KAAK,MAAM,GAAG,WAAW;AACvE,cAAU,KAAK;AAAA,MACb;AAAA,MACA,OAAO,QAAQ,IAAI;AAAA,MACnB,SAAS,GAAG;AAAA,MACZ,OACE,mBAAmB,IAAI,GAAG,UAAU,QACpC,mCAAmC,KAAK,GAAG,QAAQ;AAAA,MACrD,MAAM,GAAG;AAAA,MACT,KAAK,QAAQ,QAAQ,MAAM,GAAG,IAAI;AAAA,MAClC,WAAW,OAAO,UAAU;AAAA,IAC9B,CAAC;AAAA,EACH;AACA,SAAO,EAAE,SAAS,UAAU;AAC9B;AAWA,eAAsB,YAAY,IAAW,GAAiC;AAC5E,QAAM,MAAM,EAAE,KAAK,KAAK;AACxB,QAAM,WAAW,EAAE,UAAU,KAAK,EAAE,YAAY;AAChD,QAAM,MAAM,EAAE,KAAK,KAAK,IAAI,EAAE,IAAI,KAAK,IAAI;AAC3C,MAAI,OAAO,CAAC,oBAAoB,KAAK,GAAG;AACtC,UAAM,IAAI,WAAW,6CAA6C;AACpE,MAAI,YAAY,CAAC,KAAK,KAAK,QAAQ;AACjC,UAAM,IAAI;AAAA,MACR,IAAI,QAAQ;AAAA,MACZ;AAAA,IACF;AACF,MAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAK,OAAM,IAAI,WAAW,gCAAgC;AAEpF,QAAM,SAAS,OAAO,YAAY,OAAO;AACzC,QAAM,IAAI,MAAM,QAAQ,IAAI,QAAQ,MAAM,MAAM,WAAW,MAAM,SAAY,KAAK;AAAA,IAChF,GAAI,OAAO,WAAW,EAAE,MAAM,SAAS,IAAI,CAAC;AAAA,EAC9C,CAAC;AACD,SAAO,gBAAgB,CAAC;AAC1B;AAGA,eAAsB,eAAe,IAAW,MAA+B;AAC7E,QAAM,QAAQ,KAAK,KAAK,EAAE,YAAY;AACtC,MAAI,CAAC,KAAK,KAAK,KAAK,EAAG,OAAM,IAAI,WAAW,IAAI,IAAI,0BAA0B;AAC9E,QAAM,QAAQ,aAAa,GAAG,GAAG;AACjC,QAAM,eACJ,UAAU,cAAc,QAAQ,MAAM,oBAAoB,eAAe,KAAK,GAAG,KAAK;AACxF,QAAM,aAAa,MAAM,SAAS,OAAO,KAAK;AAC9C,MAAI,CAAC,gBAAgB,CAAC;AACpB,WAAO,GAAG,KAAK;AACjB,SAAO,WAAW,KAAK,GAAG,aAAa,wBAAwB,EAAE;AACnE;","names":[]}
1
+ {"version":3,"sources":["../src/cli/chat-settings.ts"],"sourcesContent":["import { clearKey, findProviderKey, maskKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { PROVIDER_TEMPLATES, removeEndpointBlock } from './commands/providers.js';\nimport { connect, describeConnect } from './connect.js';\nimport type { CliIO } from './io.js';\n\n/**\n * Provider settings for editor panels (`omnexx chat-server`): what can be added, what is set up,\n * and adding or removing one. Keys go to the 0600 credentials file and are only ever shown masked.\n */\n\nexport interface CatalogProvider {\n name: string;\n label: string;\n baseUrl: string;\n /** A local server the person runs themselves: no key. */\n local: boolean;\n /** Where to get a key. */\n signup?: string;\n /** How its keys start, for a placeholder. */\n keyPrefix?: string;\n}\n\nexport interface ConnectedProvider {\n name: string;\n label: string;\n baseUrl: string;\n local: boolean;\n free: boolean;\n /** The key, masked (`sk-…wxyz`), or null when none is set. */\n key: string | null;\n /** Where the key comes from: an environment variable or the credentials file. */\n keySource: string | null;\n}\n\nexport interface ProviderSettings {\n catalog: CatalogProvider[];\n connected: ConnectedProvider[];\n}\n\nconst ANTHROPIC: CatalogProvider = {\n name: 'anthropic',\n label: 'Anthropic (Claude)',\n baseUrl: 'https://api.anthropic.com',\n local: false,\n signup: 'https://console.anthropic.com/settings/keys',\n keyPrefix: 'sk-ant-',\n};\n\nconst NAME = /^[a-z][a-z0-9_-]{0,39}$/;\n\nexport function providerCatalog(): CatalogProvider[] {\n const listed = Object.entries(PROVIDER_TEMPLATES)\n .filter(([name]) => name !== 'custom')\n .map(([name, t]) => ({\n name,\n label: t.label ?? name,\n baseUrl: t.baseUrl,\n local: t.local === true,\n ...(t.signup ? { signup: t.signup } : {}),\n ...(t.keyPrefix ? { keyPrefix: t.keyPrefix } : {}),\n }));\n return [ANTHROPIC, ...listed];\n}\n\nexport async function providerSettings(io: CliIO): Promise<ProviderSettings> {\n const paths = resolvePaths(io.env);\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const catalog = providerCatalog();\n const labelOf = (name: string): string => catalog.find((c) => c.name === name)?.label ?? name;\n const connected: ConnectedProvider[] = [];\n const anthropic = await findProviderKey(paths, io.env, 'anthropic');\n if (anthropic)\n connected.push({\n name: 'anthropic',\n label: ANTHROPIC.label,\n baseUrl: ANTHROPIC.baseUrl,\n local: false,\n free: false,\n key: maskKey(anthropic.key),\n keySource: anthropic.source,\n });\n for (const [name, ep] of Object.entries(config.providers.endpoints)) {\n const found = await findProviderKey(paths, io.env, name, ep.api_key_env);\n connected.push({\n name,\n label: labelOf(name),\n baseUrl: ep.base_url,\n local:\n PROVIDER_TEMPLATES[name]?.local === true ||\n /\\/\\/(localhost|127\\.0\\.0\\.1)[:/]/.test(ep.base_url),\n free: ep.free,\n key: found ? maskKey(found.key) : null,\n keySource: found?.source ?? null,\n });\n }\n return { catalog, connected };\n}\n\nexport interface AddProvider {\n /** A catalog name, or a new name for a custom URL. */\n provider?: string | undefined;\n key?: string | undefined;\n /** For a custom or local OpenAI-compatible server. */\n url?: string | undefined;\n}\n\n/** Connect a provider from the settings form; returns what to tell the person. */\nexport async function addProvider(io: CliIO, a: AddProvider): Promise<string> {\n const url = a.url?.trim();\n const provider = a.provider?.trim().toLowerCase();\n const key = a.key?.trim() ? a.key.trim() : undefined;\n if (url && !/^https?:\\/\\/\\S+$/i.test(url))\n throw new UsageError('the URL must start with http:// or https://');\n if (provider && !NAME.test(provider))\n throw new UsageError(\n `\"${provider}\" is not a usable name`,\n 'lowercase letters, digits, _ and -',\n );\n if (!url && !provider && !key) throw new UsageError('pick a provider or enter a URL');\n // A catalog provider is connected by name; a URL gets the name it was given, if any.\n const target = url ?? provider ?? key ?? '';\n const r = await connect(io, target, url ? key : target === key ? undefined : key, {\n ...(url && provider ? { name: provider } : {}),\n });\n return describeConnect(r);\n}\n\n/** Remove a provider: its endpoint block and its stored key. Environment variables stay. */\nexport async function removeProvider(io: CliIO, name: string): Promise<string> {\n const lower = name.trim().toLowerCase();\n if (!NAME.test(lower)) throw new UsageError(`\"${name}\" is not a provider name`);\n const paths = resolvePaths(io.env);\n const removedBlock =\n lower === 'anthropic' ? false : await removeEndpointBlock(userConfigFile(paths), lower);\n const removedKey = await clearKey(paths, lower);\n if (!removedBlock && !removedKey)\n return `${lower} had nothing to remove (a key from an environment variable stays set)`;\n return `removed ${lower}${removedKey ? ' and its stored key' : ''}`;\n}\n\nexport interface ProviderTest {\n name: string;\n ok: boolean;\n /** How many models the provider listed, when it answered. */\n models: number;\n detail: string;\n}\n\nconst TEST_TIMEOUT_MS = 8_000;\n\n/**\n * Check a provider from the settings page: list its models with the stored key. Anthropic is\n * built in and has no list endpoint here, so it only reports whether a key is set.\n */\nexport async function testProvider(io: CliIO, name: string): Promise<ProviderTest> {\n const lower = name.trim().toLowerCase();\n if (!NAME.test(lower)) throw new UsageError(`\"${name}\" is not a provider name`);\n const paths = resolvePaths(io.env);\n if (lower === 'anthropic') {\n const found = await findProviderKey(paths, io.env, 'anthropic');\n return {\n name: lower,\n ok: found !== undefined,\n models: found ? 3 : 0,\n detail: found ? 'key set (opus, sonnet, haiku)' : 'no key set',\n };\n }\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const ep = config.providers.endpoints[lower];\n if (!ep) return { name: lower, ok: false, models: 0, detail: 'not set up' };\n const key = (await findProviderKey(paths, io.env, lower, ep.api_key_env))?.key;\n const { discoverModels } = await import('../providers/discovery.js');\n const models = await discoverModels(\n { baseUrl: ep.base_url, ...(key ? { apiKey: key } : {}) },\n { timeoutMs: TEST_TIMEOUT_MS, ...(io.fetch ? { fetch: io.fetch } : {}) },\n );\n if (models.length)\n return { name: lower, ok: true, models: models.length, detail: `${models.length} models` };\n return {\n name: lower,\n ok: false,\n models: 0,\n detail: key || ep.free ? \"couldn't reach it or it listed no models\" : 'no key set',\n };\n}\n"],"mappings":"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","names":[]}
@@ -73,7 +73,7 @@ import {
73
73
  writeFileAtomic,
74
74
  writeIntentTool,
75
75
  writeJsonAtomic
76
- } from "./chunk-N2WJ2YTX.js";
76
+ } from "./chunk-NPXKRK6O.js";
77
77
  import {
78
78
  println
79
79
  } from "./chunk-7PC4SSYS.js";
@@ -84,7 +84,7 @@ import {
84
84
  formatDuration,
85
85
  parseDuration,
86
86
  resolvePaths
87
- } from "./chunk-S3TUDM5B.js";
87
+ } from "./chunk-ZGB5IZJR.js";
88
88
  import {
89
89
  LockError,
90
90
  OmnexxError,
@@ -5536,4 +5536,4 @@ export {
5536
5536
  EventTail,
5537
5537
  LiveFeed
5538
5538
  };
5539
- //# sourceMappingURL=chunk-PY47EEHG.js.map
5539
+ //# sourceMappingURL=chunk-2TKRLH7N.js.map
@@ -2,18 +2,20 @@
2
2
  import {
3
3
  PROVIDER_TEMPLATES,
4
4
  appendEndpointBlock
5
- } from "./chunk-5T4EFIY2.js";
5
+ } from "./chunk-M23TQTS6.js";
6
6
  import {
7
- discoverModels,
8
7
  findProviderKey,
9
8
  loadConfig,
10
9
  resolvePaths,
11
10
  storeKey,
12
11
  userConfigFile
13
- } from "./chunk-S3TUDM5B.js";
12
+ } from "./chunk-ZGB5IZJR.js";
14
13
  import {
15
14
  UsageError
16
15
  } from "./chunk-7PNGHZYH.js";
16
+ import {
17
+ discoverModels
18
+ } from "./chunk-UX4MF6V4.js";
17
19
 
18
20
  // src/cli/connect.ts
19
21
  var KEY_PREFIXES = [
@@ -179,4 +181,4 @@ export {
179
181
  envKeys,
180
182
  connectFromEnv
181
183
  };
182
- //# sourceMappingURL=chunk-AOIUX7TJ.js.map
184
+ //# sourceMappingURL=chunk-FC5LAF6U.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/connect.ts"],"sourcesContent":["import { storeKey, findProviderKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport {\n appendEndpointBlock,\n PROVIDER_TEMPLATES,\n type ProviderTemplate,\n} from './commands/providers.js';\nimport type { CliIO } from './io.js';\n\n/** Key prefixes that name their provider, from the catalog; longest first so sk-or- beats sk-. */\nconst KEY_PREFIXES: readonly (readonly [string, string])[] = (\n [\n ['sk-ant-', 'anthropic'],\n ...Object.entries(PROVIDER_TEMPLATES).flatMap(([name, t]): [string, string][] =>\n t.keyPrefix ? [[t.keyPrefix, name]] : [],\n ),\n ] as [string, string][]\n).sort((a, b) => b[0].length - a[0].length);\n\n/** The provider a pasted key belongs to, or undefined when it doesn't look like a key. */\nexport function providerForKey(text: string): string | undefined {\n const t = text.trim();\n if (/\\s/.test(t) || t.length < 20) return undefined;\n return KEY_PREFIXES.find(([p]) => t.startsWith(p))?.[1];\n}\n\n/** A token long and random enough to be a secret: 24+ chars mixing letters and digits. */\nconst SECRETISH = /^[A-Za-z0-9][A-Za-z0-9_.-]{23,}$/;\n\nexport interface FoundKey {\n key: string;\n /** Undefined when the key has no telltale prefix and the text names no provider. */\n provider: string | undefined;\n}\n\n/**\n * An API key inside a message (\"here's my mistral key abc123…\"): a token with a known prefix, or\n * a secret-looking token next to a provider name the catalog knows. Detection is local; the key is\n * never sent to a model.\n */\nexport function findKeyInText(text: string): FoundKey | undefined {\n const tokens = text.split(/[\\s\"'`,;()<>]+/).map((t) => t.replace(/[.:]+$/, ''));\n for (const t of tokens) {\n const provider = providerForKey(t);\n if (provider) return { key: t, provider };\n }\n const key = tokens.find(\n (t) => SECRETISH.test(t) && /\\d/.test(t) && /[A-Za-z]/.test(t) && !/^https?/i.test(t),\n );\n if (!key) return undefined;\n const words = text.toLowerCase();\n const named = Object.entries(PROVIDER_TEMPLATES).find(\n ([name, t]) =>\n name !== 'custom' &&\n !t.local &&\n (new RegExp(`\\\\b${name}\\\\b`).test(words) ||\n (t.label !== undefined && words.includes(t.label.toLowerCase()))),\n );\n if (named) return { key, provider: named[0] };\n return /\\b(api[ _-]?key|key|token|secret)\\b/.test(words)\n ? { key, provider: undefined }\n : undefined;\n}\n\nexport function looksLikeKey(text: string): boolean {\n return providerForKey(text) !== undefined;\n}\n\n/** A name for an endpoint URL: a matching template, else the host's distinctive label. */\nexport function nameForUrl(url: string): string {\n const u = new URL(url);\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (name === 'custom') continue;\n const tu = new URL(t.baseUrl);\n if (tu.host === u.host) return name;\n }\n if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return 'local';\n const labels = u.hostname.split('.').filter((l) => !['api', 'www', 'inference'].includes(l));\n const label = labels.length > 1 ? labels[labels.length - 2] : labels[0];\n return (label ?? 'custom').toLowerCase().replace(/[^a-z0-9_-]/g, '-');\n}\n\nexport interface ConnectResult {\n name: string;\n added: boolean;\n keyStored: boolean;\n models: string[];\n /** A ready-to-use model ref, e.g. \"openrouter:deepseek/deepseek-v4\". */\n suggested: string | undefined;\n notes: string[];\n}\n\n/**\n * One step from nothing to a usable provider. `target` is a provider name (\"openrouter\"), a base\n * URL (\"http://localhost:11434/v1\"), or a pasted API key. The key goes to the 0600 credentials\n * file, the endpoint to user config, and the endpoint's models are listed to prove it works.\n */\nexport async function connect(\n io: CliIO,\n target: string,\n key?: string,\n opts: { name?: string } = {},\n): Promise<ConnectResult> {\n let t = target.trim();\n let k = key?.trim() ? key.trim() : undefined;\n if (!t) throw new UsageError('nothing to connect', 'try `connect openrouter <key>` or a URL');\n const keyProvider = providerForKey(t);\n if (keyProvider && !k) {\n k = t;\n t = keyProvider;\n }\n let name: string;\n let template: ProviderTemplate | undefined;\n if (/^https?:\\/\\//i.test(t)) {\n name = opts.name ?? nameForUrl(t);\n const local = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)/i.test(t);\n template = { baseUrl: t, keyEnv: undefined, free: local && !k, kind: 'openai' };\n } else {\n name = (opts.name ?? t).toLowerCase();\n template = PROVIDER_TEMPLATES[name];\n }\n if (!/^[a-z][a-z0-9_-]*$/.test(name))\n throw new UsageError(`bad provider name \"${name}\"`, 'use lowercase letters, digits, _ or -');\n\n const paths = resolvePaths(io.env);\n const notes: string[] = [];\n let keyStored = false;\n if (k) {\n await storeKey(paths, name, k);\n keyStored = true;\n }\n if (name === 'anthropic') {\n const found = await findProviderKey(paths, io.env, 'anthropic');\n if (!found) notes.push('no Anthropic key yet: paste one (sk-ant-…)');\n return {\n name,\n added: false,\n keyStored,\n models: ['opus', 'sonnet', 'haiku'],\n suggested: found ? 'anthropic:sonnet' : undefined,\n notes,\n };\n }\n\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const existing = config.providers.endpoints[name];\n let added = false;\n let baseUrl = existing?.base_url;\n if (!existing) {\n if (!template)\n throw new UsageError(\n `unknown provider \"${name}\"`,\n `known: ${Object.keys(PROVIDER_TEMPLATES).join(', ')}; or connect a base URL`,\n );\n // Keys live in the credentials file, so the endpoint doesn't need an env var.\n await appendEndpointBlock(userConfigFile(paths), name, { ...template, keyEnv: undefined });\n baseUrl = template.baseUrl;\n added = true;\n }\n const apiKey = (await findProviderKey(paths, io.env, name, existing?.api_key_env))?.key;\n if (!apiKey && !(existing?.free ?? template?.free)) notes.push(`no key for ${name} yet`);\n const local = /localhost|127\\.0\\.0\\.1/.test(baseUrl ?? '');\n const models = baseUrl\n ? (\n await discoverModels(\n { baseUrl, ...(apiKey ? { apiKey } : {}) },\n io.fetch ? { fetch: io.fetch } : {},\n )\n ).map((m) => m.id)\n : [];\n if (!models.length)\n notes.push(\n local\n ? `nothing answered at ${baseUrl}; is the server running?`\n : `couldn't list models at ${baseUrl}; check the key`,\n );\n const first = models[0];\n return {\n name,\n added,\n keyStored,\n models,\n suggested: first ? `${name}:${first}` : undefined,\n notes,\n };\n}\n\nexport function describeConnect(r: ConnectResult): string {\n const lines = [\n `${r.added ? 'added' : 'updated'} ${r.name}${r.keyStored ? ' (key saved, 0600)' : ''}`,\n ];\n if (r.models.length) {\n const shown = r.models.slice(0, 8).join(', ');\n lines.push(\n `${r.models.length} model${r.models.length === 1 ? '' : 's'}: ${shown}${r.models.length > 8 ? ', …' : ''}`,\n );\n }\n for (const n of r.notes) lines.push(n);\n if (r.suggested) lines.push(`chat with it: /model ${r.suggested}`);\n return lines.join('\\n');\n}\n\nexport interface EnvKey {\n name: string;\n label: string;\n keyEnv: string;\n}\n\n/**\n * Providers whose standard key variable is already set in the environment but that aren't\n * configured yet: the fastest way to a working model on a machine that already has keys.\n */\nexport async function envKeys(io: CliIO): Promise<EnvKey[]> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const out: EnvKey[] = [];\n if (io.env.ANTHROPIC_API_KEY?.trim())\n out.push({ name: 'anthropic', label: 'Anthropic', keyEnv: 'ANTHROPIC_API_KEY' });\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (!t.keyEnv || config.providers.endpoints[name]) continue;\n if (io.env[t.keyEnv]?.trim()) out.push({ name, label: t.label ?? name, keyEnv: t.keyEnv });\n }\n return out;\n}\n\n/**\n * `/connect env`: add every provider found by envKeys(), pointing each endpoint at its env var\n * (the key is never copied into omnexx's files). Anthropic needs nothing: its key is read from\n * the environment already.\n */\nexport async function connectFromEnv(io: CliIO): Promise<EnvKey[]> {\n const found = await envKeys(io);\n const file = userConfigFile(resolvePaths(io.env));\n // One after another: every block is appended to the same file.\n await found.reduce(async (prev, k) => {\n await prev;\n const t = PROVIDER_TEMPLATES[k.name];\n if (t) await appendEndpointBlock(file, k.name, { ...t, keyEnv: k.keyEnv });\n }, Promise.resolve());\n return found;\n}\n"],"mappings":"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","names":["local"]}
1
+ {"version":3,"sources":["../src/cli/connect.ts"],"sourcesContent":["import { storeKey, findProviderKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport {\n appendEndpointBlock,\n PROVIDER_TEMPLATES,\n type ProviderTemplate,\n} from './commands/providers.js';\nimport type { CliIO } from './io.js';\n\n/** Key prefixes that name their provider, from the catalog; longest first so sk-or- beats sk-. */\nconst KEY_PREFIXES: readonly (readonly [string, string])[] = (\n [\n ['sk-ant-', 'anthropic'],\n ...Object.entries(PROVIDER_TEMPLATES).flatMap(([name, t]): [string, string][] =>\n t.keyPrefix ? [[t.keyPrefix, name]] : [],\n ),\n ] as [string, string][]\n).sort((a, b) => b[0].length - a[0].length);\n\n/** The provider a pasted key belongs to, or undefined when it doesn't look like a key. */\nexport function providerForKey(text: string): string | undefined {\n const t = text.trim();\n if (/\\s/.test(t) || t.length < 20) return undefined;\n return KEY_PREFIXES.find(([p]) => t.startsWith(p))?.[1];\n}\n\n/** A token long and random enough to be a secret: 24+ chars mixing letters and digits. */\nconst SECRETISH = /^[A-Za-z0-9][A-Za-z0-9_.-]{23,}$/;\n\nexport interface FoundKey {\n key: string;\n /** Undefined when the key has no telltale prefix and the text names no provider. */\n provider: string | undefined;\n}\n\n/**\n * An API key inside a message (\"here's my mistral key abc123…\"): a token with a known prefix, or\n * a secret-looking token next to a provider name the catalog knows. Detection is local; the key is\n * never sent to a model.\n */\nexport function findKeyInText(text: string): FoundKey | undefined {\n const tokens = text.split(/[\\s\"'`,;()<>]+/).map((t) => t.replace(/[.:]+$/, ''));\n for (const t of tokens) {\n const provider = providerForKey(t);\n if (provider) return { key: t, provider };\n }\n const key = tokens.find(\n (t) => SECRETISH.test(t) && /\\d/.test(t) && /[A-Za-z]/.test(t) && !/^https?/i.test(t),\n );\n if (!key) return undefined;\n const words = text.toLowerCase();\n const named = Object.entries(PROVIDER_TEMPLATES).find(\n ([name, t]) =>\n name !== 'custom' &&\n !t.local &&\n (new RegExp(`\\\\b${name}\\\\b`).test(words) ||\n (t.label !== undefined && words.includes(t.label.toLowerCase()))),\n );\n if (named) return { key, provider: named[0] };\n return /\\b(api[ _-]?key|key|token|secret)\\b/.test(words)\n ? { key, provider: undefined }\n : undefined;\n}\n\nexport function looksLikeKey(text: string): boolean {\n return providerForKey(text) !== undefined;\n}\n\n/** A name for an endpoint URL: a matching template, else the host's distinctive label. */\nexport function nameForUrl(url: string): string {\n const u = new URL(url);\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (name === 'custom') continue;\n const tu = new URL(t.baseUrl);\n if (tu.host === u.host) return name;\n }\n if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return 'local';\n const labels = u.hostname.split('.').filter((l) => !['api', 'www', 'inference'].includes(l));\n const label = labels.length > 1 ? labels[labels.length - 2] : labels[0];\n return (label ?? 'custom').toLowerCase().replace(/[^a-z0-9_-]/g, '-');\n}\n\nexport interface ConnectResult {\n name: string;\n added: boolean;\n keyStored: boolean;\n models: string[];\n /** A ready-to-use model ref, e.g. \"openrouter:deepseek/deepseek-v4\". */\n suggested: string | undefined;\n notes: string[];\n}\n\n/**\n * One step from nothing to a usable provider. `target` is a provider name (\"openrouter\"), a base\n * URL (\"http://localhost:11434/v1\"), or a pasted API key. The key goes to the 0600 credentials\n * file, the endpoint to user config, and the endpoint's models are listed to prove it works.\n */\nexport async function connect(\n io: CliIO,\n target: string,\n key?: string,\n opts: { name?: string } = {},\n): Promise<ConnectResult> {\n let t = target.trim();\n let k = key?.trim() ? key.trim() : undefined;\n if (!t) throw new UsageError('nothing to connect', 'try `connect openrouter <key>` or a URL');\n const keyProvider = providerForKey(t);\n if (keyProvider && !k) {\n k = t;\n t = keyProvider;\n }\n let name: string;\n let template: ProviderTemplate | undefined;\n if (/^https?:\\/\\//i.test(t)) {\n name = opts.name ?? nameForUrl(t);\n const local = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)/i.test(t);\n template = { baseUrl: t, keyEnv: undefined, free: local && !k, kind: 'openai' };\n } else {\n name = (opts.name ?? t).toLowerCase();\n template = PROVIDER_TEMPLATES[name];\n }\n if (!/^[a-z][a-z0-9_-]*$/.test(name))\n throw new UsageError(`bad provider name \"${name}\"`, 'use lowercase letters, digits, _ or -');\n\n const paths = resolvePaths(io.env);\n const notes: string[] = [];\n let keyStored = false;\n if (k) {\n await storeKey(paths, name, k);\n keyStored = true;\n }\n if (name === 'anthropic') {\n const found = await findProviderKey(paths, io.env, 'anthropic');\n if (!found) notes.push('no Anthropic key yet: paste one (sk-ant-…)');\n return {\n name,\n added: false,\n keyStored,\n models: ['opus', 'sonnet', 'haiku'],\n suggested: found ? 'anthropic:sonnet' : undefined,\n notes,\n };\n }\n\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const existing = config.providers.endpoints[name];\n let added = false;\n let baseUrl = existing?.base_url;\n if (!existing) {\n if (!template)\n throw new UsageError(\n `unknown provider \"${name}\"`,\n `known: ${Object.keys(PROVIDER_TEMPLATES).join(', ')}; or connect a base URL`,\n );\n // Keys live in the credentials file, so the endpoint doesn't need an env var.\n await appendEndpointBlock(userConfigFile(paths), name, { ...template, keyEnv: undefined });\n baseUrl = template.baseUrl;\n added = true;\n }\n const apiKey = (await findProviderKey(paths, io.env, name, existing?.api_key_env))?.key;\n if (!apiKey && !(existing?.free ?? template?.free)) notes.push(`no key for ${name} yet`);\n const local = /localhost|127\\.0\\.0\\.1/.test(baseUrl ?? '');\n const models = baseUrl\n ? (\n await discoverModels(\n { baseUrl, ...(apiKey ? { apiKey } : {}) },\n io.fetch ? { fetch: io.fetch } : {},\n )\n ).map((m) => m.id)\n : [];\n if (!models.length)\n notes.push(\n local\n ? `nothing answered at ${baseUrl}; is the server running?`\n : `couldn't list models at ${baseUrl}; check the key`,\n );\n const first = models[0];\n return {\n name,\n added,\n keyStored,\n models,\n suggested: first ? `${name}:${first}` : undefined,\n notes,\n };\n}\n\nexport function describeConnect(r: ConnectResult): string {\n const lines = [\n `${r.added ? 'added' : 'updated'} ${r.name}${r.keyStored ? ' (key saved, 0600)' : ''}`,\n ];\n if (r.models.length) {\n const shown = r.models.slice(0, 8).join(', ');\n lines.push(\n `${r.models.length} model${r.models.length === 1 ? '' : 's'}: ${shown}${r.models.length > 8 ? ', …' : ''}`,\n );\n }\n for (const n of r.notes) lines.push(n);\n if (r.suggested) lines.push(`chat with it: /model ${r.suggested}`);\n return lines.join('\\n');\n}\n\nexport interface EnvKey {\n name: string;\n label: string;\n keyEnv: string;\n}\n\n/**\n * Providers whose standard key variable is already set in the environment but that aren't\n * configured yet: the fastest way to a working model on a machine that already has keys.\n */\nexport async function envKeys(io: CliIO): Promise<EnvKey[]> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const out: EnvKey[] = [];\n if (io.env.ANTHROPIC_API_KEY?.trim())\n out.push({ name: 'anthropic', label: 'Anthropic', keyEnv: 'ANTHROPIC_API_KEY' });\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (!t.keyEnv || config.providers.endpoints[name]) continue;\n if (io.env[t.keyEnv]?.trim()) out.push({ name, label: t.label ?? name, keyEnv: t.keyEnv });\n }\n return out;\n}\n\n/**\n * `/connect env`: add every provider found by envKeys(), pointing each endpoint at its env var\n * (the key is never copied into omnexx's files). Anthropic needs nothing: its key is read from\n * the environment already.\n */\nexport async function connectFromEnv(io: CliIO): Promise<EnvKey[]> {\n const found = await envKeys(io);\n const file = userConfigFile(resolvePaths(io.env));\n // One after another: every block is appended to the same file.\n await found.reduce(async (prev, k) => {\n await prev;\n const t = PROVIDER_TEMPLATES[k.name];\n if (t) await appendEndpointBlock(file, k.name, { ...t, keyEnv: k.keyEnv });\n }, Promise.resolve());\n return found;\n}\n"],"mappings":"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","names":["local"]}
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  envKeys
4
- } from "./chunk-AOIUX7TJ.js";
4
+ } from "./chunk-FC5LAF6U.js";
5
5
  import {
6
- discoverModels,
7
6
  findProviderKey,
8
7
  loadConfig,
9
8
  resolvePaths
10
- } from "./chunk-S3TUDM5B.js";
9
+ } from "./chunk-ZGB5IZJR.js";
10
+ import {
11
+ discoverModels
12
+ } from "./chunk-UX4MF6V4.js";
11
13
 
12
14
  // src/tui/model-picker.tsx
13
15
  import { Box, Text } from "ink";
@@ -143,4 +145,4 @@ export {
143
145
  ModelPicker,
144
146
  RankEditor
145
147
  };
146
- //# sourceMappingURL=chunk-UZ6WXVKI.js.map
148
+ //# sourceMappingURL=chunk-JHJW7UQV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tui/model-picker.tsx","../src/tui/colors.ts"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { findProviderKey } from '../auth/keys.js';\nimport type { CliIO } from '../cli/io.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths } from '../core/paths.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport { envKeys } from '../cli/connect.js';\nimport type { PoolMode } from '../config/ranking.js';\nimport { CYAN, GRAY, GREEN } from './colors.js';\n\nexport type PickerItem =\n | { kind: 'model'; ref: string; provider: string; model: string }\n | { kind: 'add-key'; label: string }\n | { kind: 'add-env'; label: string }\n | { kind: 'add-url'; label: string };\n\nexport interface ModelPickerState {\n items: PickerItem[];\n query: string;\n cursor: number;\n current: string | undefined;\n /** Picking for the pool: tab ticks several models, enter adds them all. */\n checked?: Set<string>;\n /** Providers whose model list couldn't be fetched (server down, bad key). */\n unreachable: string[];\n}\n\nconst ADD: PickerItem[] = [\n {\n kind: 'add-key',\n label: '+ paste an API key (OpenAI, OpenRouter, Groq, Gemini, xAI, Anthropic…)',\n },\n { kind: 'add-url', label: '+ add a custom endpoint (any OpenAI-compatible URL)' },\n];\n\n/** Built-in Anthropic aliases, offered when a key is set. */\nconst ANTHROPIC = ['opus', 'sonnet', 'haiku'];\n\n/**\n * Every model the person can chat with right now: Anthropic's (with a key) and each configured\n * endpoint's `/models` list (endpoints with a key or marked free), fetched in parallel.\n */\nexport async function loadModelChoices(\n io: CliIO,\n): Promise<{ items: PickerItem[]; unreachable: string[] }> {\n const paths = resolvePaths(io.env);\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const items: PickerItem[] = [];\n if (await findProviderKey(paths, io.env, 'anthropic'))\n for (const m of ANTHROPIC)\n items.push({ kind: 'model', ref: `anthropic:${m}`, provider: 'anthropic', model: m });\n const unreachable: string[] = [];\n const lists = await Promise.all(\n Object.entries(config.providers.endpoints).map(async ([name, ep]) => {\n const apiKey = (await findProviderKey(paths, io.env, name, ep.api_key_env))?.key;\n if (!apiKey && !ep.free) return [name, undefined] as const;\n const models = await discoverModels(\n { baseUrl: ep.base_url, ...(apiKey ? { apiKey } : {}) },\n { timeoutMs: 3_000, ...(io.fetch ? { fetch: io.fetch } : {}) },\n );\n return [name, models.map((m) => m.id)] as const;\n }),\n );\n for (const [name, ids] of lists) {\n if (!ids?.length) {\n unreachable.push(name);\n continue;\n }\n for (const id of ids)\n items.push({ kind: 'model', ref: `${name}:${id}`, provider: name, model: id });\n }\n const env = await envKeys(io).catch(() => []);\n const fromEnv: PickerItem[] = env.length\n ? [\n {\n kind: 'add-env',\n label: `+ use the keys in your environment (${env.map((k) => k.label).join(', ')})`,\n },\n ]\n : [];\n return { items: [...items, ...fromEnv, ...ADD], unreachable };\n}\n\n/** Items matching the typed query (every word must appear); the add actions always stay. */\nexport function filterChoices(items: readonly PickerItem[], query: string): PickerItem[] {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return [...items];\n return items.filter(\n (it) => it.kind !== 'model' || words.every((w) => it.ref.toLowerCase().includes(w)),\n );\n}\n\nconst VISIBLE = 12;\n\nexport function ModelPicker({ state, width }: { state: ModelPickerState; width: number }) {\n const shown = filterChoices(state.items, state.query);\n const cursor = Math.min(state.cursor, Math.max(0, shown.length - 1));\n const start = Math.max(0, Math.min(cursor - Math.floor(VISIBLE / 2), shown.length - VISIBLE));\n const page = shown.slice(start, start + VISIBLE);\n const models = shown.filter((i) => i.kind === 'model').length;\n return (\n <Box flexDirection=\"column\" borderStyle=\"single\" borderColor={GREEN} paddingX={1} width={width}>\n <Text>\n <Text color={GREEN} bold>\n choose a model\n </Text>\n <Text\n color={GRAY}\n >{` ${models} match${models === 1 ? '' : 'es'} · type to filter · ↑↓ ·${state.checked ? ` tab tick (${state.checked.size}) · enter add ticked ·` : ' enter ·'} esc`}</Text>\n </Text>\n <Text>\n <Text color={GRAY}>{'filter: '}</Text>\n <Text>{state.query}</Text>\n <Text color={GREEN}>█</Text>\n </Text>\n {page.map((it, i) => {\n const at = start + i === cursor;\n const box =\n state.checked && it.kind === 'model' ? (state.checked.has(it.ref) ? '[x] ' : '[ ] ') : '';\n const label =\n it.kind === 'model'\n ? `${box}${it.ref}${it.ref === state.current ? ' (current)' : ''}`\n : it.label;\n return (\n <Text key={it.kind === 'model' ? it.ref : it.kind} inverse={at} wrap=\"truncate-end\">\n <Text {...(at ? {} : { color: it.kind === 'model' ? CYAN : GREEN })}>{label}</Text>\n </Text>\n );\n })}\n {shown.length > VISIBLE && (\n <Text color={GRAY}>{` … ${shown.length - VISIBLE} more, keep typing to narrow`}</Text>\n )}\n {state.unreachable.length > 0 && (\n <Text color={GRAY}>{`not reachable right now: ${state.unreachable.join(', ')}`}</Text>\n )}\n </Box>\n );\n}\n\nexport interface RankState {\n ranked: string[];\n mode: PoolMode;\n /** Refs out of quota right now (skipped until their quota resets). */\n exhausted: Set<string>;\n cursor: number;\n /** Space picked the row up: ↑↓ move it instead of the cursor. */\n grabbed: boolean;\n}\n\nconst MODE_HINT: Record<PoolMode, string> = {\n ordered: 'order: top first. each model until its quota runs out, then the next',\n random: 'order: random. one model at a time, in a daily shuffle, until its quota runs out',\n smart: 'order: smart. Nimble picks the best model with quota left per action (else top first)',\n};\n\nconst ROLE_HINT = [\n 'chat, planner, worker and helpers try this first',\n 'first fallback',\n 'next fallback',\n];\n\n/** /models: the ranked provider:model list every role falls back through, top first. */\nexport function RankEditor({ state, width }: { state: RankState; width: number }) {\n return (\n <Box flexDirection=\"column\" borderStyle=\"single\" borderColor={GREEN} paddingX={1} width={width}>\n <Text>\n <Text color={GREEN} bold>\n your model pool\n </Text>\n <Text color={GRAY}>\n {' ↑↓ move · space pick up/drop · a add · x remove · m order · esc done'}\n </Text>\n </Text>\n <Text color={GRAY}>{MODE_HINT[state.mode]}</Text>\n {state.ranked.length === 0 && (\n <Text color={GRAY}>\n empty: roles use your config.toml models. press a to add your best model first.\n </Text>\n )}\n {state.ranked.map((ref, i) => (\n <Text key={ref} inverse={i === state.cursor} wrap=\"truncate-end\">\n <Text color={i === state.cursor && state.grabbed ? CYAN : GREEN}>\n {`${i + 1}. ${i === state.cursor && state.grabbed ? '⇕ ' : ''}${ref}`}\n </Text>\n <Text color={GRAY}>\n {state.exhausted.has(ref)\n ? ' out of quota, back after reset'\n : ` ${state.mode !== 'ordered' ? 'in the pool' : (ROLE_HINT[i] ?? 'fallback')}`}\n </Text>\n </Text>\n ))}\n </Box>\n );\n}\n","export const GREEN = '#00FF41';\nexport const CYAN = '#00E5FF';\nexport const GRAY = '#666666';\nexport const RED = '#FF3B30';\nexport const PINK = '#FF6EC7';\nexport const AMBER = '#FFC857';\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/tui/model-picker.tsx","../src/tui/colors.ts"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { findProviderKey } from '../auth/keys.js';\nimport type { CliIO } from '../cli/io.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths } from '../core/paths.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport { envKeys } from '../cli/connect.js';\nimport type { PoolMode } from '../config/ranking.js';\nimport { CYAN, GRAY, GREEN } from './colors.js';\n\nexport type PickerItem =\n | { kind: 'model'; ref: string; provider: string; model: string }\n | { kind: 'add-key'; label: string }\n | { kind: 'add-env'; label: string }\n | { kind: 'add-url'; label: string };\n\nexport interface ModelPickerState {\n items: PickerItem[];\n query: string;\n cursor: number;\n current: string | undefined;\n /** Picking for the pool: tab ticks several models, enter adds them all. */\n checked?: Set<string>;\n /** Providers whose model list couldn't be fetched (server down, bad key). */\n unreachable: string[];\n}\n\nconst ADD: PickerItem[] = [\n {\n kind: 'add-key',\n label: '+ paste an API key (OpenAI, OpenRouter, Groq, Gemini, xAI, Anthropic…)',\n },\n { kind: 'add-url', label: '+ add a custom endpoint (any OpenAI-compatible URL)' },\n];\n\n/** Built-in Anthropic aliases, offered when a key is set. */\nconst ANTHROPIC = ['opus', 'sonnet', 'haiku'];\n\n/**\n * Every model the person can chat with right now: Anthropic's (with a key) and each configured\n * endpoint's `/models` list (endpoints with a key or marked free), fetched in parallel.\n */\nexport async function loadModelChoices(\n io: CliIO,\n): Promise<{ items: PickerItem[]; unreachable: string[] }> {\n const paths = resolvePaths(io.env);\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const items: PickerItem[] = [];\n if (await findProviderKey(paths, io.env, 'anthropic'))\n for (const m of ANTHROPIC)\n items.push({ kind: 'model', ref: `anthropic:${m}`, provider: 'anthropic', model: m });\n const unreachable: string[] = [];\n const lists = await Promise.all(\n Object.entries(config.providers.endpoints).map(async ([name, ep]) => {\n const apiKey = (await findProviderKey(paths, io.env, name, ep.api_key_env))?.key;\n if (!apiKey && !ep.free) return [name, undefined] as const;\n const models = await discoverModels(\n { baseUrl: ep.base_url, ...(apiKey ? { apiKey } : {}) },\n { timeoutMs: 3_000, ...(io.fetch ? { fetch: io.fetch } : {}) },\n );\n return [name, models.map((m) => m.id)] as const;\n }),\n );\n for (const [name, ids] of lists) {\n if (!ids?.length) {\n unreachable.push(name);\n continue;\n }\n for (const id of ids)\n items.push({ kind: 'model', ref: `${name}:${id}`, provider: name, model: id });\n }\n const env = await envKeys(io).catch(() => []);\n const fromEnv: PickerItem[] = env.length\n ? [\n {\n kind: 'add-env',\n label: `+ use the keys in your environment (${env.map((k) => k.label).join(', ')})`,\n },\n ]\n : [];\n return { items: [...items, ...fromEnv, ...ADD], unreachable };\n}\n\n/** Items matching the typed query (every word must appear); the add actions always stay. */\nexport function filterChoices(items: readonly PickerItem[], query: string): PickerItem[] {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return [...items];\n return items.filter(\n (it) => it.kind !== 'model' || words.every((w) => it.ref.toLowerCase().includes(w)),\n );\n}\n\nconst VISIBLE = 12;\n\nexport function ModelPicker({ state, width }: { state: ModelPickerState; width: number }) {\n const shown = filterChoices(state.items, state.query);\n const cursor = Math.min(state.cursor, Math.max(0, shown.length - 1));\n const start = Math.max(0, Math.min(cursor - Math.floor(VISIBLE / 2), shown.length - VISIBLE));\n const page = shown.slice(start, start + VISIBLE);\n const models = shown.filter((i) => i.kind === 'model').length;\n return (\n <Box flexDirection=\"column\" borderStyle=\"single\" borderColor={GREEN} paddingX={1} width={width}>\n <Text>\n <Text color={GREEN} bold>\n choose a model\n </Text>\n <Text\n color={GRAY}\n >{` ${models} match${models === 1 ? '' : 'es'} · type to filter · ↑↓ ·${state.checked ? ` tab tick (${state.checked.size}) · enter add ticked ·` : ' enter ·'} esc`}</Text>\n </Text>\n <Text>\n <Text color={GRAY}>{'filter: '}</Text>\n <Text>{state.query}</Text>\n <Text color={GREEN}>█</Text>\n </Text>\n {page.map((it, i) => {\n const at = start + i === cursor;\n const box =\n state.checked && it.kind === 'model' ? (state.checked.has(it.ref) ? '[x] ' : '[ ] ') : '';\n const label =\n it.kind === 'model'\n ? `${box}${it.ref}${it.ref === state.current ? ' (current)' : ''}`\n : it.label;\n return (\n <Text key={it.kind === 'model' ? it.ref : it.kind} inverse={at} wrap=\"truncate-end\">\n <Text {...(at ? {} : { color: it.kind === 'model' ? CYAN : GREEN })}>{label}</Text>\n </Text>\n );\n })}\n {shown.length > VISIBLE && (\n <Text color={GRAY}>{` … ${shown.length - VISIBLE} more, keep typing to narrow`}</Text>\n )}\n {state.unreachable.length > 0 && (\n <Text color={GRAY}>{`not reachable right now: ${state.unreachable.join(', ')}`}</Text>\n )}\n </Box>\n );\n}\n\nexport interface RankState {\n ranked: string[];\n mode: PoolMode;\n /** Refs out of quota right now (skipped until their quota resets). */\n exhausted: Set<string>;\n cursor: number;\n /** Space picked the row up: ↑↓ move it instead of the cursor. */\n grabbed: boolean;\n}\n\nconst MODE_HINT: Record<PoolMode, string> = {\n ordered: 'order: top first. each model until its quota runs out, then the next',\n random: 'order: random. one model at a time, in a daily shuffle, until its quota runs out',\n smart: 'order: smart. Nimble picks the best model with quota left per action (else top first)',\n};\n\nconst ROLE_HINT = [\n 'chat, planner, worker and helpers try this first',\n 'first fallback',\n 'next fallback',\n];\n\n/** /models: the ranked provider:model list every role falls back through, top first. */\nexport function RankEditor({ state, width }: { state: RankState; width: number }) {\n return (\n <Box flexDirection=\"column\" borderStyle=\"single\" borderColor={GREEN} paddingX={1} width={width}>\n <Text>\n <Text color={GREEN} bold>\n your model pool\n </Text>\n <Text color={GRAY}>\n {' ↑↓ move · space pick up/drop · a add · x remove · m order · esc done'}\n </Text>\n </Text>\n <Text color={GRAY}>{MODE_HINT[state.mode]}</Text>\n {state.ranked.length === 0 && (\n <Text color={GRAY}>\n empty: roles use your config.toml models. press a to add your best model first.\n </Text>\n )}\n {state.ranked.map((ref, i) => (\n <Text key={ref} inverse={i === state.cursor} wrap=\"truncate-end\">\n <Text color={i === state.cursor && state.grabbed ? CYAN : GREEN}>\n {`${i + 1}. ${i === state.cursor && state.grabbed ? '⇕ ' : ''}${ref}`}\n </Text>\n <Text color={GRAY}>\n {state.exhausted.has(ref)\n ? ' out of quota, back after reset'\n : ` ${state.mode !== 'ordered' ? 'in the pool' : (ROLE_HINT[i] ?? 'fallback')}`}\n </Text>\n </Text>\n ))}\n </Box>\n );\n}\n","export const GREEN = '#00FF41';\nexport const CYAN = '#00E5FF';\nexport const GRAY = '#666666';\nexport const RED = '#FF3B30';\nexport const PINK = '#FF6EC7';\nexport const AMBER = '#FFC857';\n"],"mappings":"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","names":[]}
@@ -11,7 +11,7 @@ import {
11
11
  parseDuration,
12
12
  resolvePaths,
13
13
  userConfigFile
14
- } from "./chunk-S3TUDM5B.js";
14
+ } from "./chunk-ZGB5IZJR.js";
15
15
  import {
16
16
  UsageError
17
17
  } from "./chunk-7PNGHZYH.js";
@@ -275,4 +275,4 @@ export {
275
275
  removeEndpointBlock,
276
276
  register
277
277
  };
278
- //# sourceMappingURL=chunk-5T4EFIY2.js.map
278
+ //# sourceMappingURL=chunk-M23TQTS6.js.map