omnexx 0.7.0 → 0.9.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 (60) hide show
  1. package/dist/chat-context-37IAD72P.js +120 -0
  2. package/dist/chat-context-37IAD72P.js.map +1 -0
  3. package/dist/{chat-server-X2QSACYD.js → chat-server-RMR3QGFJ.js} +49 -10
  4. package/dist/chat-server-RMR3QGFJ.js.map +1 -0
  5. package/dist/{chat-settings-DZCYNQF5.js → chat-settings-AMSAWVT6.js} +12 -9
  6. package/dist/{chat-settings-DZCYNQF5.js.map → chat-settings-AMSAWVT6.js.map} +1 -1
  7. package/dist/chunk-27GOEUTN.js +97 -0
  8. package/dist/chunk-27GOEUTN.js.map +1 -0
  9. package/dist/{chunk-FC5LAF6U.js → chunk-3S4ULH6B.js} +10 -8
  10. package/dist/{chunk-FC5LAF6U.js.map → chunk-3S4ULH6B.js.map} +1 -1
  11. package/dist/chunk-DGKPM32R.js +60 -0
  12. package/dist/chunk-DGKPM32R.js.map +1 -0
  13. package/dist/{chunk-M23TQTS6.js → chunk-EDB7FXDP.js} +9 -7
  14. package/dist/{chunk-M23TQTS6.js.map → chunk-EDB7FXDP.js.map} +1 -1
  15. package/dist/{chunk-ZA7YDO2B.js → chunk-FNHEOL3X.js} +30 -20
  16. package/dist/chunk-FNHEOL3X.js.map +1 -0
  17. package/dist/{chunk-2TKRLH7N.js → chunk-IXTUJETQ.js} +190 -116
  18. package/dist/chunk-IXTUJETQ.js.map +1 -0
  19. package/dist/{chunk-NPXKRK6O.js → chunk-IZYY3NPJ.js} +948 -1432
  20. package/dist/chunk-IZYY3NPJ.js.map +1 -0
  21. package/dist/{chunk-ZCBVW3DZ.js → chunk-LU3LFPGQ.js} +48 -19
  22. package/dist/chunk-LU3LFPGQ.js.map +1 -0
  23. package/dist/chunk-NV4LO7BY.js +724 -0
  24. package/dist/chunk-NV4LO7BY.js.map +1 -0
  25. package/dist/chunk-NVDO3QO5.js +1055 -0
  26. package/dist/chunk-NVDO3QO5.js.map +1 -0
  27. package/dist/{chunk-JHJW7UQV.js → chunk-STU6YIQJ.js} +9 -5
  28. package/dist/{chunk-JHJW7UQV.js.map → chunk-STU6YIQJ.js.map} +1 -1
  29. package/dist/chunk-T2NUPPUG.js +65 -0
  30. package/dist/chunk-T2NUPPUG.js.map +1 -0
  31. package/dist/chunk-TJWLF5MU.js +674 -0
  32. package/dist/chunk-TJWLF5MU.js.map +1 -0
  33. package/dist/{chunk-7PNGHZYH.js → chunk-XQTM3SWX.js} +23 -1
  34. package/dist/chunk-XQTM3SWX.js.map +1 -0
  35. package/dist/cli.js +85 -37
  36. package/dist/cli.js.map +1 -1
  37. package/dist/{model-picker-RVX6A2E3.js → model-picker-QX6NHUGJ.js} +8 -6
  38. package/dist/{ollama-ELPMF6WY.js → ollama-USDNXHTB.js} +7 -5
  39. package/dist/{ollama-ELPMF6WY.js.map → ollama-USDNXHTB.js.map} +1 -1
  40. package/dist/{print-KGSMCQ2Y.js → print-K4HFWHCL.js} +9 -5
  41. package/dist/{print-KGSMCQ2Y.js.map → print-K4HFWHCL.js.map} +1 -1
  42. package/dist/quota-ledger-7F6QTZAM.js +11 -0
  43. package/dist/roles-IHKWWYJL.js +19 -0
  44. package/dist/roles-IHKWWYJL.js.map +1 -0
  45. package/dist/session-QGVKS2YJ.js +32 -0
  46. package/dist/session-QGVKS2YJ.js.map +1 -0
  47. package/dist/{start-LAVPSRH2.js → start-NBU4UITZ.js} +15 -10
  48. package/dist/{start-LAVPSRH2.js.map → start-NBU4UITZ.js.map} +1 -1
  49. package/package.json +3 -2
  50. package/dist/chat-server-X2QSACYD.js.map +0 -1
  51. package/dist/chunk-2TKRLH7N.js.map +0 -1
  52. package/dist/chunk-7PNGHZYH.js.map +0 -1
  53. package/dist/chunk-NPXKRK6O.js.map +0 -1
  54. package/dist/chunk-ZA7YDO2B.js.map +0 -1
  55. package/dist/chunk-ZCBVW3DZ.js.map +0 -1
  56. package/dist/chunk-ZGB5IZJR.js +0 -1501
  57. package/dist/chunk-ZGB5IZJR.js.map +0 -1
  58. package/dist/session-OKHTHELI.js +0 -27
  59. /package/dist/{model-picker-RVX6A2E3.js.map → model-picker-QX6NHUGJ.js.map} +0 -0
  60. /package/dist/{session-OKHTHELI.js.map → quota-ledger-7F6QTZAM.js.map} +0 -0
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ repoRoot
4
+ } from "./chunk-T2NUPPUG.js";
5
+ import {
6
+ RunStore,
7
+ listRunIds,
8
+ repoMemoryFile
9
+ } from "./chunk-NV4LO7BY.js";
10
+ import {
11
+ SECTION_IDS,
12
+ SECTION_INFO,
13
+ SECTION_MODES,
14
+ estimateTokens,
15
+ loadConfig,
16
+ readSectionModes,
17
+ sectionsFile
18
+ } from "./chunk-NVDO3QO5.js";
19
+ import {
20
+ UsageError,
21
+ resolvePaths
22
+ } from "./chunk-XQTM3SWX.js";
23
+ import "./chunk-27GOEUTN.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-37IAD72P.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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,MAAM,iBAAiB;AACjD,SAAS,SAAS,YAAY;AAsC9B,eAAsB,eAAe,IAAW,IAAY,MAA6B;AACvF,MAAI,CAAE,YAAkC,SAAS,EAAE,EAAG,OAAM,IAAI,WAAW,eAAe,EAAE,GAAG;AAC/F,MAAI,CAAE,cAAoC,SAAS,IAAI;AACrD,UAAM,IAAI,WAAW,kBAAkB,cAAc,KAAK,IAAI,CAAC,EAAE;AACnE,QAAM,IAAI,aAAa,GAAG,GAAG;AAC7B,QAAM,QAAQ,EAAE,GAAI,MAAM,iBAAiB,CAAC,GAAI,CAAC,EAAE,GAAG,KAAK;AAC3D,QAAM,MAAM,QAAQ,aAAa,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACzD,QAAM,UAAU,aAAa,CAAC,GAAG,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,CAAI;AACxE;AAEA,eAAe,OAAO,MAA4D;AAChF,MAAI;AACF,QAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,EAAG,QAAO,EAAE,QAAQ,OAAO,QAAQ,EAAE;AACpE,WAAO,EAAE,QAAQ,MAAM,QAAQ,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,EAAE;AAAA,EAC9E,QAAQ;AACN,WAAO,EAAE,QAAQ,OAAO,QAAQ,EAAE;AAAA,EACpC;AACF;AAGA,eAAe,UAAU,OAAoB,MAA6C;AACxF,aAAW,MAAM,CAAC,GAAI,MAAM,WAAW,KAAK,CAAE,EAAE,QAAQ,GAAG;AACzD,UAAM,QAAQ,IAAI,SAAS,OAAO,EAAE;AACpC,UAAM,IAAI,MAAM,MAAM,UAAU,EAAE,MAAM,MAAM,MAAS;AACvD,QAAI,GAAG,aAAa,KAAM,QAAO;AAAA,EACnC;AACA,SAAO;AACT;AAEA,eAAsB,aACpB,IACyD;AACzD,QAAM,QAAQ,aAAa,GAAG,GAAG;AACjC,QAAM,OAAO,MAAM,SAAS,GAAG,GAAG,EAAE,MAAM,MAAM,GAAG,GAAG;AACtD,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,EAAE,KAAK,GAAG,KAAK,KAAK,GAAG,IAAI,CAAC;AAChE,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,MAAM,MAAM,UAAU,OAAO,IAAI;AACvC,QAAM,QAAQ,MAAM,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,MAAS,IAAI;AACnE,QAAM,YAAY,QAAQ,MAAM,eAAe,EAAE,MAAM,MAAM,KAAK,GAAG,IAAI,CAAC;AAE1E,QAAM,UAAoD,CAAC;AAC3D,QAAM,UAAU,CAAC,IAAe,MAAc,aAA4B;AACxE,UAAM,OAAO,aAAa,EAAE;AAC5B,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT,MAAM,MAAM,EAAE;AAAA,IAChB,CAAC;AAAA,EACH;AACA,MAAI,KAAK;AACP,QAAI,OAAO,OAAQ,SAAQ,QAAQ,IAAI,KAAK,KAAK,SAAS,GAAG,MAAM,MAAM,KAAK,CAAC,GAAG,IAAI;AACtF,YAAQ,WAAW,IAAI,KAAK,YAAY,GAAG,KAAK;AAChD,YAAQ,UAAU,IAAI,KAAK,WAAW,GAAG,IAAI;AAC7C,YAAQ,WAAW,IAAI,KAAK,UAAU,GAAG,KAAK;AAC9C,YAAQ,YAAY,IAAI,KAAK,aAAa,GAAG,KAAK;AAClD,YAAQ,KAAK;AAAA,MACX,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,MAAM,IAAI,KAAK,SAAS;AAAA,MACxB,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACA,UAAQ;AAAA,IACN;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,MAAM,KAAK,MAAM,WAAW,SAAS;AAAA,MACrC,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,MAAM,KAAK,WAAW,UAAU,YAAY;AAAA,MAC5C,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,MAAM,KAAK,WAAW,UAAU,SAAS;AAAA,MACzC,UAAU;AAAA,IACZ;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAO,OAAO,EAAE,GAAG,GAAG,GAAI,MAAM,OAAO,EAAE,IAAI,EAAG,EAAE,CAAC;AAC/F,SAAO,EAAE,OAAO,KAAK,SAAS,MAAM,MAAM;AAC5C;","names":[]}
@@ -3,11 +3,13 @@ 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";
11
13
  import { readFile } from "fs/promises";
