omnexx 0.6.0 → 0.8.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 (66) hide show
  1. package/README.md +53 -4
  2. package/dist/chat-context-OL7LLKBT.js +120 -0
  3. package/dist/chat-context-OL7LLKBT.js.map +1 -0
  4. package/dist/{chat-server-NB3GPOMD.js → chat-server-HUYEZGT7.js} +90 -11
  5. package/dist/chat-server-HUYEZGT7.js.map +1 -0
  6. package/dist/{chat-settings-D3WKVSFU.js → chat-settings-4OW4MUSE.js} +47 -10
  7. package/dist/{chat-settings-D3WKVSFU.js.map → chat-settings-4OW4MUSE.js.map} +1 -1
  8. package/dist/{chunk-55VJCO7L.js → chunk-263JZDVT.js} +60 -21
  9. package/dist/chunk-263JZDVT.js.map +1 -0
  10. package/dist/chunk-6OQWQDGP.js +1038 -0
  11. package/dist/chunk-6OQWQDGP.js.map +1 -0
  12. package/dist/chunk-7T24V7UU.js +701 -0
  13. package/dist/chunk-7T24V7UU.js.map +1 -0
  14. package/dist/chunk-ANSOFQ32.js +67 -0
  15. package/dist/chunk-ANSOFQ32.js.map +1 -0
  16. package/dist/{chunk-M64SDRL3.js → chunk-CNUYDXSI.js} +68 -21
  17. package/dist/chunk-CNUYDXSI.js.map +1 -0
  18. package/dist/chunk-DGKPM32R.js +60 -0
  19. package/dist/chunk-DGKPM32R.js.map +1 -0
  20. package/dist/{chunk-N2WJ2YTX.js → chunk-JEHAJ45I.js} +677 -1211
  21. package/dist/chunk-JEHAJ45I.js.map +1 -0
  22. package/dist/{chunk-PY47EEHG.js → chunk-KC7YVX2B.js} +115 -89
  23. package/dist/chunk-KC7YVX2B.js.map +1 -0
  24. package/dist/{chunk-UZ6WXVKI.js → chunk-L43V7MUO.js} +12 -6
  25. package/dist/{chunk-UZ6WXVKI.js.map → chunk-L43V7MUO.js.map} +1 -1
  26. package/dist/{chunk-5T4EFIY2.js → chunk-O75OQQDP.js} +9 -7
  27. package/dist/{chunk-5T4EFIY2.js.map → chunk-O75OQQDP.js.map} +1 -1
  28. package/dist/chunk-TJWLF5MU.js +674 -0
  29. package/dist/chunk-TJWLF5MU.js.map +1 -0
  30. package/dist/{chunk-AOIUX7TJ.js → chunk-UCIHYU6B.js} +12 -8
  31. package/dist/{chunk-AOIUX7TJ.js.map → chunk-UCIHYU6B.js.map} +1 -1
  32. package/dist/chunk-UX4MF6V4.js +70 -0
  33. package/dist/chunk-UX4MF6V4.js.map +1 -0
  34. package/dist/{chunk-7PNGHZYH.js → chunk-XQTM3SWX.js} +23 -1
  35. package/dist/chunk-XQTM3SWX.js.map +1 -0
  36. package/dist/chunk-YEUB2OJI.js +65 -0
  37. package/dist/chunk-YEUB2OJI.js.map +1 -0
  38. package/dist/cli.js +86 -37
  39. package/dist/cli.js.map +1 -1
  40. package/dist/discovery-2LUVI2SX.js +11 -0
  41. package/dist/model-picker-M2ZETIKE.js +23 -0
  42. package/dist/{ollama-V6LN6HAR.js → ollama-HHDVJRPX.js} +8 -5
  43. package/dist/{ollama-V6LN6HAR.js.map → ollama-HHDVJRPX.js.map} +1 -1
  44. package/dist/{print-OUSVBDAA.js → print-WTEHAUQ6.js} +10 -5
  45. package/dist/{print-OUSVBDAA.js.map → print-WTEHAUQ6.js.map} +1 -1
  46. package/dist/quota-ledger-7F6QTZAM.js +11 -0
  47. package/dist/quota-ledger-7F6QTZAM.js.map +1 -0
  48. package/dist/roles-PPNR2ECT.js +21 -0
  49. package/dist/roles-PPNR2ECT.js.map +1 -0
  50. package/dist/session-GUDTYRZ5.js +32 -0
  51. package/dist/session-GUDTYRZ5.js.map +1 -0
  52. package/dist/{start-ZJQLZBS6.js → start-3JFBG6PM.js} +16 -10
  53. package/dist/{start-ZJQLZBS6.js.map → start-3JFBG6PM.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/dist/chat-server-NB3GPOMD.js.map +0 -1
  56. package/dist/chunk-55VJCO7L.js.map +0 -1
  57. package/dist/chunk-7PNGHZYH.js.map +0 -1
  58. package/dist/chunk-M64SDRL3.js.map +0 -1
  59. package/dist/chunk-N2WJ2YTX.js.map +0 -1
  60. package/dist/chunk-PY47EEHG.js.map +0 -1
  61. package/dist/chunk-S3TUDM5B.js +0 -1560
  62. package/dist/chunk-S3TUDM5B.js.map +0 -1
  63. package/dist/model-picker-7NLP3QO6.js +0 -20
  64. package/dist/session-YXEM22IA.js +0 -26
  65. /package/dist/{model-picker-7NLP3QO6.js.map → discovery-2LUVI2SX.js.map} +0 -0
  66. /package/dist/{session-YXEM22IA.js.map → model-picker-M2ZETIKE.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
 
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ repoRoot
4
+ } from "./chunk-YEUB2OJI.js";
5
+ import {
6
+ RunStore,
7
+ listRunIds,
8
+ repoMemoryFile
9
+ } from "./chunk-7T24V7UU.js";
10
+ import {
11
+ SECTION_IDS,
12
+ SECTION_INFO,
13
+ SECTION_MODES,
14
+ estimateTokens,
15
+ loadConfig,
16
+ readSectionModes,
17
+ sectionsFile
18
+ } from "./chunk-6OQWQDGP.js";
19
+ import {
20
+ UsageError,
21
+ resolvePaths
22
+ } from "./chunk-XQTM3SWX.js";
23
+ import "./chunk-ANSOFQ32.js";
24
+ import "./chunk-2H7UOFLK.js";
25
+
26
+ // src/cli/chat-context.ts
27
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
28
+ import { dirname, join } from "path";
29
+ async function setSectionMode(io, id, mode) {
30
+ if (!SECTION_IDS.includes(id)) throw new UsageError(`no section "${id}"`);
31
+ if (!SECTION_MODES.includes(mode))
32
+ throw new UsageError(`mode is one of ${SECTION_MODES.join(", ")}`);
33
+ const p = resolvePaths(io.env);
34
+ const modes = { ...await readSectionModes(p), [id]: mode };
35
+ await mkdir(dirname(sectionsFile(p)), { recursive: true });
36
+ await writeFile(sectionsFile(p), `${JSON.stringify(modes, null, 2)}
37
+ `);
38
+ }
39
+ async function sizeOf(path) {
40
+ try {
41
+ if (!(await stat(path)).isFile()) return { exists: false, tokens: 0 };
42
+ return { exists: true, tokens: estimateTokens(await readFile(path, "utf8")) };
43
+ } catch {
44
+ return { exists: false, tokens: 0 };
45
+ }
46
+ }
47
+ async function latestRun(paths, root) {
48
+ for (const id of [...await listRunIds(paths)].reverse()) {
49
+ const store = new RunStore(paths, id);
50
+ const s = await store.readState().catch(() => void 0);
51
+ if (s?.repoRoot === root) return store;
52
+ }
53
+ return void 0;
54
+ }
55
+ async function contextFiles(io) {
56
+ const paths = resolvePaths(io.env);
57
+ const root = await repoRoot(io.cwd).catch(() => io.cwd);
58
+ const { config } = await loadConfig({ cwd: io.cwd, env: io.env });
59
+ const modes = config.context.sections;
60
+ const run = await latestRun(paths, root);
61
+ const state = run ? await run.readState().catch(() => void 0) : void 0;
62
+ const memoryDir = dirname(await repoMemoryFile({ home: paths.home }, root));
63
+ const entries = [];
64
+ const section = (id, path, editable) => {
65
+ const info = SECTION_INFO[id];
66
+ entries.push({
67
+ id,
68
+ label: info.label,
69
+ about: info.about,
70
+ path,
71
+ editable,
72
+ section: id,
73
+ mode: modes[id]
74
+ });
75
+ };
76
+ if (run) {
77
+ if (state?.taskId) section("role", run.file(join("roles", `${state.taskId}.md`)), true);
78
+ section("codemap", run.file("codemap.md"), false);
79
+ section("intent", run.file("intent.md"), true);
80
+ section("lessons", run.file("notes.md"), false);
81
+ section("progress", run.file("progress.md"), false);
82
+ entries.push({
83
+ id: "goal",
84
+ label: "Goal",
85
+ about: "what this run is for",
86
+ path: run.file("goal.md"),
87
+ editable: true
88
+ });
89
+ }
90
+ entries.push(
91
+ {
92
+ id: "repo-role",
93
+ label: "Repo role note",
94
+ about: "added to every role card in this repo",
95
+ path: join(root, ".omnexx", "role.md"),
96
+ editable: true
97
+ },
98
+ {
99
+ id: "general-notes",
100
+ label: "General notes",
101
+ about: "what the agent keeps about this repo across runs",
102
+ path: join(memoryDir, "scopes", "general.md"),
103
+ editable: true
104
+ },
105
+ {
106
+ id: "repo-notes",
107
+ label: "Repo notes",
108
+ about: "architecture and where things live, across runs",
109
+ path: join(memoryDir, "scopes", "repo.md"),
110
+ editable: true
111
+ }
112
+ );
113
+ const files = await Promise.all(entries.map(async (e) => ({ ...e, ...await sizeOf(e.path) })));
114
+ return { runId: run?.runId ?? null, files };
115
+ }
116
+ export {
117
+ contextFiles,
118
+ setSectionMode
119
+ };
120
+ //# sourceMappingURL=chat-context-OL7LLKBT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/chat-context.ts"],"sourcesContent":["import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport {\n SECTION_IDS,\n SECTION_INFO,\n SECTION_MODES,\n type SectionId,\n type SectionMode,\n} from '../agent/sections.js';\nimport { loadConfig } from '../config/load.js';\nimport { readSectionModes, sectionsFile } from '../config/sections-file.js';\nimport { resolvePaths, type OmnexxPaths } from '../core/paths.js';\nimport { repoMemoryFile } from '../core/repo-memory.js';\nimport { listRunIds, RunStore } from '../core/run-store.js';\nimport { estimateTokens } from '../core/tokens.js';\nimport { UsageError } from '../errors.js';\nimport { repoRoot } from '../git/repo.js';\nimport type { CliIO } from './io.js';\n\n/**\n * The Context explorer's data: every file that feeds the agent's context in this workspace, its\n * size in tokens, whether it can be edited, and (for sections) how it reaches the model. Section\n * modes are saved to context-sections.json and loaded as a config layer.\n */\n\nexport interface ContextFile {\n id: string;\n label: string;\n about: string;\n path: string;\n tokens: number;\n exists: boolean;\n /** Edits are read on the next cycle; generated files are views. */\n editable: boolean;\n /** For context sections: how it reaches the model. */\n section?: SectionId;\n mode?: SectionMode;\n}\n\nexport async function setSectionMode(io: CliIO, id: string, mode: string): Promise<void> {\n if (!(SECTION_IDS as readonly string[]).includes(id)) throw new UsageError(`no section \"${id}\"`);\n if (!(SECTION_MODES as readonly string[]).includes(mode))\n throw new UsageError(`mode is one of ${SECTION_MODES.join(', ')}`);\n const p = resolvePaths(io.env);\n const modes = { ...(await readSectionModes(p)), [id]: mode };\n await mkdir(dirname(sectionsFile(p)), { recursive: true });\n await writeFile(sectionsFile(p), `${JSON.stringify(modes, null, 2)}\\n`);\n}\n\nasync function sizeOf(path: string): Promise<{ exists: boolean; tokens: number }> {\n try {\n if (!(await stat(path)).isFile()) return { exists: false, tokens: 0 };\n return { exists: true, tokens: estimateTokens(await readFile(path, 'utf8')) };\n } catch {\n return { exists: false, tokens: 0 };\n }\n}\n\n/** The newest run started from this repository, if any. */\nasync function latestRun(paths: OmnexxPaths, root: string): Promise<RunStore | undefined> {\n for (const id of [...(await listRunIds(paths))].reverse()) {\n const store = new RunStore(paths, id);\n const s = await store.readState().catch(() => undefined);\n if (s?.repoRoot === root) return store;\n }\n return undefined;\n}\n\nexport async function contextFiles(\n io: CliIO,\n): Promise<{ runId: string | null; files: ContextFile[] }> {\n const paths = resolvePaths(io.env);\n const root = await repoRoot(io.cwd).catch(() => io.cwd);\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const modes = config.context.sections;\n const run = await latestRun(paths, root);\n const state = run ? await run.readState().catch(() => undefined) : undefined;\n const memoryDir = dirname(await repoMemoryFile({ home: paths.home }, root));\n\n const entries: Omit<ContextFile, 'tokens' | 'exists'>[] = [];\n const section = (id: SectionId, path: string, editable: boolean): void => {\n const info = SECTION_INFO[id];\n entries.push({\n id,\n label: info.label,\n about: info.about,\n path,\n editable,\n section: id,\n mode: modes[id],\n });\n };\n if (run) {\n if (state?.taskId) section('role', run.file(join('roles', `${state.taskId}.md`)), true);\n section('codemap', run.file('codemap.md'), false);\n section('intent', run.file('intent.md'), true);\n section('lessons', run.file('notes.md'), false);\n section('progress', run.file('progress.md'), false);\n entries.push({\n id: 'goal',\n label: 'Goal',\n about: 'what this run is for',\n path: run.file('goal.md'),\n editable: true,\n });\n }\n entries.push(\n {\n id: 'repo-role',\n label: 'Repo role note',\n about: 'added to every role card in this repo',\n path: join(root, '.omnexx', 'role.md'),\n editable: true,\n },\n {\n id: 'general-notes',\n label: 'General notes',\n about: 'what the agent keeps about this repo across runs',\n path: join(memoryDir, 'scopes', 'general.md'),\n editable: true,\n },\n {\n id: 'repo-notes',\n label: 'Repo notes',\n about: 'architecture and where things live, across runs',\n path: join(memoryDir, 'scopes', 'repo.md'),\n editable: true,\n },\n );\n const files = await Promise.all(entries.map(async (e) => ({ ...e, ...(await sizeOf(e.path)) })));\n return { runId: run?.runId ?? null, files };\n}\n"],"mappings":"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","names":[]}
@@ -3,12 +3,16 @@ import {
3
3
  println
4
4
  } from "./chunk-7PC4SSYS.js";
5
5
  import {
6
- describeError
7
- } from "./chunk-7PNGHZYH.js";
6
+ describeError,
7
+ resolvePaths
8
+ } from "./chunk-XQTM3SWX.js";
8
9
  import "./chunk-2H7UOFLK.js";
