omnexx 0.11.3 → 0.13.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 (73) hide show
  1. package/README.md +17 -4
  2. package/dist/{chat-context-PTZRLJEV.js → chat-context-P4ZON7M4.js} +4 -3
  3. package/dist/{chat-context-PTZRLJEV.js.map → chat-context-P4ZON7M4.js.map} +1 -1
  4. package/dist/chat-history-SMI5AWQG.js +21 -0
  5. package/dist/{chat-server-HFGPZ7RU.js → chat-server-FMF26CBV.js} +55 -11
  6. package/dist/chat-server-FMF26CBV.js.map +1 -0
  7. package/dist/{chat-settings-X7R4JKI7.js → chat-settings-4XUKDH6H.js} +10 -9
  8. package/dist/{chat-settings-X7R4JKI7.js.map → chat-settings-4XUKDH6H.js.map} +1 -1
  9. package/dist/chunk-23MT7K6X.js +209 -0
  10. package/dist/chunk-23MT7K6X.js.map +1 -0
  11. package/dist/{chunk-3Y7NVLVI.js → chunk-3OILFLAA.js} +424 -50
  12. package/dist/chunk-3OILFLAA.js.map +1 -0
  13. package/dist/{chunk-4DX4BPIF.js → chunk-ET6OMKAS.js} +4 -2
  14. package/dist/chunk-ET6OMKAS.js.map +1 -0
  15. package/dist/chunk-FR5DUUJ4.js +56 -0
  16. package/dist/chunk-FR5DUUJ4.js.map +1 -0
  17. package/dist/chunk-FSA52KQB.js +569 -0
  18. package/dist/chunk-FSA52KQB.js.map +1 -0
  19. package/dist/{chunk-LRBHQHO6.js → chunk-GBFAEDOD.js} +47 -4
  20. package/dist/chunk-GBFAEDOD.js.map +1 -0
  21. package/dist/chunk-H4XHEBFQ.js +257 -0
  22. package/dist/chunk-H4XHEBFQ.js.map +1 -0
  23. package/dist/{chunk-6U3WB5AL.js → chunk-KJ5SPE6X.js} +718 -115
  24. package/dist/chunk-KJ5SPE6X.js.map +1 -0
  25. package/dist/{chunk-YAWRCULP.js → chunk-L2YBK4AY.js} +18 -6
  26. package/dist/chunk-L2YBK4AY.js.map +1 -0
  27. package/dist/{chunk-AXEJCZPA.js → chunk-MC4NBIIA.js} +7 -7
  28. package/dist/{chunk-FLIPPDUD.js → chunk-PRAOJIKH.js} +148 -219
  29. package/dist/chunk-PRAOJIKH.js.map +1 -0
  30. package/dist/{chunk-LZGGM2YZ.js → chunk-RSQGFSCA.js} +4 -4
  31. package/dist/{chunk-UAW5RG53.js → chunk-V5RPCSON.js} +43 -71
  32. package/dist/chunk-V5RPCSON.js.map +1 -0
  33. package/dist/chunk-WXMTUSCS.js +88 -0
  34. package/dist/chunk-WXMTUSCS.js.map +1 -0
  35. package/dist/{chunk-IGUI6IJP.js → chunk-XKO2GYOF.js} +1067 -817
  36. package/dist/chunk-XKO2GYOF.js.map +1 -0
  37. package/dist/chunk-XP4J622W.js +35 -0
  38. package/dist/chunk-XP4J622W.js.map +1 -0
  39. package/dist/{chunk-E6NF2XSG.js → chunk-YJ7BPHUB.js} +4 -4
  40. package/dist/cli.js +99 -61
  41. package/dist/cli.js.map +1 -1
  42. package/dist/{list-4L6TDLAZ.js → list-V6A7DKXF.js} +2 -2
  43. package/dist/{model-picker-JKB7LVQR.js → model-picker-IR7QMEWU.js} +7 -6
  44. package/dist/ollama-setup-EJY37RBS.js +31 -0
  45. package/dist/ollama-setup-EJY37RBS.js.map +1 -0
  46. package/dist/ollama-start-EBZJFMNR.js +72 -0
  47. package/dist/ollama-start-EBZJFMNR.js.map +1 -0
  48. package/dist/{print-AMD3ZAUQ.js → print-HXC5CFGD.js} +11 -7
  49. package/dist/{print-AMD3ZAUQ.js.map → print-HXC5CFGD.js.map} +1 -1
  50. package/dist/session-2YQYN6VU.js +41 -0
  51. package/dist/session-2YQYN6VU.js.map +1 -0
  52. package/dist/{start-LXX7MA37.js → start-KMBA2W6F.js} +30 -15
  53. package/dist/start-KMBA2W6F.js.map +1 -0
  54. package/package.json +2 -1
  55. package/dist/chat-server-HFGPZ7RU.js.map +0 -1
  56. package/dist/chunk-3Y7NVLVI.js.map +0 -1
  57. package/dist/chunk-4DX4BPIF.js.map +0 -1
  58. package/dist/chunk-6U3WB5AL.js.map +0 -1
  59. package/dist/chunk-FLIPPDUD.js.map +0 -1
  60. package/dist/chunk-IGUI6IJP.js.map +0 -1
  61. package/dist/chunk-LRBHQHO6.js.map +0 -1
  62. package/dist/chunk-UAW5RG53.js.map +0 -1
  63. package/dist/chunk-YAWRCULP.js.map +0 -1
  64. package/dist/ollama-G2MYWBEE.js +0 -71
  65. package/dist/ollama-G2MYWBEE.js.map +0 -1
  66. package/dist/session-OORM43WG.js +0 -36
  67. package/dist/start-LXX7MA37.js.map +0 -1
  68. /package/dist/{list-4L6TDLAZ.js.map → chat-history-SMI5AWQG.js.map} +0 -0
  69. /package/dist/{chunk-AXEJCZPA.js.map → chunk-MC4NBIIA.js.map} +0 -0
  70. /package/dist/{chunk-LZGGM2YZ.js.map → chunk-RSQGFSCA.js.map} +0 -0
  71. /package/dist/{chunk-E6NF2XSG.js.map → chunk-YJ7BPHUB.js.map} +0 -0
  72. /package/dist/{model-picker-JKB7LVQR.js.map → list-V6A7DKXF.js.map} +0 -0
  73. /package/dist/{session-OORM43WG.js.map → model-picker-IR7QMEWU.js.map} +0 -0