12
14
  import { createInterface } from "readline";
13
15
  var MAX_FRAME_BYTES = 4 * 1024 * 1024;
@@ -30,7 +32,7 @@ function text(c, key) {
30
32
  return v;
31
33
  }
32
34
  async function serveChat(io, runCli, opts) {
33
- const { Session } = await import("./session-OKHTHELI.js");
35
+ const { Session } = await import("./session-QGVKS2YJ.js");
34
36
  const session = new Session(io, runCli);
35
37
  const write = (msg) => {
36
38
  println(io.stdout, JSON.stringify(msg));
@@ -100,7 +102,7 @@ async function serveChat(io, runCli, opts) {
100
102
  };
101
103
  session.onChange(schedule);
102
104
  const sendModels = async () => {
103
- const { loadModelChoices } = await import("./model-picker-RVX6A2E3.js");
105
+ const { loadModelChoices } = await import("./model-picker-QX6NHUGJ.js");
104
106
  const { items, unreachable } = await loadModelChoices(io);
105
107
  write({
106
108
  type: "models",
@@ -110,9 +112,23 @@ async function serveChat(io, runCli, opts) {
110
112
  });
111
113
  };
112
114
  const sendSettings = async () => {
113
- const { providerSettings } = await import("./chat-settings-DZCYNQF5.js");
115
+ const { providerSettings } = await import("./chat-settings-AMSAWVT6.js");
114
116
  write({ type: "settings", ...await providerSettings(io) });
115
117
  };
118
+ const sendContextFiles = async () => {
119
+ const { contextFiles } = await import("./chat-context-37IAD72P.js");
120
+ write({ type: "contextFiles", ...await contextFiles(io) });
121
+ };
122
+ const sendRoles = async () => {
123
+ const { readRoles } = await import("./roles-IHKWWYJL.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
+ };
116
132
  const providersChanged = async (message) => {
117
133
  session.push("system", message);
118
134
  await sendSettings();
@@ -159,7 +175,7 @@ async function serveChat(io, runCli, opts) {
159
175
  return;
160
176
  }
161
177
  case "yolo":
162
- if (c.on === true !== session.autoApprove) run(session.submit("/yolo"));
178
+ if (c.on === true !== session.autoApprove) run(session.command("yolo"));
163
179
  return;
164
180
  case "model":
165
181
  run(session.switchModel(text(c, "ref")));
@@ -176,7 +192,7 @@ async function serveChat(io, runCli, opts) {
176
192
  return typeof v === "string" ? v : void 0;
177
193
  };
178
194
  run(
179
- import("./chat-settings-DZCYNQF5.js").then(async ({ addProvider }) => {
195
+ import("./chat-settings-AMSAWVT6.js").then(async ({ addProvider }) => {
180
196
  await providersChanged(
181
197
  await addProvider(io, {
182
198
  provider: str("provider"),
@@ -190,14 +206,37 @@ async function serveChat(io, runCli, opts) {
190
206
  }
191
207
  case "testProvider":
192
208
  run(
193
- import("./chat-settings-DZCYNQF5.js").then(async ({ testProvider }) => {
209
+ import("./chat-settings-AMSAWVT6.js").then(async ({ testProvider }) => {
194
210
  write({ type: "providerTest", ...await testProvider(io, text(c, "name")) });
195
211
  })
196
212
  );
197
213
  return;
214
+ case "contextFiles":
215
+ run(sendContextFiles());
216
+ return;
217
+ case "setSectionMode":
218
+ run(
219
+ import("./chat-context-37IAD72P.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-IHKWWYJL.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;
198
237
  case "removeProvider":
199
238
  run(
200
- import("./chat-settings-DZCYNQF5.js").then(async ({ removeProvider }) => {
239
+ import("./chat-settings-AMSAWVT6.js").then(async ({ removeProvider }) => {
201
240
  await providersChanged(await removeProvider(io, text(c, "name")));
202
241
  })
203
242
  );
@@ -242,4 +281,4 @@ async function serveChat(io, runCli, opts) {
242
281
  export {
243
282
  serveChat
244
283
  };
245
- //# sourceMappingURL=chat-server-X2QSACYD.js.map
284
+ //# sourceMappingURL=chat-server-RMR3QGFJ.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 /** 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 /** 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,26 @@
2
2
  import {
3
3
  connect,
4
4
  describeConnect
5
- } from "./chunk-FC5LAF6U.js";
5
+ } from "./chunk-3S4ULH6B.js";
6
6
  import {
7
7
  PROVIDER_TEMPLATES,
8
8
  removeEndpointBlock
9
- } from "./chunk-M23TQTS6.js";
9
+ } from "./chunk-EDB7FXDP.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-NVDO3QO5.js";
19
+ import {
20
+ UsageError,
16
21
  resolvePaths,
17
22
  userConfigFile
18
- } from "./chunk-ZGB5IZJR.js";
19
- import {
20
- UsageError
21
- } from "./chunk-7PNGHZYH.js";
23
+ } from "./chunk-XQTM3SWX.js";
24
+ import "./chunk-27GOEUTN.js";
22
25
  import "./chunk-UX4MF6V4.js";
23
26
  import "./chunk-2H7UOFLK.js";
24
27
 
@@ -141,4 +144,4 @@ export {
141
144
  removeProvider,
142
145
  testProvider
143
146
  };
144
- //# sourceMappingURL=chat-settings-DZCYNQF5.js.map
147
+ //# sourceMappingURL=chat-settings-AMSAWVT6.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\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":[]}
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":[]}
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config/routing.ts
4
+ var STRATEGIES = ["priority", "random", "least-busy", "smart"];
5
+ function orderForStrategy(chain, strategy, ctx = {}) {
6
+ const indexed = chain.map((m, i) => ({ m, i }));
7
+ switch (strategy) {
8
+ case "random": {
9
+ const rnd = ctx.random ?? Math.random;
10
+ const keys = new Map(indexed.map((x) => [x.i, rnd()]));
11
+ return indexed.sort((a, b) => (keys.get(a.i) ?? 0) - (keys.get(b.i) ?? 0)).map((x) => x.m);
12
+ }
13
+ case "least-busy": {
14
+ const busy = ctx.busy ?? /* @__PURE__ */ new Map();
15
+ return indexed.sort((a, b) => (busy.get(a.m.provider) ?? 0) - (busy.get(b.m.provider) ?? 0) || a.i - b.i).map((x) => x.m);
16
+ }
17
+ case "smart":
18
+ return indexed.sort((a, b) => (b.m.price?.output ?? 0) - (a.m.price?.output ?? 0) || a.i - b.i).map((x) => x.m);
19
+ default:
20
+ return [...chain];
21
+ }
22
+ }
23
+ function strategyFor(models, role) {
24
+ const set = models.routing?.[role];
25
+ if (set) return set;
26
+ return role === "helpers" && models.spread_helpers !== false ? "least-busy" : "priority";
27
+ }
28
+
29
+ // src/config/roles.ts
30
+ import { mkdir, readFile, writeFile } from "fs/promises";
31
+ import { dirname, join } from "path";
32
+ var ROLE_NAMES = ["chat", "planner", "worker", "cheap", "helpers"];
33
+ var REF = /^[a-z][a-z0-9_-]*:\S+$/;
34
+ var MAX_PER_ROLE = 20;
35
+ var rolesFile = (p) => join(p.configHome, "roles.json");
36
+ function cleanList(value) {
37
+ if (!Array.isArray(value)) return [];
38
+ const refs = value.filter((r) => typeof r === "string" && REF.test(r));
39
+ return [...new Set(refs)].slice(0, MAX_PER_ROLE);
40
+ }
41
+ function parseRoles(raw) {
42
+ const obj = typeof raw === "object" && raw !== null ? raw : {};
43
+ const modelsIn = typeof obj.models === "object" && obj.models !== null ? obj.models : {};
44
+ const models = {};
45
+ for (const role of ROLE_NAMES) {
46
+ const list = cleanList(modelsIn[role]);
47
+ if (list.length) models[role] = list;
48
+ }
49
+ const strategiesIn = typeof obj.strategies === "object" && obj.strategies !== null ? obj.strategies : {};
50
+ const strategies = {};
51
+ for (const role of ROLE_NAMES) {
52
+ const s = strategiesIn[role];
53
+ if (typeof s === "string" && STRATEGIES.includes(s))
54
+ strategies[role] = s;
55
+ }
56
+ return { models, strategies, spreadHelpers: obj.spreadHelpers !== false };
57
+ }
58
+ async function readRoles(p) {
59
+ try {
60
+ return parseRoles(JSON.parse(await readFile(rolesFile(p), "utf8")));
61
+ } catch {
62
+ return { models: {}, strategies: {}, spreadHelpers: true };
63
+ }
64
+ }
65
+ async function writeRoles(p, roles) {
66
+ const clean = parseRoles(roles);
67
+ const file = rolesFile(p);
68
+ await mkdir(dirname(file), { recursive: true });
69
+ await writeFile(file, `${JSON.stringify(clean, null, 2)}
70
+ `);
71
+ return clean;
72
+ }
73
+ function rolesLayer(r) {
74
+ const models = {};
75
+ const { chat, planner, worker, cheap, helpers } = r.models;
76
+ if (chat?.[0]) models.chat = chat[0];
77
+ if (planner?.length) models.planner = planner;
78
+ if (worker?.length) models.worker = worker;
79
+ if (cheap?.length) models.cheap = cheap;
80
+ if (helpers?.length) models.helpers = helpers;
81
+ if (!r.spreadHelpers) models.spread_helpers = false;
82
+ if (Object.keys(r.strategies).length) models.routing = r.strategies;
83
+ return Object.keys(models).length ? { models } : void 0;
84
+ }
85
+
86
+ export {
87
+ STRATEGIES,
88
+ orderForStrategy,
89
+ strategyFor,
90
+ ROLE_NAMES,
91
+ rolesFile,
92
+ parseRoles,
93
+ readRoles,
94
+ writeRoles,
95
+ rolesLayer
96
+ };
97
+ //# sourceMappingURL=chunk-27GOEUTN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/routing.ts","../src/config/roles.ts"],"sourcesContent":["/**\n * How a role's models are ordered before each use. The first model is tried first; the rest are\n * the failover chain, so every strategy keeps every model as a fallback.\n * - priority: as the person arranged them.\n * - random: a fresh shuffle each time, spreading load evenly.\n * - least-busy: providers with the fewest helpers running go first (parallel helpers spread out).\n * - smart: the most capable first, with output price as the stand-in for capability.\n */\nexport const STRATEGIES = ['priority', 'random', 'least-busy', 'smart'] as const;\nexport type Strategy = (typeof STRATEGIES)[number];\n\ninterface Routable {\n provider: string;\n price?: { output?: number } | undefined;\n}\n\nexport interface OrderContext {\n /** Helpers running per provider right now (least-busy). */\n busy?: ReadonlyMap<string, number>;\n /** Source of randomness (tests pass a fixed one). */\n random?: () => number;\n}\n\nexport function orderForStrategy<T extends Routable>(\n chain: readonly T[],\n strategy: Strategy,\n ctx: OrderContext = {},\n): T[] {\n const indexed = chain.map((m, i) => ({ m, i }));\n switch (strategy) {\n case 'random': {\n const rnd = ctx.random ?? Math.random;\n const keys = new Map(indexed.map((x) => [x.i, rnd()]));\n return indexed.sort((a, b) => (keys.get(a.i) ?? 0) - (keys.get(b.i) ?? 0)).map((x) => x.m);\n }\n case 'least-busy': {\n const busy = ctx.busy ?? new Map<string, number>();\n return indexed\n .sort((a, b) => (busy.get(a.m.provider) ?? 0) - (busy.get(b.m.provider) ?? 0) || a.i - b.i)\n .map((x) => x.m);\n }\n case 'smart':\n return indexed\n .sort((a, b) => (b.m.price?.output ?? 0) - (a.m.price?.output ?? 0) || a.i - b.i)\n .map((x) => x.m);\n default:\n return [...chain];\n }\n}\n\n/** The strategy a role uses: its setting, else least-busy for helpers (when spreading) and priority. */\nexport function strategyFor(\n models: { routing?: Partial<Record<string, Strategy>>; spread_helpers?: boolean },\n role: string,\n): Strategy {\n const set = models.routing?.[role];\n if (set) return set;\n return role === 'helpers' && models.spread_helpers !== false ? 'least-busy' : 'priority';\n}\n","import { STRATEGIES, type Strategy } from './routing.js';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type { OmnexxPaths } from '../core/paths.js';\n\n/**\n * Which models power which role, chosen in the editor's settings and kept next to the /models\n * pool (`roles.json`). A role left empty falls back to the pool and config.toml as before.\n * `helpers` powers subagents. Each role also has a routing strategy (see routing.ts): least-busy\n * helpers spread over providers, so several providers' quotas are used at once.\n */\nexport const ROLE_NAMES = ['chat', 'planner', 'worker', 'cheap', 'helpers'] as const;\nexport type Role = (typeof ROLE_NAMES)[number];\n\nexport interface Roles {\n models: Partial<Record<Role, string[]>>;\n /** How each role routes between its models; unset roles use the default. */\n strategies: Partial<Record<Role, Strategy>>;\n spreadHelpers: boolean;\n}\n\nconst REF = /^[a-z][a-z0-9_-]*:\\S+$/;\nconst MAX_PER_ROLE = 20;\n\nexport const rolesFile = (p: OmnexxPaths): string => join(p.configHome, 'roles.json');\n\n/** Keep only well-formed, distinct refs, in order. */\nfunction cleanList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const refs = value.filter((r): r is string => typeof r === 'string' && REF.test(r));\n return [...new Set(refs)].slice(0, MAX_PER_ROLE);\n}\n\nexport function parseRoles(raw: unknown): Roles {\n const obj = typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : {};\n const modelsIn =\n typeof obj.models === 'object' && obj.models !== null\n ? (obj.models as Record<string, unknown>)\n : {};\n const models: Partial<Record<Role, string[]>> = {};\n for (const role of ROLE_NAMES) {\n const list = cleanList(modelsIn[role]);\n if (list.length) models[role] = list;\n }\n const strategiesIn =\n typeof obj.strategies === 'object' && obj.strategies !== null\n ? (obj.strategies as Record<string, unknown>)\n : {};\n const strategies: Partial<Record<Role, Strategy>> = {};\n for (const role of ROLE_NAMES) {\n const s = strategiesIn[role];\n if (typeof s === 'string' && (STRATEGIES as readonly string[]).includes(s))\n strategies[role] = s as Strategy;\n }\n return { models, strategies, spreadHelpers: obj.spreadHelpers !== false };\n}\n\nexport async function readRoles(p: OmnexxPaths): Promise<Roles> {\n try {\n return parseRoles(JSON.parse(await readFile(rolesFile(p), 'utf8')));\n } catch {\n return { models: {}, strategies: {}, spreadHelpers: true };\n }\n}\n\nexport async function writeRoles(p: OmnexxPaths, roles: Roles): Promise<Roles> {\n const clean = parseRoles(roles);\n const file = rolesFile(p);\n await mkdir(dirname(file), { recursive: true });\n await writeFile(file, `${JSON.stringify(clean, null, 2)}\\n`);\n return clean;\n}\n\n/** The roles as a config layer: only the roles that were set, so the rest fall through. */\nexport function rolesLayer(r: Roles): Record<string, unknown> | undefined {\n const models: Record<string, unknown> = {};\n const { chat, planner, worker, cheap, helpers } = r.models;\n if (chat?.[0]) models.chat = chat[0];\n if (planner?.length) models.planner = planner;\n if (worker?.length) models.worker = worker;\n if (cheap?.length) models.cheap = cheap;\n if (helpers?.length) models.helpers = helpers;\n if (!r.spreadHelpers) models.spread_helpers = false;\n if (Object.keys(r.strategies).length) models.routing = r.strategies;\n return Object.keys(models).length ? { models } : undefined;\n}\n"],"mappings":"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","names":[]}
@@ -2,17 +2,19 @@
2
2
  import {
3
3
  PROVIDER_TEMPLATES,
4
4
  appendEndpointBlock
5
- } from "./chunk-M23TQTS6.js";
5
+ } from "./chunk-EDB7FXDP.js";
6
6
  import {
7
7
  findProviderKey,
8
- loadConfig,
8
+ storeKey
9
+ } from "./chunk-TJWLF5MU.js";
10
+ import {
11
+ loadConfig
12
+ } from "./chunk-NVDO3QO5.js";
13
+ import {
14
+ UsageError,
9
15
  resolvePaths,
10
- storeKey,
11
16
  userConfigFile
12
- } from "./chunk-ZGB5IZJR.js";
13
- import {
14
- UsageError
15
- } from "./chunk-7PNGHZYH.js";
17
+ } from "./chunk-XQTM3SWX.js";
16
18
  import {
17
19
  discoverModels
18
20
  } from "./chunk-UX4MF6V4.js";
@@ -181,4 +183,4 @@ export {
181
183
  envKeys,
182
184
  connectFromEnv
183
185
  };
184
- //# sourceMappingURL=chunk-FC5LAF6U.js.map
186
+ //# sourceMappingURL=chunk-3S4ULH6B.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli/connect.ts"],"sourcesContent":["import { storeKey, findProviderKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport {\n appendEndpointBlock,\n PROVIDER_TEMPLATES,\n type ProviderTemplate,\n} from './commands/providers.js';\nimport type { CliIO } from './io.js';\n\n/** Key prefixes that name their provider, from the catalog; longest first so sk-or- beats sk-. */\nconst KEY_PREFIXES: readonly (readonly [string, string])[] = (\n [\n ['sk-ant-', 'anthropic'],\n ...Object.entries(PROVIDER_TEMPLATES).flatMap(([name, t]): [string, string][] =>\n t.keyPrefix ? [[t.keyPrefix, name]] : [],\n ),\n ] as [string, string][]\n).sort((a, b) => b[0].length - a[0].length);\n\n/** The provider a pasted key belongs to, or undefined when it doesn't look like a key. */\nexport function providerForKey(text: string): string | undefined {\n const t = text.trim();\n if (/\\s/.test(t) || t.length < 20) return undefined;\n return KEY_PREFIXES.find(([p]) => t.startsWith(p))?.[1];\n}\n\n/** A token long and random enough to be a secret: 24+ chars mixing letters and digits. */\nconst SECRETISH = /^[A-Za-z0-9][A-Za-z0-9_.-]{23,}$/;\n\nexport interface FoundKey {\n key: string;\n /** Undefined when the key has no telltale prefix and the text names no provider. */\n provider: string | undefined;\n}\n\n/**\n * An API key inside a message (\"here's my mistral key abc123…\"): a token with a known prefix, or\n * a secret-looking token next to a provider name the catalog knows. Detection is local; the key is\n * never sent to a model.\n */\nexport function findKeyInText(text: string): FoundKey | undefined {\n const tokens = text.split(/[\\s\"'`,;()<>]+/).map((t) => t.replace(/[.:]+$/, ''));\n for (const t of tokens) {\n const provider = providerForKey(t);\n if (provider) return { key: t, provider };\n }\n const key = tokens.find(\n (t) => SECRETISH.test(t) && /\\d/.test(t) && /[A-Za-z]/.test(t) && !/^https?/i.test(t),\n );\n if (!key) return undefined;\n const words = text.toLowerCase();\n const named = Object.entries(PROVIDER_TEMPLATES).find(\n ([name, t]) =>\n name !== 'custom' &&\n !t.local &&\n (new RegExp(`\\\\b${name}\\\\b`).test(words) ||\n (t.label !== undefined && words.includes(t.label.toLowerCase()))),\n );\n if (named) return { key, provider: named[0] };\n return /\\b(api[ _-]?key|key|token|secret)\\b/.test(words)\n ? { key, provider: undefined }\n : undefined;\n}\n\nexport function looksLikeKey(text: string): boolean {\n return providerForKey(text) !== undefined;\n}\n\n/** A name for an endpoint URL: a matching template, else the host's distinctive label. */\nexport function nameForUrl(url: string): string {\n const u = new URL(url);\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (name === 'custom') continue;\n const tu = new URL(t.baseUrl);\n if (tu.host === u.host) return name;\n }\n if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return 'local';\n const labels = u.hostname.split('.').filter((l) => !['api', 'www', 'inference'].includes(l));\n const label = labels.length > 1 ? labels[labels.length - 2] : labels[0];\n return (label ?? 'custom').toLowerCase().replace(/[^a-z0-9_-]/g, '-');\n}\n\nexport interface ConnectResult {\n name: string;\n added: boolean;\n keyStored: boolean;\n models: string[];\n /** A ready-to-use model ref, e.g. \"openrouter:deepseek/deepseek-v4\". */\n suggested: string | undefined;\n notes: string[];\n}\n\n/**\n * One step from nothing to a usable provider. `target` is a provider name (\"openrouter\"), a base\n * URL (\"http://localhost:11434/v1\"), or a pasted API key. The key goes to the 0600 credentials\n * file, the endpoint to user config, and the endpoint's models are listed to prove it works.\n */\nexport async function connect(\n io: CliIO,\n target: string,\n key?: string,\n opts: { name?: string } = {},\n): Promise<ConnectResult> {\n let t = target.trim();\n let k = key?.trim() ? key.trim() : undefined;\n if (!t) throw new UsageError('nothing to connect', 'try `connect openrouter <key>` or a URL');\n const keyProvider = providerForKey(t);\n if (keyProvider && !k) {\n k = t;\n t = keyProvider;\n }\n let name: string;\n let template: ProviderTemplate | undefined;\n if (/^https?:\\/\\//i.test(t)) {\n name = opts.name ?? nameForUrl(t);\n const local = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)/i.test(t);\n template = { baseUrl: t, keyEnv: undefined, free: local && !k, kind: 'openai' };\n } else {\n name = (opts.name ?? t).toLowerCase();\n template = PROVIDER_TEMPLATES[name];\n }\n if (!/^[a-z][a-z0-9_-]*$/.test(name))\n throw new UsageError(`bad provider name \"${name}\"`, 'use lowercase letters, digits, _ or -');\n\n const paths = resolvePaths(io.env);\n const notes: string[] = [];\n let keyStored = false;\n if (k) {\n await storeKey(paths, name, k);\n keyStored = true;\n }\n if (name === 'anthropic') {\n const found = await findProviderKey(paths, io.env, 'anthropic');\n if (!found) notes.push('no Anthropic key yet: paste one (sk-ant-…)');\n return {\n name,\n added: false,\n keyStored,\n models: ['opus', 'sonnet', 'haiku'],\n suggested: found ? 'anthropic:sonnet' : undefined,\n notes,\n };\n }\n\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const existing = config.providers.endpoints[name];\n let added = false;\n let baseUrl = existing?.base_url;\n if (!existing) {\n if (!template)\n throw new UsageError(\n `unknown provider \"${name}\"`,\n `known: ${Object.keys(PROVIDER_TEMPLATES).join(', ')}; or connect a base URL`,\n );\n // Keys live in the credentials file, so the endpoint doesn't need an env var.\n await appendEndpointBlock(userConfigFile(paths), name, { ...template, keyEnv: undefined });\n baseUrl = template.baseUrl;\n added = true;\n }\n const apiKey = (await findProviderKey(paths, io.env, name, existing?.api_key_env))?.key;\n if (!apiKey && !(existing?.free ?? template?.free)) notes.push(`no key for ${name} yet`);\n const local = /localhost|127\\.0\\.0\\.1/.test(baseUrl ?? '');\n const models = baseUrl\n ? (\n await discoverModels(\n { baseUrl, ...(apiKey ? { apiKey } : {}) },\n io.fetch ? { fetch: io.fetch } : {},\n )\n ).map((m) => m.id)\n : [];\n if (!models.length)\n notes.push(\n local\n ? `nothing answered at ${baseUrl}; is the server running?`\n : `couldn't list models at ${baseUrl}; check the key`,\n );\n const first = models[0];\n return {\n name,\n added,\n keyStored,\n models,\n suggested: first ? `${name}:${first}` : undefined,\n notes,\n };\n}\n\nexport function describeConnect(r: ConnectResult): string {\n const lines = [\n `${r.added ? 'added' : 'updated'} ${r.name}${r.keyStored ? ' (key saved, 0600)' : ''}`,\n ];\n if (r.models.length) {\n const shown = r.models.slice(0, 8).join(', ');\n lines.push(\n `${r.models.length} model${r.models.length === 1 ? '' : 's'}: ${shown}${r.models.length > 8 ? ', …' : ''}`,\n );\n }\n for (const n of r.notes) lines.push(n);\n if (r.suggested) lines.push(`chat with it: /model ${r.suggested}`);\n return lines.join('\\n');\n}\n\nexport interface EnvKey {\n name: string;\n label: string;\n keyEnv: string;\n}\n\n/**\n * Providers whose standard key variable is already set in the environment but that aren't\n * configured yet: the fastest way to a working model on a machine that already has keys.\n */\nexport async function envKeys(io: CliIO): Promise<EnvKey[]> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const out: EnvKey[] = [];\n if (io.env.ANTHROPIC_API_KEY?.trim())\n out.push({ name: 'anthropic', label: 'Anthropic', keyEnv: 'ANTHROPIC_API_KEY' });\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (!t.keyEnv || config.providers.endpoints[name]) continue;\n if (io.env[t.keyEnv]?.trim()) out.push({ name, label: t.label ?? name, keyEnv: t.keyEnv });\n }\n return out;\n}\n\n/**\n * `/connect env`: add every provider found by envKeys(), pointing each endpoint at its env var\n * (the key is never copied into omnexx's files). Anthropic needs nothing: its key is read from\n * the environment already.\n */\nexport async function connectFromEnv(io: CliIO): Promise<EnvKey[]> {\n const found = await envKeys(io);\n const file = userConfigFile(resolvePaths(io.env));\n // One after another: every block is appended to the same file.\n await found.reduce(async (prev, k) => {\n await prev;\n const t = PROVIDER_TEMPLATES[k.name];\n if (t) await appendEndpointBlock(file, k.name, { ...t, keyEnv: k.keyEnv });\n }, Promise.resolve());\n return found;\n}\n"],"mappings":"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","names":["local"]}
1
+ {"version":3,"sources":["../src/cli/connect.ts"],"sourcesContent":["import { storeKey, findProviderKey } from '../auth/keys.js';\nimport { loadConfig } from '../config/load.js';\nimport { resolvePaths, userConfigFile } from '../core/paths.js';\nimport { UsageError } from '../errors.js';\nimport { discoverModels } from '../providers/discovery.js';\nimport {\n appendEndpointBlock,\n PROVIDER_TEMPLATES,\n type ProviderTemplate,\n} from './commands/providers.js';\nimport type { CliIO } from './io.js';\n\n/** Key prefixes that name their provider, from the catalog; longest first so sk-or- beats sk-. */\nconst KEY_PREFIXES: readonly (readonly [string, string])[] = (\n [\n ['sk-ant-', 'anthropic'],\n ...Object.entries(PROVIDER_TEMPLATES).flatMap(([name, t]): [string, string][] =>\n t.keyPrefix ? [[t.keyPrefix, name]] : [],\n ),\n ] as [string, string][]\n).sort((a, b) => b[0].length - a[0].length);\n\n/** The provider a pasted key belongs to, or undefined when it doesn't look like a key. */\nexport function providerForKey(text: string): string | undefined {\n const t = text.trim();\n if (/\\s/.test(t) || t.length < 20) return undefined;\n return KEY_PREFIXES.find(([p]) => t.startsWith(p))?.[1];\n}\n\n/** A token long and random enough to be a secret: 24+ chars mixing letters and digits. */\nconst SECRETISH = /^[A-Za-z0-9][A-Za-z0-9_.-]{23,}$/;\n\nexport interface FoundKey {\n key: string;\n /** Undefined when the key has no telltale prefix and the text names no provider. */\n provider: string | undefined;\n}\n\n/**\n * An API key inside a message (\"here's my mistral key abc123…\"): a token with a known prefix, or\n * a secret-looking token next to a provider name the catalog knows. Detection is local; the key is\n * never sent to a model.\n */\nexport function findKeyInText(text: string): FoundKey | undefined {\n const tokens = text.split(/[\\s\"'`,;()<>]+/).map((t) => t.replace(/[.:]+$/, ''));\n for (const t of tokens) {\n const provider = providerForKey(t);\n if (provider) return { key: t, provider };\n }\n const key = tokens.find(\n (t) => SECRETISH.test(t) && /\\d/.test(t) && /[A-Za-z]/.test(t) && !/^https?/i.test(t),\n );\n if (!key) return undefined;\n const words = text.toLowerCase();\n const named = Object.entries(PROVIDER_TEMPLATES).find(\n ([name, t]) =>\n name !== 'custom' &&\n !t.local &&\n (new RegExp(`\\\\b${name}\\\\b`).test(words) ||\n (t.label !== undefined && words.includes(t.label.toLowerCase()))),\n );\n if (named) return { key, provider: named[0] };\n return /\\b(api[ _-]?key|key|token|secret)\\b/.test(words)\n ? { key, provider: undefined }\n : undefined;\n}\n\nexport function looksLikeKey(text: string): boolean {\n return providerForKey(text) !== undefined;\n}\n\n/** A name for an endpoint URL: a matching template, else the host's distinctive label. */\nexport function nameForUrl(url: string): string {\n const u = new URL(url);\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (name === 'custom') continue;\n const tu = new URL(t.baseUrl);\n if (tu.host === u.host) return name;\n }\n if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return 'local';\n const labels = u.hostname.split('.').filter((l) => !['api', 'www', 'inference'].includes(l));\n const label = labels.length > 1 ? labels[labels.length - 2] : labels[0];\n return (label ?? 'custom').toLowerCase().replace(/[^a-z0-9_-]/g, '-');\n}\n\nexport interface ConnectResult {\n name: string;\n added: boolean;\n keyStored: boolean;\n models: string[];\n /** A ready-to-use model ref, e.g. \"openrouter:deepseek/deepseek-v4\". */\n suggested: string | undefined;\n notes: string[];\n}\n\n/**\n * One step from nothing to a usable provider. `target` is a provider name (\"openrouter\"), a base\n * URL (\"http://localhost:11434/v1\"), or a pasted API key. The key goes to the 0600 credentials\n * file, the endpoint to user config, and the endpoint's models are listed to prove it works.\n */\nexport async function connect(\n io: CliIO,\n target: string,\n key?: string,\n opts: { name?: string } = {},\n): Promise<ConnectResult> {\n let t = target.trim();\n let k = key?.trim() ? key.trim() : undefined;\n if (!t) throw new UsageError('nothing to connect', 'try `connect openrouter <key>` or a URL');\n const keyProvider = providerForKey(t);\n if (keyProvider && !k) {\n k = t;\n t = keyProvider;\n }\n let name: string;\n let template: ProviderTemplate | undefined;\n if (/^https?:\\/\\//i.test(t)) {\n name = opts.name ?? nameForUrl(t);\n const local = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1)/i.test(t);\n template = { baseUrl: t, keyEnv: undefined, free: local && !k, kind: 'openai' };\n } else {\n name = (opts.name ?? t).toLowerCase();\n template = PROVIDER_TEMPLATES[name];\n }\n if (!/^[a-z][a-z0-9_-]*$/.test(name))\n throw new UsageError(`bad provider name \"${name}\"`, 'use lowercase letters, digits, _ or -');\n\n const paths = resolvePaths(io.env);\n const notes: string[] = [];\n let keyStored = false;\n if (k) {\n await storeKey(paths, name, k);\n keyStored = true;\n }\n if (name === 'anthropic') {\n const found = await findProviderKey(paths, io.env, 'anthropic');\n if (!found) notes.push('no Anthropic key yet: paste one (sk-ant-…)');\n return {\n name,\n added: false,\n keyStored,\n models: ['opus', 'sonnet', 'haiku'],\n suggested: found ? 'anthropic:sonnet' : undefined,\n notes,\n };\n }\n\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const existing = config.providers.endpoints[name];\n let added = false;\n let baseUrl = existing?.base_url;\n if (!existing) {\n if (!template)\n throw new UsageError(\n `unknown provider \"${name}\"`,\n `known: ${Object.keys(PROVIDER_TEMPLATES).join(', ')}; or connect a base URL`,\n );\n // Keys live in the credentials file, so the endpoint doesn't need an env var.\n await appendEndpointBlock(userConfigFile(paths), name, { ...template, keyEnv: undefined });\n baseUrl = template.baseUrl;\n added = true;\n }\n const apiKey = (await findProviderKey(paths, io.env, name, existing?.api_key_env))?.key;\n if (!apiKey && !(existing?.free ?? template?.free)) notes.push(`no key for ${name} yet`);\n const local = /localhost|127\\.0\\.0\\.1/.test(baseUrl ?? '');\n const models = baseUrl\n ? (\n await discoverModels(\n { baseUrl, ...(apiKey ? { apiKey } : {}) },\n io.fetch ? { fetch: io.fetch } : {},\n )\n ).map((m) => m.id)\n : [];\n if (!models.length)\n notes.push(\n local\n ? `nothing answered at ${baseUrl}; is the server running?`\n : `couldn't list models at ${baseUrl}; check the key`,\n );\n const first = models[0];\n return {\n name,\n added,\n keyStored,\n models,\n suggested: first ? `${name}:${first}` : undefined,\n notes,\n };\n}\n\nexport function describeConnect(r: ConnectResult): string {\n const lines = [\n `${r.added ? 'added' : 'updated'} ${r.name}${r.keyStored ? ' (key saved, 0600)' : ''}`,\n ];\n if (r.models.length) {\n const shown = r.models.slice(0, 8).join(', ');\n lines.push(\n `${r.models.length} model${r.models.length === 1 ? '' : 's'}: ${shown}${r.models.length > 8 ? ', …' : ''}`,\n );\n }\n for (const n of r.notes) lines.push(n);\n if (r.suggested) lines.push(`chat with it: /model ${r.suggested}`);\n return lines.join('\\n');\n}\n\nexport interface EnvKey {\n name: string;\n label: string;\n keyEnv: string;\n}\n\n/**\n * Providers whose standard key variable is already set in the environment but that aren't\n * configured yet: the fastest way to a working model on a machine that already has keys.\n */\nexport async function envKeys(io: CliIO): Promise<EnvKey[]> {\n const { config } = await loadConfig({ cwd: io.cwd, env: io.env });\n const out: EnvKey[] = [];\n if (io.env.ANTHROPIC_API_KEY?.trim())\n out.push({ name: 'anthropic', label: 'Anthropic', keyEnv: 'ANTHROPIC_API_KEY' });\n for (const [name, t] of Object.entries(PROVIDER_TEMPLATES)) {\n if (!t.keyEnv || config.providers.endpoints[name]) continue;\n if (io.env[t.keyEnv]?.trim()) out.push({ name, label: t.label ?? name, keyEnv: t.keyEnv });\n }\n return out;\n}\n\n/**\n * `/connect env`: add every provider found by envKeys(), pointing each endpoint at its env var\n * (the key is never copied into omnexx's files). Anthropic needs nothing: its key is read from\n * the environment already.\n */\nexport async function connectFromEnv(io: CliIO): Promise<EnvKey[]> {\n const found = await envKeys(io);\n const file = userConfigFile(resolvePaths(io.env));\n // One after another: every block is appended to the same file.\n await found.reduce(async (prev, k) => {\n await prev;\n const t = PROVIDER_TEMPLATES[k.name];\n if (t) await appendEndpointBlock(file, k.name, { ...t, keyEnv: k.keyEnv });\n }, Promise.resolve());\n return found;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAaA,IAAM,eACJ;AAAA,EACE,CAAC,WAAW,WAAW;AAAA,EACvB,GAAG,OAAO,QAAQ,kBAAkB,EAAE;AAAA,IAAQ,CAAC,CAAC,MAAM,CAAC,MACrD,EAAE,YAAY,CAAC,CAAC,EAAE,WAAW,IAAI,CAAC,IAAI,CAAC;AAAA,EACzC;AACF,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AAGnC,SAAS,eAAe,MAAkC;AAC/D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,KAAK,KAAK,CAAC,KAAK,EAAE,SAAS,GAAI,QAAO;AAC1C,SAAO,aAAa,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC;AACxD;AAGA,IAAM,YAAY;AAaX,SAAS,cAAc,MAAoC;AAChE,QAAM,SAAS,KAAK,MAAM,gBAAgB,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,UAAU,EAAE,CAAC;AAC9E,aAAW,KAAK,QAAQ;AACtB,UAAM,WAAW,eAAe,CAAC;AACjC,QAAI,SAAU,QAAO,EAAE,KAAK,GAAG,SAAS;AAAA,EAC1C;AACA,QAAM,MAAM,OAAO;AAAA,IACjB,CAAC,MAAM,UAAU,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,WAAW,KAAK,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC;AAAA,EACtF;AACA,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,KAAK,YAAY;AAC/B,QAAM,QAAQ,OAAO,QAAQ,kBAAkB,EAAE;AAAA,IAC/C,CAAC,CAAC,MAAM,CAAC,MACP,SAAS,YACT,CAAC,EAAE,UACF,IAAI,OAAO,MAAM,IAAI,KAAK,EAAE,KAAK,KAAK,KACpC,EAAE,UAAU,UAAa,MAAM,SAAS,EAAE,MAAM,YAAY,CAAC;AAAA,EACpE;AACA,MAAI,MAAO,QAAO,EAAE,KAAK,UAAU,MAAM,CAAC,EAAE;AAC5C,SAAO,sCAAsC,KAAK,KAAK,IACnD,EAAE,KAAK,UAAU,OAAU,IAC3B;AACN;AAEO,SAAS,aAAa,MAAuB;AAClD,SAAO,eAAe,IAAI,MAAM;AAClC;AAGO,SAAS,WAAW,KAAqB;AAC9C,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,aAAW,CAAC,MAAM,CAAC,KAAK,OAAO,QAAQ,kBAAkB,GAAG;AAC1D,QAAI,SAAS,SAAU;AACvB,UAAM,KAAK,IAAI,IAAI,EAAE,OAAO;AAC5B,QAAI,GAAG,SAAS,EAAE,KAAM,QAAO;AAAA,EACjC;AACA,MAAI,EAAE,aAAa,eAAe,EAAE,aAAa,YAAa,QAAO;AACrE,QAAM,SAAS,EAAE,SAAS,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,WAAW,EAAE,SAAS,CAAC,CAAC;AAC3F,QAAM,QAAQ,OAAO,SAAS,IAAI,OAAO,OAAO,SAAS,CAAC,IAAI,OAAO,CAAC;AACtE,UAAQ,SAAS,UAAU,YAAY,EAAE,QAAQ,gBAAgB,GAAG;AACtE;AAiBA,eAAsB,QACpB,IACA,QACA,KACA,OAA0B,CAAC,GACH;AACxB,MAAI,IAAI,OAAO,KAAK;AACpB,MAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI;AACnC,MAAI,CAAC,EAAG,OAAM,IAAI,WAAW,sBAAsB,yCAAyC;AAC5F,QAAM,cAAc,eAAe,CAAC;AACpC,MAAI,eAAe,CAAC,GAAG;AACrB,QAAI;AACJ,QAAI;AAAA,EACN;AACA,MAAI;AACJ,MAAI;AACJ,MAAI,gBAAgB,KAAK,CAAC,GAAG;AAC3B,WAAO,KAAK,QAAQ,WAAW,CAAC;AAChC,UAAMA,SAAQ,wCAAwC,KAAK,CAAC;AAC5D,eAAW,EAAE,SAAS,GAAG,QAAQ,QAAW,MAAMA,UAAS,CAAC,GAAG,MAAM,SAAS;AAAA,EAChF,OAAO;AACL,YAAQ,KAAK,QAAQ,GAAG,YAAY;AACpC,eAAW,mBAAmB,IAAI;AAAA,EACpC;AACA,MAAI,CAAC,qBAAqB,KAAK,IAAI;AACjC,UAAM,IAAI,WAAW,sBAAsB,IAAI,KAAK,uCAAuC;AAE7F,QAAM,QAAQ,aAAa,GAAG,GAAG;AACjC,QAAM,QAAkB,CAAC;AACzB,MAAI,YAAY;AAChB,MAAI,GAAG;AACL,UAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,gBAAY;AAAA,EACd;AACA,MAAI,SAAS,aAAa;AACxB,UAAM,QAAQ,MAAM,gBAAgB,OAAO,GAAG,KAAK,WAAW;AAC9D,QAAI,CAAC,MAAO,OAAM,KAAK,iDAA4C;AACnE,WAAO;AAAA,MACL;AAAA,MACA,OAAO;AAAA,MACP;AAAA,MACA,QAAQ,CAAC,QAAQ,UAAU,OAAO;AAAA,MAClC,WAAW,QAAQ,qBAAqB;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,EAAE,KAAK,GAAG,KAAK,KAAK,GAAG,IAAI,CAAC;AAChE,QAAM,WAAW,OAAO,UAAU,UAAU,IAAI;AAChD,MAAI,QAAQ;AACZ,MAAI,UAAU,UAAU;AACxB,MAAI,CAAC,UAAU;AACb,QAAI,CAAC;AACH,YAAM,IAAI;AAAA,QACR,qBAAqB,IAAI;AAAA,QACzB,UAAU,OAAO,KAAK,kBAAkB,EAAE,KAAK,IAAI,CAAC;AAAA,MACtD;AAEF,UAAM,oBAAoB,eAAe,KAAK,GAAG,MAAM,EAAE,GAAG,UAAU,QAAQ,OAAU,CAAC;AACzF,cAAU,SAAS;AACnB,YAAQ;AAAA,EACV;AACA,QAAM,UAAU,MAAM,gBAAgB,OAAO,GAAG,KAAK,MAAM,UAAU,WAAW,IAAI;AACpF,MAAI,CAAC,UAAU,EAAE,UAAU,QAAQ,UAAU,MAAO,OAAM,KAAK,cAAc,IAAI,MAAM;AACvF,QAAM,QAAQ,yBAAyB,KAAK,WAAW,EAAE;AACzD,QAAM,SAAS,WAET,MAAM;AAAA,IACJ,EAAE,SAAS,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG;AAAA,IACzC,GAAG,QAAQ,EAAE,OAAO,GAAG,MAAM,IAAI,CAAC;AAAA,EACpC,GACA,IAAI,CAAC,MAAM,EAAE,EAAE,IACjB,CAAC;AACL,MAAI,CAAC,OAAO;AACV,UAAM;AAAA,MACJ,QACI,uBAAuB,OAAO,6BAC9B,2BAA2B,OAAO;AAAA,IACxC;AACF,QAAM,QAAQ,OAAO,CAAC;AACtB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,QAAQ,GAAG,IAAI,IAAI,KAAK,KAAK;AAAA,IACxC;AAAA,EACF;AACF;AAEO,SAAS,gBAAgB,GAA0B;AACxD,QAAM,QAAQ;AAAA,IACZ,GAAG,EAAE,QAAQ,UAAU,SAAS,IAAI,EAAE,IAAI,GAAG,EAAE,YAAY,uBAAuB,EAAE;AAAA,EACtF;AACA,MAAI,EAAE,OAAO,QAAQ;AACnB,UAAM,QAAQ,EAAE,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI;AAC5C,UAAM;AAAA,MACJ,GAAG,EAAE,OAAO,MAAM,SAAS,EAAE,OAAO,WAAW,IAAI,KAAK,GAAG,KAAK,KAAK,GAAG,EAAE,OAAO,SAAS,IAAI,aAAQ,EAAE;AAAA,IAC1G;AAAA,EACF;AACA,aAAW,KAAK,EAAE,MAAO,OAAM,KAAK,CAAC;AACrC,MAAI,EAAE,UAAW,OAAM,KAAK,wBAAwB,EAAE,SAAS,EAAE;AACjE,SAAO,MAAM,KAAK,IAAI;AACxB;AAYA,eAAsB,QAAQ,IAA8B;AAC1D,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,EAAE,KAAK,GAAG,KAAK,KAAK,GAAG,IAAI,CAAC;AAChE,QAAM,MAAgB,CAAC;AACvB,MAAI,GAAG,IAAI,mBAAmB,KAAK;AACjC,QAAI,KAAK,EAAE,MAAM,aAAa,OAAO,aAAa,QAAQ,oBAAoB,CAAC;AACjF,aAAW,CAAC,MAAM,CAAC,KAAK,OAAO,QAAQ,kBAAkB,GAAG;AAC1D,QAAI,CAAC,EAAE,UAAU,OAAO,UAAU,UAAU,IAAI,EAAG;AACnD,QAAI,GAAG,IAAI,EAAE,MAAM,GAAG,KAAK,EAAG,KAAI,KAAK,EAAE,MAAM,OAAO,EAAE,SAAS,MAAM,QAAQ,EAAE,OAAO,CAAC;AAAA,EAC3F;AACA,SAAO;AACT;AAOA,eAAsB,eAAe,IAA8B;AACjE,QAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,QAAM,OAAO,eAAe,aAAa,GAAG,GAAG,CAAC;AAEhD,QAAM,MAAM,OAAO,OAAO,MAAM,MAAM;AACpC,UAAM;AACN,UAAM,IAAI,mBAAmB,EAAE,IAAI;AACnC,QAAI,EAAG,OAAM,oBAAoB,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,QAAQ,EAAE,OAAO,CAAC;AAAA,EAC3E,GAAG,QAAQ,QAAQ,CAAC;AACpB,SAAO;AACT;","names":["local"]}