9
10
 
10
11
  // src/cli/chat-server.ts
12
+ import { join } from "path";
13
+ import { readFile } from "fs/promises";
11
14
  import { createInterface } from "readline";
15
+ var MAX_FRAME_BYTES = 4 * 1024 * 1024;
12
16
  var MODES = /* @__PURE__ */ new Set(["chat", "plan", "run"]);
13
17
  var EMIT_EVERY_MS = 40;
14
18
  function parseCommand(line) {
@@ -28,7 +32,7 @@ function text(c, key) {
28
32
  return v;
29
33
  }
30
34
  async function serveChat(io, runCli, opts) {
31
- const { Session } = await import("./session-YXEM22IA.js");
35
+ const { Session } = await import("./session-GUDTYRZ5.js");
32
36
  const session = new Session(io, runCli);
33
37
  const write = (msg) => {
34
38
  println(io.stdout, JSON.stringify(msg));
@@ -37,6 +41,22 @@ async function serveChat(io, runCli, opts) {
37
41
  write({ type: "error", message: describeError(err) });
38
42
  };
39
43
  let sentUpTo = -1;
44
+ let sentFrame = 0;
45
+ const sendFrame = async () => {
46
+ const f = session.browserFrame;
47
+ if (!f || f.seq === sentFrame) return;
48
+ sentFrame = f.seq;
49
+ const data = await readFile(f.path).catch(() => void 0);
50
+ if (!data || data.length > MAX_FRAME_BYTES) return;
51
+ write({
52
+ type: "browserFrame",
53
+ seq: f.seq,
54
+ mime: f.mime,
55
+ url: f.url,
56
+ action: f.action,
57
+ data: data.toString("base64")
58
+ });
59
+ };
40
60
  let clearedAt = -1;
41
61
  let timer;
42
62
  const emit = () => {
@@ -65,16 +85,24 @@ async function serveChat(io, runCli, opts) {
65
85
  toolErrors: t.toolErrors,
66
86
  compactions: t.compactions
67
87
  } : null,
68
- run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null
88
+ run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null,
89
+ helpers: session.helpers,
90
+ browser: session.browserFrame ? {
91
+ seq: session.browserFrame.seq,
92
+ url: session.browserFrame.url,
93
+ action: session.browserFrame.action,
94
+ at: session.browserFrame.at
95
+ } : null
69
96
  };
70
97
  write(state);
98
+ void sendFrame();
71
99
  };
72
100
  const schedule = () => {
73
101
  timer ??= setTimeout(emit, EMIT_EVERY_MS);
74
102
  };
75
103
  session.onChange(schedule);
76
104
  const sendModels = async () => {
77
- const { loadModelChoices } = await import("./model-picker-7NLP3QO6.js");
105
+ const { loadModelChoices } = await import("./model-picker-M2ZETIKE.js");
78
106
  const { items, unreachable } = await loadModelChoices(io);
79
107
  write({
80
108
  type: "models",
@@ -84,16 +112,35 @@ async function serveChat(io, runCli, opts) {
84
112
  });
85
113
  };
86
114
  const sendSettings = async () => {
87
- const { providerSettings } = await import("./chat-settings-D3WKVSFU.js");
115
+ const { providerSettings } = await import("./chat-settings-4OW4MUSE.js");
88
116
  write({ type: "settings", ...await providerSettings(io) });
89
117
  };
118
+ const sendContextFiles = async () => {
119
+ const { contextFiles } = await import("./chat-context-OL7LLKBT.js");
120
+ write({ type: "contextFiles", ...await contextFiles(io) });
121
+ };
122
+ const sendRoles = async () => {
123
+ const { readRoles } = await import("./roles-PPNR2ECT.js");
124
+ const { QuotaLedger } = await import("./quota-ledger-7F6QTZAM.js");
125
+ const paths = resolvePaths(io.env);
126
+ write({
127
+ type: "roles",
128
+ roles: await readRoles(paths),
129
+ exhausted: new QuotaLedger(join(paths.configHome, "quota.json")).all()
130
+ });
131
+ };
90
132
  const providersChanged = async (message) => {
91
133
  session.push("system", message);
92
134
  await sendSettings();
93
135
  await sendModels();
94
136
  };
137
+ const inflight = /* @__PURE__ */ new Set();
95
138
  const run = (p) => {
96
- p.catch(fail).finally(schedule);
139
+ const tracked = p.catch(fail).finally(() => {
140
+ inflight.delete(tracked);
141
+ schedule();
142
+ });
143
+ inflight.add(tracked);
97
144
  };
98
145
  const handle = (c) => {
99
146
  switch (c.type) {
@@ -128,7 +175,7 @@ async function serveChat(io, runCli, opts) {
128
175
  return;
129
176
  }
130
177
  case "yolo":
131
- if (c.on === true !== session.autoApprove) run(session.submit("/yolo"));
178
+ if (c.on === true !== session.autoApprove) run(session.command("yolo"));
132
179
  return;
133
180
  case "model":
134
181
  run(session.switchModel(text(c, "ref")));
@@ -145,7 +192,7 @@ async function serveChat(io, runCli, opts) {
145
192
  return typeof v === "string" ? v : void 0;
146
193
  };
147
194
  run(
148
- import("./chat-settings-D3WKVSFU.js").then(async ({ addProvider }) => {
195
+ import("./chat-settings-4OW4MUSE.js").then(async ({ addProvider }) => {
149
196
  await providersChanged(
150
197
  await addProvider(io, {
151
198
  provider: str("provider"),
@@ -157,9 +204,39 @@ async function serveChat(io, runCli, opts) {
157
204
  );
158
205
  return;
159
206
  }
207
+ case "testProvider":
208
+ run(
209
+ import("./chat-settings-4OW4MUSE.js").then(async ({ testProvider }) => {
210
+ write({ type: "providerTest", ...await testProvider(io, text(c, "name")) });
211
+ })
212
+ );
213
+ return;
214
+ case "contextFiles":
215
+ run(sendContextFiles());
216
+ return;
217
+ case "setSectionMode":
218
+ run(
219
+ import("./chat-context-OL7LLKBT.js").then(async ({ setSectionMode }) => {
220
+ await setSectionMode(io, text(c, "id"), text(c, "mode"));
221
+ await sendContextFiles();
222
+ })
223
+ );
224
+ return;
225
+ case "roles":
226
+ run(sendRoles());
227
+ return;
228
+ case "setRoles":
229
+ run(
230
+ import("./roles-PPNR2ECT.js").then(async ({ parseRoles, writeRoles }) => {
231
+ await writeRoles(resolvePaths(io.env), parseRoles(c.roles));
232
+ session.push("system", "model roles saved; new chats and runs use them");
233
+ await sendRoles();
234
+ })
235
+ );
236
+ return;
160
237
  case "removeProvider":
161
238
  run(
162
- import("./chat-settings-D3WKVSFU.js").then(async ({ removeProvider }) => {
239
+ import("./chat-settings-4OW4MUSE.js").then(async ({ removeProvider }) => {
163
240
  await providersChanged(await removeProvider(io, text(c, "name")));
164
241
  })
165
242
  );
@@ -171,6 +248,7 @@ async function serveChat(io, runCli, opts) {
171
248
  }
172
249
  case "sync":
173
250
  sentUpTo = clearedAt;
251
+ sentFrame = 0;
174
252
  write({ type: "ready", version: opts.version });
175
253
  emit();
176
254
  return;
@@ -193,6 +271,7 @@ async function serveChat(io, runCli, opts) {
193
271
  }
194
272
  }
195
273
  if (session.chatBusy) await session.command("stop");
274
+ await Promise.allSettled([...inflight]);
196
275
  if (timer) {
197
276
  clearTimeout(timer);
198
277
  emit();
@@ -202,4 +281,4 @@ async function serveChat(io, runCli, opts) {
202
281
  export {
203
282
  serveChat
204
283
  };
205
- //# sourceMappingURL=chat-server-NB3GPOMD.js.map
284
+ //# sourceMappingURL=chat-server-HUYEZGT7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/chat-server.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { resolvePaths } from '../core/paths.js';\nimport { 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 * roles · setRoles {roles} · contextFiles · setSectionMode {id, mode}.\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 const sendContextFiles = async (): Promise<void> => {\n const { contextFiles } = await import('./chat-context.js');\n write({ type: 'contextFiles', ...(await contextFiles(io)) });\n };\n /** Roles as saved, plus which models are out of quota right now (ref → when it ran out). */\n const sendRoles = async (): Promise<void> => {\n const { readRoles } = await import('../config/roles.js');\n const { QuotaLedger } = await import('../providers/quota-ledger.js');\n const paths = resolvePaths(io.env);\n write({\n type: 'roles',\n roles: await readRoles(paths),\n exhausted: new QuotaLedger(join(paths.configHome, 'quota.json')).all(),\n });\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 run it when the state differs from what was asked, and without\n // echoing it as a message.\n if ((c.on === true) !== session.autoApprove) run(session.command('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 'contextFiles':\n run(sendContextFiles());\n return;\n case 'setSectionMode':\n run(\n import('./chat-context.js').then(async ({ setSectionMode }) => {\n await setSectionMode(io, text(c, 'id'), text(c, 'mode'));\n await sendContextFiles();\n }),\n );\n return;\n case 'roles':\n run(sendRoles());\n return;\n case 'setRoles':\n run(\n import('../config/roles.js').then(async ({ parseRoles, writeRoles }) => {\n await writeRoles(resolvePaths(io.env), parseRoles(c.roles));\n session.push('system', 'model roles saved; new chats and runs use them');\n await sendRoles();\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,23 +2,27 @@
2
2
  import {
3
3
  connect,
4
4
  describeConnect
5
- } from "./chunk-AOIUX7TJ.js";
5
+ } from "./chunk-UCIHYU6B.js";
6
6
  import {
7
7
  PROVIDER_TEMPLATES,
8
8
  removeEndpointBlock
9
- } from "./chunk-5T4EFIY2.js";
9
+ } from "./chunk-O75OQQDP.js";
10
10
  import "./chunk-7PC4SSYS.js";
11
11
  import {
12
12
  clearKey,
13
13
  findProviderKey,
14
- loadConfig,
15
- maskKey,
14
+ maskKey
15
+ } from "./chunk-TJWLF5MU.js";
16
+ import {
17
+ loadConfig
18
+ } from "./chunk-6OQWQDGP.js";
19
+ import {
20
+ UsageError,
16
21
  resolvePaths,
17
22
  userConfigFile
18
- } from "./chunk-S3TUDM5B.js";
19
- import {
20
- UsageError
21
- } from "./chunk-7PNGHZYH.js";
23
+ } from "./chunk-XQTM3SWX.js";
24
+ import "./chunk-ANSOFQ32.js";
25
+ import "./chunk-UX4MF6V4.js";
22
26
  import "./chunk-2H7UOFLK.js";
23
27
 
24
28
  // src/cli/chat-settings.ts
@@ -101,10 +105,43 @@ async function removeProvider(io, name) {
101
105
  return `${lower} had nothing to remove (a key from an environment variable stays set)`;
102
106
  return `removed ${lower}${removedKey ? " and its stored key" : ""}`;
103
107
  }
108
+ var TEST_TIMEOUT_MS = 8e3;
109
+ async function testProvider(io, name) {
110
+ const lower = name.trim().toLowerCase();
111
+ if (!NAME.test(lower)) throw new UsageError(`"${name}" is not a provider name`);
112
+ const paths = resolvePaths(io.env);
113
+ if (lower === "anthropic") {
114
+ const found = await findProviderKey(paths, io.env, "anthropic");
115
+ return {
116
+ name: lower,
117
+ ok: found !== void 0,
118
+ models: found ? 3 : 0,
119
+ detail: found ? "key set (opus, sonnet, haiku)" : "no key set"
120
+ };
121
+ }
122
+ const { config } = await loadConfig({ cwd: io.cwd, env: io.env });
123
+ const ep = config.providers.endpoints[lower];
124
+ if (!ep) return { name: lower, ok: false, models: 0, detail: "not set up" };
125
+ const key = (await findProviderKey(paths, io.env, lower, ep.api_key_env))?.key;
126
+ const { discoverModels } = await import("./discovery-2LUVI2SX.js");
127
+ const models = await discoverModels(
128
+ { baseUrl: ep.base_url, ...key ? { apiKey: key } : {} },
129
+ { timeoutMs: TEST_TIMEOUT_MS, ...io.fetch ? { fetch: io.fetch } : {} }
130
+ );
131
+ if (models.length)
132
+ return { name: lower, ok: true, models: models.length, detail: `${models.length} models` };
133
+ return {
134
+ name: lower,
135
+ ok: false,
136
+ models: 0,
137
+ detail: key || ep.free ? "couldn't reach it or it listed no models" : "no key set"
138
+ };
139
+ }
104
140
  export {
105
141
  addProvider,
106
142
  providerCatalog,
107
143
  providerSettings,
108
- removeProvider
144
+ removeProvider,
145
+ testProvider
109
146
  };
110
- //# sourceMappingURL=chat-settings-D3WKVSFU.js.map
147
+ //# sourceMappingURL=chat-settings-4OW4MUSE.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":"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","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":[]}