package/README.md CHANGED
@@ -121,14 +121,27 @@ Optional: an advisory **fast judge** (Nimble on Ollama, usually on your Mac over
121
121
 
122
122
  ## Free and local with Ollama
123
123
 
124
+ If Ollama is running, `omnexx` (or `omnexx --ollama`, or the VS Code panel) just works. With no
125
+ provider set up, it:
126
+
127
+ - finds Ollama on `localhost:11434` (or `OLLAMA_HOST`) and lists the models you've pulled;
128
+ - keeps only models that can call tools (`/api/show` capabilities), skips `:cloud` models, and
129
+ ranks coding models first (qwen3-coder, devstral, gpt-oss, qwen3.x, qwen2.5-coder, ...), then
130
+ bigger, then longer context;
131
+ - makes a copy with a usable context window (`omnexx-<model>`, `num_ctx` 32k, or 16k under 16 GB
132
+ of RAM, never more than the model supports), since Ollama's default window is too small;
133
+ - saves it as the chat, planner, worker and cheap model, and says so in one line:
134
+ `using Ollama's qwen3-coder:30b (local, free, tools, 32k context) for chat, planning and work`.
135
+
136
+ No usable model yet? It suggests one for your machine (`ollama pull qwen3-coder`, or
137
+ `qwen2.5-coder:7b` under 32 GB) and offers to pull it: `y` in the terminal, **Pull it now** in
138
+ VS Code. Ollama not running? It says how to install (https://ollama.com/download) or start it.
139
+
124
140
  ```bash
125
- ollama pull qwen2.5-coder # or any model you like
126
- npx omnexx --ollama # finds Ollama on localhost:11434, picks a coding model, opens the chat
141
+ npx omnexx --ollama # set up as above, then open the chat
127
142
  npx omnexx --ollama llama3.3 # or name the model
128
143
  ```
129
144
 
130
- No key and no config: with no provider set up, omnexx also connects a running Ollama by itself.
131
-
132
145
  ## From the plain terminal
133
146
 
134
147
  ```bash
@@ -4,7 +4,7 @@ import {
4
4
  listRunIds,
5
5
  repoMemoryFile,
6
6
  repoRoot
7
- } from "./chunk-LRBHQHO6.js";
7
+ } from "./chunk-GBFAEDOD.js";
8
8
  import {
9
9
  SECTION_IDS,
10
10
  SECTION_INFO,
@@ -13,7 +13,8 @@ import {
13
13
  loadConfig,
14
14
  readSectionModes,
15
15
  sectionsFile
16
- } from "./chunk-UAW5RG53.js";
16
+ } from "./chunk-V5RPCSON.js";
17
+ import "./chunk-FR5DUUJ4.js";
17
18
  import {
18
19
  resolvePaths
19
20
  } from "./chunk-Q3MDYD6K.js";
@@ -117,4 +118,4 @@ export {
117
118
  contextFiles,
118
119
  setSectionMode
119
120
  };
120
- //# sourceMappingURL=chat-context-PTZRLJEV.js.map
121
+ //# sourceMappingURL=chat-context-P4ZON7M4.js.map
@@ -1 +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":[]}
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":[]}
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CHAT_FILE,
4
+ chatTitle,
5
+ listChats,
6
+ readSavedChat,
7
+ transcriptEntries,
8
+ userMessageCount
9
+ } from "./chunk-WXMTUSCS.js";
10
+ import "./chunk-FR5DUUJ4.js";
11
+ import "./chunk-Q3MDYD6K.js";
12
+ import "./chunk-2H7UOFLK.js";
13
+ export {
14
+ CHAT_FILE,
15
+ chatTitle,
16
+ listChats,
17
+ readSavedChat,
18
+ transcriptEntries,
19
+ userMessageCount
20
+ };
21
+ //# sourceMappingURL=chat-history-SMI5AWQG.js.map
@@ -33,8 +33,13 @@ function text(c, key) {
33
33
  if (typeof v !== "string" || !v.trim()) throw new Error(`"${c.type}" needs a "${key}" string`);
34
34
  return v;
35
35
  }
36
+ function rawText(c, key) {
37
+ const v = c[key];
38
+ if (typeof v !== "string") throw new Error(`"${c.type}" needs a "${key}" string`);
39
+ return v;
40
+ }
36
41
  async function serveChat(io, runCli, opts) {
37
- const { Session } = await import("./session-OORM43WG.js");
42
+ const { Session } = await import("./session-2YQYN6VU.js");
38
43
  const session = new Session(io, runCli);
39
44
  const write = (msg) => {
40
45
  println(io.stdout, JSON.stringify(msg));
@@ -72,6 +77,7 @@ async function serveChat(io, runCli, opts) {
72
77
  live: session.live,
73
78
  todos: session.todos,
74
79
  pending: session.pending?.question ?? null,
80
+ pendingLabels: session.pending?.labels ?? null,
75
81
  mode: session.mode,
76
82
  busy: session.busy !== void 0 || session.chatBusy,
77
83
  paused: session.chatPaused,
@@ -89,6 +95,8 @@ async function serveChat(io, runCli, opts) {
89
95
  } : null,
90
96
  run: session.runId ? { id: session.runId, alive: session.runAlive, ...session.info } : null,
91
97
  helpers: session.helpers,
98
+ waiting: session.waiting,
99
+ estimate: session.estimate,
92
100
  browser: session.browserFrame ? {
93
101
  seq: session.browserFrame.seq,
94
102
  url: session.browserFrame.url,
@@ -104,7 +112,7 @@ async function serveChat(io, runCli, opts) {
104
112
  };
105
113
  session.onChange(schedule);
106
114
  const sendModels = async () => {
107
- const { loadModelChoices } = await import("./model-picker-JKB7LVQR.js");
115
+ const { loadModelChoices } = await import("./model-picker-IR7QMEWU.js");
108
116
  const { items, unreachable } = await loadModelChoices(io);
109
117
  write({
110
118
  type: "models",
@@ -114,17 +122,20 @@ async function serveChat(io, runCli, opts) {
114
122
  });
115
123
  };
116
124
  const sendSettings = async () => {
117
- const { providerSettings } = await import("./chat-settings-X7R4JKI7.js");
125
+ const { providerSettings } = await import("./chat-settings-4XUKDH6H.js");
118
126
  write({ type: "settings", ...await providerSettings(io) });
119
127
  };
120
128
  const sendBrowser = async () => {
121
- const { browserSettings } = await import("./chat-settings-X7R4JKI7.js");
129
+ const { browserSettings } = await import("./chat-settings-4XUKDH6H.js");
122
130
  write({ type: "browser", ...await browserSettings(io) });
123
131
  };
124
132
  const sendContextFiles = async () => {
125
- const { contextFiles } = await import("./chat-context-PTZRLJEV.js");
133
+ const { contextFiles } = await import("./chat-context-P4ZON7M4.js");
126
134
  write({ type: "contextFiles", ...await contextFiles(io) });
127
135
  };
136
+ const sendContext = () => {
137
+ write({ type: "contextItems", items: session.contextItems(), busy: session.chatBusy });
138
+ };
128
139
  const sendRoles = async () => {
129
140
  const { readRoles } = await import("./roles-IHKWWYJL.js");
130
141
  const { QuotaLedger } = await import("./quota-ledger-7F6QTZAM.js");
@@ -198,7 +209,7 @@ async function serveChat(io, runCli, opts) {
198
209
  return typeof v === "string" ? v : void 0;
199
210
  };
200
211
  run(
201
- import("./chat-settings-X7R4JKI7.js").then(async ({ addProvider }) => {
212
+ import("./chat-settings-4XUKDH6H.js").then(async ({ addProvider }) => {
202
213
  await providersChanged(
203
214
  await addProvider(io, {
204
215
  provider: str("provider"),
@@ -212,7 +223,7 @@ async function serveChat(io, runCli, opts) {
212
223
  }
213
224
  case "testProvider":
214
225
  run(
215
- import("./chat-settings-X7R4JKI7.js").then(async ({ testProvider }) => {
226
+ import("./chat-settings-4XUKDH6H.js").then(async ({ testProvider }) => {
216
227
  write({ type: "providerTest", ...await testProvider(io, text(c, "name")) });
217
228
  })
218
229
  );
@@ -222,18 +233,51 @@ async function serveChat(io, runCli, opts) {
222
233
  return;
223
234
  case "setSectionMode":
224
235
  run(
225
- import("./chat-context-PTZRLJEV.js").then(async ({ setSectionMode }) => {
236
+ import("./chat-context-P4ZON7M4.js").then(async ({ setSectionMode }) => {
226
237
  await setSectionMode(io, text(c, "id"), text(c, "mode"));
227
238
  await sendContextFiles();
228
239
  })
229
240
  );
230
241
  return;
242
+ case "chats":
243
+ run(
244
+ session.savedChats().then((chats) => {
245
+ write({ type: "chats", chats });
246
+ })
247
+ );
248
+ return;
249
+ case "resumeChat":
250
+ run(
251
+ (async () => {
252
+ clearedAt = session.entries.at(-1)?.id ?? clearedAt;
253
+ sentUpTo = clearedAt;
254
+ await session.resumeChat(text(c, "id"));
255
+ write({ type: "ready", version: opts.version });
256
+ emit();
257
+ })()
258
+ );
259
+ return;
260
+ case "contextList":
261
+ sendContext();
262
+ return;
263
+ case "contextUpdate":
264
+ session.changeContext({ op: "edit", id: text(c, "id"), text: rawText(c, "text") });
265
+ sendContext();
266
+ return;
267
+ case "contextDelete":
268
+ session.changeContext({ op: "delete", id: text(c, "id") });
269
+ sendContext();
270
+ return;
271
+ case "contextReset":
272
+ session.changeContext({ op: "reset" });
273
+ sendContext();
274
+ return;
231
275
  case "browser":
232
276
  run(sendBrowser());
233
277
  return;
234
278
  case "setBrowser":
235
279
  run(
236
- import("./chat-settings-X7R4JKI7.js").then(async ({ setBrowserSettings }) => {
280
+ import("./chat-settings-4XUKDH6H.js").then(async ({ setBrowserSettings }) => {
237
281
  await setBrowserSettings(io, c);
238
282
  session.push("system", "browser settings saved; new runs use them");
239
283
  await sendBrowser();
@@ -254,7 +298,7 @@ async function serveChat(io, runCli, opts) {
254
298
  return;
255
299
  case "removeProvider":
256
300
  run(
257
- import("./chat-settings-X7R4JKI7.js").then(async ({ removeProvider }) => {
301
+ import("./chat-settings-4XUKDH6H.js").then(async ({ removeProvider }) => {
258
302
  await providersChanged(await removeProvider(io, text(c, "name")));
259
303
  })
260
304
  );
@@ -299,4 +343,4 @@ async function serveChat(io, runCli, opts) {
299
343
  export {
300
344
  serveChat
301
345
  };
302
- //# sourceMappingURL=chat-server-HFGPZ7RU.js.map
346
+ //# sourceMappingURL=chat-server-FMF26CBV.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} · contextList ·\n * contextUpdate {id, text} · contextDelete {id} · contextReset · chats · resumeChat {id} · browser ·\n * setBrowser {engine, profile?, sessionName?, headless}.\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} · contextItems {items, busy} · chats {chats} · browser {engine, profile, sessionName, headless} ·\n * 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 /** Button names when the question is an offer (Pull it now / Not now) rather than a guard. */\n pendingLabels: { yes: string; no: string } | null;\n mode: Mode;\n busy: boolean;\n paused: boolean;\n autoApprove: boolean;\n model: string | null;\n /** Tokens in use, the compaction limit, and what they are made of. */\n context: {\n used: number;\n limit: number;\n parts?: { system: number; tools: number; conversation: number };\n } | 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 /** A model request in flight: when it went out and how long it usually takes (null: unknown). */\n waiting: { startedAt: number; estimateMs: number | null } | null;\n /** \"est. ~3 min, ~40k tokens, ~$0.02\" before work, \"~2 min left\" during it, or null. */\n estimate: string | null;\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\n/** A string that may be empty (an edit can clear a block's text). */\nfunction rawText(c: Command, key: string): string {\n const v = c[key];\n if (typeof v !== 'string') 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 pendingLabels: session.pending?.labels ?? 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 waiting: session.waiting,\n estimate: session.estimate,\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 sendBrowser = async (): Promise<void> => {\n const { browserSettings } = await import('./chat-settings.js');\n write({ type: 'browser', ...(await browserSettings(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 /** The context inspector's items: what the next request sends, with token counts. */\n const sendContext = (): void => {\n write({ type: 'contextItems', items: session.contextItems(), busy: session.chatBusy });\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 'chats':\n run(\n session.savedChats().then((chats) => {\n write({ type: 'chats', chats });\n }),\n );\n return;\n case 'resumeChat':\n run(\n (async () => {\n // Mark the panel's start first, so a reload shows only the reopened chat.\n clearedAt = session.entries.at(-1)?.id ?? clearedAt;\n sentUpTo = clearedAt;\n await session.resumeChat(text(c, 'id'));\n write({ type: 'ready', version: opts.version });\n emit();\n })(),\n );\n return;\n case 'contextList':\n sendContext();\n return;\n case 'contextUpdate':\n session.changeContext({ op: 'edit', id: text(c, 'id'), text: rawText(c, 'text') });\n sendContext();\n return;\n case 'contextDelete':\n session.changeContext({ op: 'delete', id: text(c, 'id') });\n sendContext();\n return;\n case 'contextReset':\n session.changeContext({ op: 'reset' });\n sendContext();\n return;\n case 'browser':\n run(sendBrowser());\n return;\n case 'setBrowser':\n run(\n import('./chat-settings.js').then(async ({ setBrowserSettings }) => {\n await setBrowserSettings(io, c);\n session.push('system', 'browser settings saved; new runs use them');\n await sendBrowser();\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":[]}
@@ -1,24 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ setTable
4
+ } from "./chunk-QS6LKKWO.js";
2
5
  import {
3
6
  connect,
4
7
  describeConnect
5
- } from "./chunk-LZGGM2YZ.js";
8
+ } from "./chunk-RSQGFSCA.js";
6
9
  import {
7
10
  PROVIDER_TEMPLATES,
8
11
  removeEndpointBlock
9
- } from "./chunk-AXEJCZPA.js";
10
- import "./chunk-7PC4SSYS.js";
11
- import {
12
- setTable
13
- } from "./chunk-QS6LKKWO.js";
12
+ } from "./chunk-MC4NBIIA.js";
14
13
  import {
15
14
  clearKey,
16
15
  findProviderKey,
17
16
  maskKey
18
- } from "./chunk-YAWRCULP.js";
17
+ } from "./chunk-L2YBK4AY.js";
19
18
  import {
20
19
  loadConfig
21
- } from "./chunk-UAW5RG53.js";
20
+ } from "./chunk-V5RPCSON.js";
21
+ import "./chunk-FR5DUUJ4.js";
22
+ import "./chunk-7PC4SSYS.js";
22
23
  import {
23
24
  resolvePaths,
24
25
  userConfigFile
@@ -227,4 +228,4 @@ export {
227
228
  setBrowserSettings,
228
229
  testProvider
229
230
  };
230
- //# sourceMappingURL=chat-settings-X7R4JKI7.js.map
231
+ //# sourceMappingURL=chat-settings-4XUKDH6H.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/chat-settings.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { parse } from 'smol-toml';\nimport { 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 { setTable, type TomlValue } from '../integrations/toml-edit.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\n/** The browser tool's engine, profile and session, as the settings page shows and saves them. */\nexport interface BrowserSettings {\n engine: 'chromium' | 'camoufox';\n /** A folder (or Chrome profile name) whose logins the agent reuses; null when unset. */\n profile: string | null;\n sessionName: string | null;\n headless: boolean;\n}\n\nconst ENGINES = ['chromium', 'camoufox'] as const;\n// Same rule as browser.session_name in src/config/sections/browser.ts.\nconst SESSION_NAME = /^[\\w.-]{1,64}$/;\n/** C0 and C1 control characters, newlines included. */\nconst hasControl = (text: string): boolean =>\n Array.from(text).some((ch) => {\n const code = ch.codePointAt(0) ?? 0;\n return code < 0x20 || (code >= 0x7f && code <= 0x9f);\n });\n\nexport async function browserSettings(io: CliIO): Promise<BrowserSettings> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const b = config.browser;\n return {\n engine: b.engine,\n profile: b.profile ?? null,\n sessionName: b.session_name ?? null,\n headless: b.headless,\n };\n}\n\n/** A trimmed optional text field: empty is unset; newlines and control characters are refused. */\nfunction optionalText(raw: unknown, label: string): string | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (typeof raw !== 'string') throw new UsageError(`${label} must be text`);\n const trimmed = raw.trim();\n if (hasControl(trimmed))\n throw new UsageError(`${label} can't contain newlines or control characters`);\n return trimmed || undefined;\n}\n\n/** Check what the settings form sent; throws a UsageError naming the bad field. */\nexport function parseBrowserSettings(raw: unknown): BrowserSettings {\n const obj = typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : {};\n const engine = obj.engine ?? 'chromium';\n if (typeof engine !== 'string' || !(ENGINES as readonly string[]).includes(engine))\n throw new UsageError('the browser engine must be chromium or camoufox');\n const profile = optionalText(obj.profile, 'the browser profile');\n const sessionName = optionalText(obj.sessionName, 'the session name');\n if (sessionName && !SESSION_NAME.test(sessionName))\n throw new UsageError(\n `\"${sessionName}\" is not a usable session name`,\n 'up to 64 letters, digits, _ . and -',\n );\n if (obj.headless !== undefined && typeof obj.headless !== 'boolean')\n throw new UsageError('headless must be on or off');\n return {\n engine: engine as BrowserSettings['engine'],\n profile: profile ?? null,\n sessionName: sessionName ?? null,\n headless: obj.headless !== false,\n };\n}\n\nasync function readText(file: string): Promise<string> {\n try {\n return await readFile(file, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return '';\n throw err;\n }\n}\n\n/** The [browser] keys already in the user config, kept when the form saves its four. */\nfunction existingBrowser(text: string): Record<string, TomlValue> {\n const table = (parse(text) as Record<string, unknown>).browser;\n return typeof table === 'object' && table !== null ? (table as Record<string, TomlValue>) : {};\n}\n\n/** Save the browser settings to the user's config.toml; other [browser] keys stay as they were. */\nexport async function setBrowserSettings(io: CliIO, raw: unknown): Promise<BrowserSettings> {\n const next = parseBrowserSettings(raw);\n const file = userConfigFile(resolvePaths(io.env));\n const text = await readText(file);\n const updated = setTable(text, ['browser'], {\n ...existingBrowser(text),\n engine: next.engine,\n profile: next.profile ?? undefined,\n session_name: next.sessionName ?? undefined,\n headless: next.headless,\n });\n await mkdir(dirname(file), { recursive: true });\n await writeFile(file, updated, 'utf8');\n return next;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/cli/chat-settings.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { parse } from 'smol-toml';\nimport { 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 { setTable, type TomlValue } from '../integrations/toml-edit.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\n/** The browser tool's engine, profile and session, as the settings page shows and saves them. */\nexport interface BrowserSettings {\n engine: 'chromium' | 'camoufox';\n /** A folder (or Chrome profile name) whose logins the agent reuses; null when unset. */\n profile: string | null;\n sessionName: string | null;\n headless: boolean;\n}\n\nconst ENGINES = ['chromium', 'camoufox'] as const;\n// Same rule as browser.session_name in src/config/sections/browser.ts.\nconst SESSION_NAME = /^[\\w.-]{1,64}$/;\n/** C0 and C1 control characters, newlines included. */\nconst hasControl = (text: string): boolean =>\n Array.from(text).some((ch) => {\n const code = ch.codePointAt(0) ?? 0;\n return code < 0x20 || (code >= 0x7f && code <= 0x9f);\n });\n\nexport async function browserSettings(io: CliIO): Promise<BrowserSettings> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const b = config.browser;\n return {\n engine: b.engine,\n profile: b.profile ?? null,\n sessionName: b.session_name ?? null,\n headless: b.headless,\n };\n}\n\n/** A trimmed optional text field: empty is unset; newlines and control characters are refused. */\nfunction optionalText(raw: unknown, label: string): string | undefined {\n if (raw === undefined || raw === null) return undefined;\n if (typeof raw !== 'string') throw new UsageError(`${label} must be text`);\n const trimmed = raw.trim();\n if (hasControl(trimmed))\n throw new UsageError(`${label} can't contain newlines or control characters`);\n return trimmed || undefined;\n}\n\n/** Check what the settings form sent; throws a UsageError naming the bad field. */\nexport function parseBrowserSettings(raw: unknown): BrowserSettings {\n const obj = typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : {};\n const engine = obj.engine ?? 'chromium';\n if (typeof engine !== 'string' || !(ENGINES as readonly string[]).includes(engine))\n throw new UsageError('the browser engine must be chromium or camoufox');\n const profile = optionalText(obj.profile, 'the browser profile');\n const sessionName = optionalText(obj.sessionName, 'the session name');\n if (sessionName && !SESSION_NAME.test(sessionName))\n throw new UsageError(\n `\"${sessionName}\" is not a usable session name`,\n 'up to 64 letters, digits, _ . and -',\n );\n if (obj.headless !== undefined && typeof obj.headless !== 'boolean')\n throw new UsageError('headless must be on or off');\n return {\n engine: engine as BrowserSettings['engine'],\n profile: profile ?? null,\n sessionName: sessionName ?? null,\n headless: obj.headless !== false,\n };\n}\n\nasync function readText(file: string): Promise<string> {\n try {\n return await readFile(file, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return '';\n throw err;\n }\n}\n\n/** The [browser] keys already in the user config, kept when the form saves its four. */\nfunction existingBrowser(text: string): Record<string, TomlValue> {\n const table = (parse(text) as Record<string, unknown>).browser;\n return typeof table === 'object' && table !== null ? (table as Record<string, TomlValue>) : {};\n}\n\n/** Save the browser settings to the user's config.toml; other [browser] keys stay as they were. */\nexport async function setBrowserSettings(io: CliIO, raw: unknown): Promise<BrowserSettings> {\n const next = parseBrowserSettings(raw);\n const file = userConfigFile(resolvePaths(io.env));\n const text = await readText(file);\n const updated = setTable(text, ['browser'], {\n ...existingBrowser(text),\n engine: next.engine,\n profile: next.profile ?? undefined,\n session_name: next.sessionName ?? undefined,\n headless: next.headless,\n });\n await mkdir(dirname(file), { recursive: true });\n await writeFile(file, updated, 'utf8');\n return next;\n}\n"],"mappings":"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","names":[]}