@tekmidian/pai 0.39.0 → 0.40.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 (126) hide show
  1. package/README.md +41 -0
  2. package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
  3. package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
  4. package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
  5. package/dist/chain-DhVHVnmT.mjs.map +1 -0
  6. package/dist/cli/index.mjs +10 -8
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/program.mjs +10 -8
  9. package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
  10. package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
  11. package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
  12. package/dist/config-D1G9IFpn.mjs.map +1 -0
  13. package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
  14. package/dist/config-Dl8lT4Lu.mjs +527 -0
  15. package/dist/config-Dl8lT4Lu.mjs.map +1 -0
  16. package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
  17. package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
  18. package/dist/daemon/index.mjs +12 -10
  19. package/dist/daemon/index.mjs.map +1 -1
  20. package/dist/daemon-BSL9sXxm.mjs +22 -0
  21. package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
  22. package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
  23. package/dist/daemon-mcp/index.mjs +127 -6
  24. package/dist/daemon-mcp/index.mjs.map +1 -1
  25. package/dist/detector-BLVOCiJ_.mjs +5 -0
  26. package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
  27. package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
  28. package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
  29. package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
  30. package/dist/factory-C7ua_3ik.mjs +3 -0
  31. package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
  32. package/dist/fallback-CdXv-Np0.mjs.map +1 -0
  33. package/dist/hooks/context-compression-hook.mjs +136 -22
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/post-compact-inject.mjs +98 -7
  36. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  37. package/dist/hooks/route-agents-to-worker.mjs +75 -6
  38. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  39. package/dist/hooks/worker-proxy.mjs +75 -6
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +149 -90
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
  44. package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
  45. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
  46. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
  47. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  48. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
  50. package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
  51. package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
  52. package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
  53. package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
  54. package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
  55. package/dist/main-resolver-E5WlilhM.mjs +6 -0
  56. package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
  57. package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
  58. package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
  59. package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
  60. package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
  61. package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
  62. package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
  63. package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
  64. package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
  65. package/dist/program-AR0qlYRg.mjs.map +1 -0
  66. package/dist/query-feedback-CGcEiGtY.mjs +3 -0
  67. package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
  68. package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
  69. package/dist/router-CAtyVQud.mjs +3 -0
  70. package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
  71. package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
  72. package/dist/server-BOuAOj9b.mjs +386 -0
  73. package/dist/server-BOuAOj9b.mjs.map +1 -0
  74. package/dist/skills/Worker/SKILL.md +19 -1
  75. package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
  76. package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
  77. package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
  78. package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
  79. package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
  80. package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
  81. package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
  82. package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
  83. package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
  84. package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
  85. package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
  86. package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
  87. package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
  88. package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
  89. package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
  90. package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
  91. package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
  92. package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
  93. package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
  94. package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
  95. package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
  96. package/docs/commands/README.md +5 -2
  97. package/docs/commands/worker.md +66 -5
  98. package/docs/images/provider-abstraction-live.png +0 -0
  99. package/docs/images/workers.png +0 -0
  100. package/docs/launcher-independence.md +70 -0
  101. package/docs/provider-abstraction.md +87 -0
  102. package/docs/provider-independence-details.md +330 -0
  103. package/docs/provider-independence.md +75 -0
  104. package/docs/worker.md +72 -11
  105. package/package.json +1 -1
  106. package/scripts/build-hooks.mjs +6 -1
  107. package/scripts/build-skill-stubs.mjs +17 -4
  108. package/src/hooks/ts/lib/context-fill.test.ts +158 -4
  109. package/src/hooks/ts/lib/context-fill.ts +102 -35
  110. package/statusline-command.sh +120 -21
  111. package/dist/chain-CTtHligI.mjs.map +0 -1
  112. package/dist/config-B64vFg14.mjs.map +0 -1
  113. package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
  114. package/dist/daemon-CmHHmKpg.mjs +0 -20
  115. package/dist/detector-DO730Zq0.mjs +0 -5
  116. package/dist/factory-BD-np0Vd.mjs +0 -3
  117. package/dist/main-resolver-D6IImXvF.mjs +0 -4
  118. package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
  119. package/dist/program-DXBwJV7h.mjs.map +0 -1
  120. package/dist/providers-FYkZjn_C.mjs.map +0 -1
  121. package/dist/query-feedback-DSVyHtrG.mjs +0 -3
  122. package/dist/router-Bk77E7hj.mjs +0 -3
  123. package/dist/runtime-paths-rni52zHX.mjs +0 -50
  124. package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
  125. package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
  126. package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
@@ -0,0 +1,527 @@
1
+ import { n as CONFIG_FILE } from "./config-D1G9IFpn.mjs";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+
6
+ //#region src/config/json-store.ts
7
+ /**
8
+ * json-store.ts — read/write JSON config files without destroying them
9
+ *
10
+ * The failure this exists to prevent:
11
+ *
12
+ * try { return JSON.parse(read(path)); } catch { return {}; }
13
+ * ... later ...
14
+ * write(path, JSON.stringify(ourData));
15
+ *
16
+ * An unreadable file becomes an empty object, and the next write makes that
17
+ * permanent. Silently, exit code 0. This shape appeared three times in this
18
+ * repo against three different files, and twice in AIBroker.
19
+ *
20
+ * The distinction that matters is between *missing* and *unreadable*:
21
+ *
22
+ * missing — legitimate first run. Start fresh; writing is safe.
23
+ * unreadable — the file exists and we could not parse it. Those bytes are
24
+ * the only copy of something. Never overwrite them.
25
+ *
26
+ * Collapsing the second into the first is the bug.
27
+ *
28
+ * NOT everything deserves this guard. For a transient buffer — an undelivered
29
+ * message queue, a cache — starting fresh IS the correct recovery, and
30
+ * refusing to write would disable the feature permanently. Use `writeJsonAtomic`
31
+ * alone there: it still prevents a crash from truncating a good file, without
32
+ * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.
33
+ */
34
+ /**
35
+ * Read a JSON file, distinguishing "absent" from "damaged".
36
+ *
37
+ * @param path file to read
38
+ * @param label how to name it to the user, e.g. "~/.claude.json"
39
+ * @throws if the file exists but cannot be read or parsed
40
+ */
41
+ function readJsonStrict(path, label = path) {
42
+ if (!existsSync(path)) return {};
43
+ let raw;
44
+ try {
45
+ raw = readFileSync(path, "utf8");
46
+ } catch (e) {
47
+ throw new Error(`Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — writing now would replace its contents with ours alone.`);
48
+ }
49
+ try {
50
+ return JSON.parse(raw);
51
+ } catch (e) {
52
+ throw new Error(`${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — overwriting it would destroy whatever it holds.\nRepair the file, or move it aside and re-run this command.`);
53
+ }
54
+ }
55
+ /**
56
+ * Write JSON without risking the existing file.
57
+ *
58
+ * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file
59
+ * and renames. Rename is atomic within a filesystem, so a crash mid-write
60
+ * leaves the original intact rather than truncated — which is how these files
61
+ * become corrupt in the first place.
62
+ */
63
+ function writeJsonAtomic(path, data, opts = {}) {
64
+ const { backup = true, label = path } = opts;
65
+ const serialized = JSON.stringify(data, null, 2) + "\n";
66
+ const dir = dirname(path);
67
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
68
+ if (backup && existsSync(path)) try {
69
+ copyFileSync(path, `${path}.bak-pai`);
70
+ } catch (e) {
71
+ throw new Error(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
72
+ }
73
+ const tmp = `${path}.tmp-pai-${process.pid}`;
74
+ try {
75
+ writeFileSync(tmp, serialized, "utf8");
76
+ renameSync(tmp, path);
77
+ } catch (e) {
78
+ try {
79
+ if (existsSync(tmp)) unlinkSync(tmp);
80
+ } catch {}
81
+ throw new Error(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged.`);
82
+ }
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/utils/model-window.ts
87
+ /**
88
+ * model-window.ts — facts derivable from a bare model id.
89
+ *
90
+ * Shared by the worker stack (provider registry) and the session hooks
91
+ * (context-fill). Zero imports on purpose: this leaf is bundled into hooks
92
+ * (scripts/build-hooks.mjs) and must never drag config or path machinery
93
+ * into a hook process.
94
+ */
95
+ /** Last-resort window when nothing on hand reports one and the model id
96
+ * carries no window information. */
97
+ const DEFAULT_CONTEXT_WINDOW = 2e5;
98
+ /** Strip a bracketed variant suffix: "glm-5.3[1m]" → "glm-5.3". The suffix
99
+ * selects a variant of the same base model (a context-window tier, a
100
+ * quantization), so the prefix is the id's family. */
101
+ function stripModelVariant(model) {
102
+ return model.replace(/\[[^\]]*\]\s*$/, "").trim() || model;
103
+ }
104
+ /**
105
+ * The context window a model id itself declares, or null when the id carries
106
+ * no window information. Only the bracketed variant suffix is read
107
+ * ("[1m]" → 1,000,000); a bare id says nothing and stays null so callers can
108
+ * fall back to whatever they measured themselves.
109
+ */
110
+ function contextWindowFromModelId(model) {
111
+ if (typeof model !== "string" || model === "") return null;
112
+ const m = /\[(\d+)m\]$/i.exec(model.trim());
113
+ if (!m) return null;
114
+ const millions = Number(m[1]);
115
+ if (!Number.isFinite(millions) || millions < 1) return null;
116
+ return millions * 1e6;
117
+ }
118
+
119
+ //#endregion
120
+ //#region src/workers/config.ts
121
+ /**
122
+ * config.ts — the `workers` section of ~/.config/pai/config.json
123
+ *
124
+ * Everything that knows about worker providers reads this module: the CLI
125
+ * (`pai worker …`), the MCP tools (worker_*), and the Agent-routing hook.
126
+ * Keep it free of commander/MCP imports so all three layers stay thin over it.
127
+ *
128
+ * Keys are never stored here — only paths to 0600 files. A provider without a
129
+ * keyFile is a local server and gets the placeholder token "local".
130
+ *
131
+ * `protocol: "openai"` providers run through the PAI proxy (src/workers/proxy):
132
+ * `upstreamUrl` is their Chat Completions base, and `run` points
133
+ * ANTHROPIC_BASE_URL at the local proxy with the provider name in the path.
134
+ * `engine: "codex"` providers run through the Codex CLI instead of Claude
135
+ * Code.
136
+ */
137
+ const MODEL_TIERS = [
138
+ "haiku",
139
+ "sonnet",
140
+ "opus"
141
+ ];
142
+ const DEFAULT_COST_TIER = 3;
143
+ /** Tags a provider may carry; classes filter auto-routing on them. */
144
+ const PROVIDER_TAGS = [
145
+ "code",
146
+ "vision",
147
+ "image-gen",
148
+ "long-context",
149
+ "fast",
150
+ "reasoning"
151
+ ];
152
+ /** The standard task classes; `workers.classes` maps each to a target. */
153
+ const WORKER_CLASSES = [
154
+ "draft",
155
+ "plan",
156
+ "implement",
157
+ "review",
158
+ "research",
159
+ "spotcheck",
160
+ "simple",
161
+ "complex",
162
+ "image"
163
+ ];
164
+ /**
165
+ * The named model capabilities a provider may carry a preference for, set
166
+ * per provider under `workers.providers.<name>.models`. "default" is the
167
+ * required catch-all; the others name what a model is *for* — "fast" the
168
+ * cheap tier (spotcheck, haiku-tier spawns), "image" the image class.
169
+ * Everything resolves through resolveModelCapability, falling back to default.
170
+ */
171
+ const MODEL_CAPABILITIES = [
172
+ "default",
173
+ "fast",
174
+ "image"
175
+ ];
176
+ /** Is this string the name of a known model capability? */
177
+ function isModelCapability(v) {
178
+ return MODEL_CAPABILITIES.includes(v);
179
+ }
180
+ /**
181
+ * Which model capability a class runs on when its target names no alias: the
182
+ * image class uses the image model; every other class the provider default.
183
+ */
184
+ const CLASS_MODEL_CAPABILITY = {
185
+ draft: "default",
186
+ plan: "default",
187
+ implement: "default",
188
+ review: "default",
189
+ research: "default",
190
+ spotcheck: "default",
191
+ simple: "default",
192
+ complex: "default",
193
+ image: "image"
194
+ };
195
+ /** Capability for a run's class (default when unset or not a standard class). */
196
+ function classModelCapability(className) {
197
+ return className && CLASS_MODEL_CAPABILITY[className] || "default";
198
+ }
199
+ const DEFAULT_LOG_DIR = "~/.claude/logs/workers";
200
+ const DEFAULT_PANE = {
201
+ enabled: true,
202
+ fontSize: 13,
203
+ autoExitSecs: 60
204
+ };
205
+ const DEFAULT_ROUTING = {
206
+ order: [],
207
+ cooldownMinutes: 30,
208
+ retryOnQuota: true
209
+ };
210
+ const DEFAULT_TREE = {
211
+ maxDepth: 2,
212
+ maxChildren: 4
213
+ };
214
+ /**
215
+ * The default MCP sets every config starts from. `desktop` names the clickr
216
+ * server so `--mcp desktop` hands a worker the machine controls (read-only
217
+ * tools always; actuating ones after the operator hands the controls over,
218
+ * see `pai worker controls`). A user's mcpSets section is merged over this,
219
+ * so `desktop: []` removes the set deliberately.
220
+ */
221
+ const DEFAULT_MCP_SETS = { desktop: ["clickr"] };
222
+ function defaultWorkersConfig() {
223
+ return {
224
+ enabled: false,
225
+ active: null,
226
+ providers: {},
227
+ classes: {},
228
+ mcpSets: { ...DEFAULT_MCP_SETS },
229
+ pane: { ...DEFAULT_PANE },
230
+ logDir: DEFAULT_LOG_DIR,
231
+ routing: {
232
+ ...DEFAULT_ROUTING,
233
+ order: []
234
+ },
235
+ tree: { ...DEFAULT_TREE },
236
+ fallback: null
237
+ };
238
+ }
239
+ var WorkersConfigError = class extends Error {};
240
+ function bad(path, why) {
241
+ throw new WorkersConfigError(`workers${path}: ${why}`);
242
+ }
243
+ function str(v) {
244
+ return typeof v === "string" ? v : "";
245
+ }
246
+ function parseProvider(name, raw) {
247
+ if (typeof raw !== "object" || raw === null) bad(`.providers.${name}`, "must be an object");
248
+ const p = raw;
249
+ const protocol = p.protocol === void 0 ? "anthropic" : str(p.protocol);
250
+ if (protocol !== "anthropic" && protocol !== "openai") bad(`.providers.${name}.protocol`, `"${str(p.protocol)}" is neither "anthropic" nor "openai"`);
251
+ const engine = p.engine === void 0 ? "claude" : str(p.engine);
252
+ if (engine !== "claude" && engine !== "codex") bad(`.providers.${name}.engine`, `"${str(p.engine)}" is neither "claude" nor "codex"`);
253
+ const baseUrl = str(p.baseUrl);
254
+ if (!baseUrl && protocol !== "openai") bad(`.providers.${name}.baseUrl`, "is required");
255
+ const keyFile = p.keyFile === void 0 || p.keyFile === null || str(p.keyFile) === "" ? null : str(p.keyFile);
256
+ const modelsRaw = p.models === void 0 ? {} : p.models;
257
+ if (typeof modelsRaw !== "object" || modelsRaw === null) bad(`.providers.${name}.models`, "must be an object");
258
+ const m = modelsRaw;
259
+ const defaultModel = str(m.default);
260
+ if (!defaultModel) bad(`.providers.${name}.models.default`, "is required");
261
+ const models = { default: defaultModel };
262
+ for (const key of Object.keys(m)) {
263
+ if (key === "default") continue;
264
+ if (!isModelCapability(key)) bad(`.providers.${name}.models.${key}`, `"${key}" is not a model capability (from: ${MODEL_CAPABILITIES.join(", ")})`);
265
+ const id = str(m[key]);
266
+ if (!id) bad(`.providers.${name}.models.${key}`, "must be a non-empty model id");
267
+ models[key] = id;
268
+ }
269
+ let modelTiers;
270
+ if (p.modelTiers !== void 0) {
271
+ if (typeof p.modelTiers !== "object" || p.modelTiers === null || Array.isArray(p.modelTiers)) bad(`.providers.${name}.modelTiers`, "must be an object of model id → haiku | sonnet | opus");
272
+ modelTiers = {};
273
+ for (const [id, tier] of Object.entries(p.modelTiers)) {
274
+ if (typeof tier !== "string" || !MODEL_TIERS.includes(tier)) bad(`.providers.${name}.modelTiers.${id}`, `must be "haiku", "sonnet" or "opus" (got ${JSON.stringify(tier)})`);
275
+ modelTiers[id] = tier;
276
+ }
277
+ }
278
+ const env = {};
279
+ if (p.env !== void 0) {
280
+ if (typeof p.env !== "object" || p.env === null || Array.isArray(p.env)) bad(`.providers.${name}.env`, "must be an object of string values");
281
+ for (const [k, v] of Object.entries(p.env)) {
282
+ if (typeof v !== "string") bad(`.providers.${name}.env.${k}`, "must be a string");
283
+ env[k] = v;
284
+ }
285
+ }
286
+ const quotaSkipAt = p.quotaSkipAt === void 0 ? void 0 : p.quotaSkipAt;
287
+ if (quotaSkipAt !== void 0) {
288
+ if (typeof quotaSkipAt !== "number" || quotaSkipAt < 0 || quotaSkipAt > 100) bad(`.providers.${name}.quotaSkipAt`, "must be a number between 0 and 100");
289
+ }
290
+ const contextWindow = p.contextWindow === void 0 ? void 0 : p.contextWindow;
291
+ if (contextWindow !== void 0) {
292
+ if (typeof contextWindow !== "number" || contextWindow <= 0) bad(`.providers.${name}.contextWindow`, "must be a positive number of tokens");
293
+ }
294
+ const upstreamUrl = str(p.upstreamUrl);
295
+ if (protocol === "openai" && !upstreamUrl) bad(`.providers.${name}.upstreamUrl`, `is required for protocol "openai" (the Chat Completions base, e.g. "https://api.openai.com/v1")`);
296
+ const costTier = p.costTier === void 0 ? void 0 : p.costTier;
297
+ if (costTier !== void 0) {
298
+ if (typeof costTier !== "number" || !Number.isInteger(costTier) || costTier < 1 || costTier > 5) bad(`.providers.${name}.costTier`, "must be an integer 1 (cheapest) … 5 (most expensive)");
299
+ }
300
+ let tags;
301
+ if (p.tags !== void 0) {
302
+ if (!Array.isArray(p.tags) || p.tags.some((x) => typeof x !== "string")) bad(`.providers.${name}.tags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
303
+ for (const t of p.tags) if (!PROVIDER_TAGS.includes(t)) bad(`.providers.${name}.tags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
304
+ tags = p.tags;
305
+ }
306
+ return {
307
+ enabled: p.enabled === void 0 ? true : p.enabled === true,
308
+ protocol,
309
+ baseUrl,
310
+ keyFile,
311
+ models,
312
+ ...modelTiers ? { modelTiers } : {},
313
+ env,
314
+ ...str(p.note) ? { note: str(p.note) } : {},
315
+ ...upstreamUrl ? { upstreamUrl } : {},
316
+ ...engine !== "claude" ? { engine } : {},
317
+ ...str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {},
318
+ ...quotaSkipAt !== void 0 ? { quotaSkipAt } : {},
319
+ ...contextWindow !== void 0 ? { contextWindow } : {},
320
+ ...costTier !== void 0 ? { costTier } : {},
321
+ ...tags ? { tags } : {}
322
+ };
323
+ }
324
+ /**
325
+ * Parse and validate a raw `workers` value. Missing section → defaults.
326
+ * Unknown-but-typed garbage → WorkersConfigError naming the offending field.
327
+ */
328
+ function parseWorkersConfig(raw) {
329
+ const d = defaultWorkersConfig();
330
+ if (raw === void 0 || raw === null) return d;
331
+ if (typeof raw !== "object" || Array.isArray(raw)) bad("", "section must be an object");
332
+ const w = raw;
333
+ const providers = {};
334
+ if (w.providers !== void 0) {
335
+ if (typeof w.providers !== "object" || w.providers === null || Array.isArray(w.providers)) bad(".providers", "must be an object keyed by provider name");
336
+ for (const [name, p] of Object.entries(w.providers)) providers[name] = parseProvider(name, p);
337
+ }
338
+ const classes = {};
339
+ const classesRaw = w.classes !== void 0 ? w.classes : w.roles;
340
+ if (classesRaw !== void 0) {
341
+ if (typeof classesRaw !== "object" || classesRaw === null || Array.isArray(classesRaw)) bad(w.classes !== void 0 ? ".classes" : ".roles", "must be an object of class → provider[/alias] or {provider, mcp, maxCostTier, requireTags, order}");
342
+ for (const [cls, target] of Object.entries(classesRaw)) if (typeof target === "object" && target !== null && !Array.isArray(target)) {
343
+ const o = target;
344
+ const provider = str(o.provider);
345
+ if (o.provider !== void 0 && (!provider || provider.includes(" "))) bad(`.classes.${cls}.provider`, `invalid provider "${provider}"`);
346
+ let mcp;
347
+ if (o.mcp !== void 0) {
348
+ if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) bad(`.classes.${cls}.mcp`, "must be an array of MCP server or set names");
349
+ mcp = o.mcp;
350
+ }
351
+ let maxCostTier;
352
+ if (o.maxCostTier !== void 0) {
353
+ if (typeof o.maxCostTier !== "number" || !Number.isInteger(o.maxCostTier) || o.maxCostTier < 1 || o.maxCostTier > 5) bad(`.classes.${cls}.maxCostTier`, "must be an integer 1 … 5");
354
+ maxCostTier = o.maxCostTier;
355
+ }
356
+ let requireTags;
357
+ if (o.requireTags !== void 0) {
358
+ if (!Array.isArray(o.requireTags) || o.requireTags.some((x) => typeof x !== "string")) bad(`.classes.${cls}.requireTags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
359
+ for (const t of o.requireTags) if (!PROVIDER_TAGS.includes(t)) bad(`.classes.${cls}.requireTags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
360
+ requireTags = o.requireTags;
361
+ }
362
+ let order;
363
+ if (o.order !== void 0) {
364
+ if (!Array.isArray(o.order) || o.order.some((x) => typeof x !== "string")) bad(`.classes.${cls}.order`, "must be an array of provider names");
365
+ order = o.order;
366
+ }
367
+ const obj = {
368
+ ...provider ? { provider } : {},
369
+ ...mcp ? { mcp } : {},
370
+ ...maxCostTier !== void 0 ? { maxCostTier } : {},
371
+ ...requireTags ? { requireTags } : {},
372
+ ...order ? { order } : {}
373
+ };
374
+ classes[cls] = Object.keys(obj).length ? obj : {};
375
+ } else {
376
+ const t = str(target);
377
+ if (!t || t.includes(" ")) bad(`.classes.${cls}`, `invalid target "${t}"`);
378
+ classes[cls] = t;
379
+ }
380
+ }
381
+ const mcpSets = { ...DEFAULT_MCP_SETS };
382
+ if (w.mcpSets !== void 0) {
383
+ if (typeof w.mcpSets !== "object" || w.mcpSets === null || Array.isArray(w.mcpSets)) bad(".mcpSets", "must be an object of set name → [server names]");
384
+ for (const [setName, servers] of Object.entries(w.mcpSets)) {
385
+ if (!Array.isArray(servers) || servers.some((x) => typeof x !== "string")) bad(`.mcpSets.${setName}`, "must be an array of MCP server names");
386
+ mcpSets[setName] = servers;
387
+ }
388
+ }
389
+ let pane = { ...DEFAULT_PANE };
390
+ if (w.pane !== void 0) {
391
+ if (typeof w.pane !== "object" || w.pane === null) bad(".pane", "must be an object");
392
+ const pc = w.pane;
393
+ if (pc.enabled !== void 0 && typeof pc.enabled !== "boolean") bad(".pane.enabled", "must be boolean");
394
+ if (pc.fontSize !== void 0 && (typeof pc.fontSize !== "number" || pc.fontSize <= 0)) bad(".pane.fontSize", "must be a positive number of points");
395
+ if (pc.autoExitSecs !== void 0 && typeof pc.autoExitSecs !== "number") bad(".pane.autoExitSecs", "must be a number");
396
+ pane = {
397
+ enabled: pc.enabled === void 0 ? DEFAULT_PANE.enabled : pc.enabled === true,
398
+ fontSize: pc.fontSize === void 0 ? DEFAULT_PANE.fontSize : pc.fontSize,
399
+ autoExitSecs: pc.autoExitSecs === void 0 ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs
400
+ };
401
+ }
402
+ let routing = {
403
+ ...DEFAULT_ROUTING,
404
+ order: []
405
+ };
406
+ if (w.routing !== void 0) {
407
+ if (typeof w.routing !== "object" || w.routing === null) bad(".routing", "must be an object");
408
+ const r = w.routing;
409
+ if (r.order !== void 0) {
410
+ if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== "string")) bad(".routing.order", "must be an array of provider names");
411
+ routing.order = r.order;
412
+ }
413
+ if (r.cooldownMinutes !== void 0 && typeof r.cooldownMinutes !== "number") bad(".routing.cooldownMinutes", "must be a number");
414
+ if (r.retryOnQuota !== void 0 && typeof r.retryOnQuota !== "boolean") bad(".routing.retryOnQuota", "must be boolean");
415
+ routing = {
416
+ order: routing.order,
417
+ cooldownMinutes: r.cooldownMinutes === void 0 ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,
418
+ retryOnQuota: r.retryOnQuota === void 0 ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true
419
+ };
420
+ }
421
+ let tree = { ...DEFAULT_TREE };
422
+ if (w.tree !== void 0) {
423
+ if (typeof w.tree !== "object" || w.tree === null || Array.isArray(w.tree)) bad(".tree", "must be an object");
424
+ const t = w.tree;
425
+ if (t.maxDepth !== void 0) {
426
+ if (typeof t.maxDepth !== "number" || !Number.isInteger(t.maxDepth) || t.maxDepth < 0) bad(".tree.maxDepth", "must be a non-negative integer");
427
+ }
428
+ if (t.maxChildren !== void 0) {
429
+ if (typeof t.maxChildren !== "number" || !Number.isInteger(t.maxChildren) || t.maxChildren < 1) bad(".tree.maxChildren", "must be a positive integer");
430
+ }
431
+ tree = {
432
+ maxDepth: t.maxDepth === void 0 ? DEFAULT_TREE.maxDepth : t.maxDepth,
433
+ maxChildren: t.maxChildren === void 0 ? DEFAULT_TREE.maxChildren : t.maxChildren
434
+ };
435
+ }
436
+ const active = w.active === void 0 || w.active === null ? null : str(w.active);
437
+ if (active !== null && active !== "auto" && !(active in providers)) {}
438
+ let fallback = null;
439
+ if (w.fallback !== void 0 && w.fallback !== null) {
440
+ if (typeof w.fallback !== "object" || Array.isArray(w.fallback)) bad(".fallback", "must be an object (written by `pai worker fallback on`)");
441
+ const f = w.fallback;
442
+ const provider = str(f.provider);
443
+ if (!provider) bad(".fallback.provider", "is required");
444
+ const savedRaw = f.saved;
445
+ if (typeof savedRaw !== "object" || savedRaw === null || Array.isArray(savedRaw)) bad(".fallback.saved", "must be an object");
446
+ const s = savedRaw;
447
+ if (typeof s.env !== "object" || s.env === null || Array.isArray(s.env)) bad(".fallback.saved.env", "must be an object of env key → previous value or null");
448
+ const env = {};
449
+ for (const [k, v] of Object.entries(s.env)) env[k] = v === null || typeof v === "string" ? v : null;
450
+ if (s.model !== null && s.model !== void 0 && typeof s.model !== "string") bad(".fallback.saved.model", "must be a string or null");
451
+ fallback = {
452
+ provider,
453
+ saved: {
454
+ env,
455
+ model: typeof s.model === "string" ? s.model : null,
456
+ envExisted: s.envExisted === true
457
+ },
458
+ on: str(f.on) || (/* @__PURE__ */ new Date(0)).toISOString()
459
+ };
460
+ }
461
+ return {
462
+ enabled: w.enabled === void 0 ? d.enabled : w.enabled === true,
463
+ active,
464
+ providers,
465
+ classes,
466
+ mcpSets,
467
+ pane,
468
+ logDir: str(w.logDir) || d.logDir,
469
+ routing,
470
+ tree,
471
+ fallback
472
+ };
473
+ }
474
+ /**
475
+ * Read the whole config file and return (raw, workers) — the raw record so
476
+ * callers can rewrite it preserving every other section, the parsed+validated
477
+ * workers section. Unreadable config throws (readJsonStrict), missing is fine.
478
+ * `path` overrides the config location (tests, CLAUDE_SETTINGS_PATH-style
479
+ * dry runs); default ~/.config/pai/config.json.
480
+ */
481
+ function readWorkersSection(path = CONFIG_FILE) {
482
+ const raw = readJsonStrict(path, "~/.config/pai/config.json");
483
+ return {
484
+ raw,
485
+ workers: parseWorkersConfig(raw.workers)
486
+ };
487
+ }
488
+ /** Write the workers section back into the config file, atomically. */
489
+ function writeWorkersSection(raw, workers, path = CONFIG_FILE) {
490
+ raw.workers = workers.fallback ? workers : {
491
+ ...workers,
492
+ fallback: void 0
493
+ };
494
+ writeJsonAtomic(path, raw, { label: "~/.config/pai/config.json" });
495
+ }
496
+ /** Expand a leading ~ (config values are written with `~` to stay portable). */
497
+ function expandHome(p) {
498
+ if (p === "~" || p.startsWith("~/")) return join(homedir(), p.slice(1));
499
+ return p;
500
+ }
501
+ function assertProviderRunnable(name, p) {
502
+ if (p.protocol === "openai" && !p.upstreamUrl) throw new WorkersConfigError(`provider "${name}" uses protocol "openai" but has no upstreamUrl — set its Chat Completions base (e.g. "https://api.openai.com/v1") so the PAI proxy knows where to translate to.`);
503
+ }
504
+ /** Where a provider's key file lives, or null. Shared by run + test + add. */
505
+ function providerKeyPath(p) {
506
+ return p.keyFile ? expandHome(p.keyFile) : null;
507
+ }
508
+ /** Cost tier of a provider for class filtering (unset = 3, the middle). */
509
+ function providerCostTier(p) {
510
+ return p.costTier ?? DEFAULT_COST_TIER;
511
+ }
512
+ /**
513
+ * The model id for a capability: the provider's preference for it, else its
514
+ * default model — the one resolution rule every consumer (image class, fast
515
+ * tier spawns, env pins) goes through.
516
+ */
517
+ function resolveModelCapability(p, capability) {
518
+ return p.models[capability] ?? p.models.default;
519
+ }
520
+ /** Directory under which inline keys (MCP `key` field) are stored, 0600. */
521
+ function keysDir() {
522
+ return join(homedir(), ".config", "pai", "keys");
523
+ }
524
+
525
+ //#endregion
526
+ export { DEFAULT_CONTEXT_WINDOW as _, WorkersConfigError as a, readJsonStrict as b, expandHome as c, parseWorkersConfig as d, providerCostTier as f, writeWorkersSection as g, resolveModelCapability as h, WORKER_CLASSES as i, isModelCapability as l, readWorkersSection as m, MODEL_CAPABILITIES as n, assertProviderRunnable as o, providerKeyPath as p, PROVIDER_TAGS as r, classModelCapability as s, DEFAULT_LOG_DIR as t, keysDir as u, contextWindowFromModelId as v, writeJsonAtomic as x, stripModelVariant as y };
527
+ //# sourceMappingURL=config-Dl8lT4Lu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-Dl8lT4Lu.mjs","names":[],"sources":["../src/config/json-store.ts","../src/utils/model-window.ts","../src/workers/config.ts"],"sourcesContent":["/**\n * json-store.ts — read/write JSON config files without destroying them\n *\n * The failure this exists to prevent:\n *\n * try { return JSON.parse(read(path)); } catch { return {}; }\n * ... later ...\n * write(path, JSON.stringify(ourData));\n *\n * An unreadable file becomes an empty object, and the next write makes that\n * permanent. Silently, exit code 0. This shape appeared three times in this\n * repo against three different files, and twice in AIBroker.\n *\n * The distinction that matters is between *missing* and *unreadable*:\n *\n * missing — legitimate first run. Start fresh; writing is safe.\n * unreadable — the file exists and we could not parse it. Those bytes are\n * the only copy of something. Never overwrite them.\n *\n * Collapsing the second into the first is the bug.\n *\n * NOT everything deserves this guard. For a transient buffer — an undelivered\n * message queue, a cache — starting fresh IS the correct recovery, and\n * refusing to write would disable the feature permanently. Use `writeJsonAtomic`\n * alone there: it still prevents a crash from truncating a good file, without\n * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n copyFileSync,\n renameSync,\n unlinkSync,\n mkdirSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n/**\n * Read a JSON file, distinguishing \"absent\" from \"damaged\".\n *\n * @param path file to read\n * @param label how to name it to the user, e.g. \"~/.claude.json\"\n * @throws if the file exists but cannot be read or parsed\n */\nexport function readJsonStrict(path: string, label = path): Record<string, unknown> {\n if (!existsSync(path)) return {};\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (e) {\n throw new Error(\n `Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue — writing now would replace its contents with ours alone.`\n );\n }\n\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n throw new Error(\n `${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue — overwriting it would destroy whatever it holds.\\n` +\n `Repair the file, or move it aside and re-run this command.`\n );\n }\n}\n\n/**\n * Write JSON without risking the existing file.\n *\n * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file\n * and renames. Rename is atomic within a filesystem, so a crash mid-write\n * leaves the original intact rather than truncated — which is how these files\n * become corrupt in the first place.\n */\nexport function writeJsonAtomic(\n path: string,\n data: Record<string, unknown>,\n opts: { backup?: boolean; label?: string } = {}\n): void {\n const { backup = true, label = path } = opts;\n const serialized = JSON.stringify(data, null, 2) + \"\\n\";\n\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n if (backup && existsSync(path)) {\n try {\n copyFileSync(path, `${path}.bak-pai`);\n } catch (e) {\n throw new Error(\n `Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to write without a backup.`\n );\n }\n }\n\n const tmp = `${path}.tmp-pai-${process.pid}`;\n try {\n writeFileSync(tmp, serialized, \"utf8\");\n renameSync(tmp, path);\n } catch (e) {\n try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best effort */ }\n throw new Error(\n `Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `The original is unchanged.`\n );\n }\n}\n","/**\n * model-window.ts — facts derivable from a bare model id.\n *\n * Shared by the worker stack (provider registry) and the session hooks\n * (context-fill). Zero imports on purpose: this leaf is bundled into hooks\n * (scripts/build-hooks.mjs) and must never drag config or path machinery\n * into a hook process.\n */\n\n/** Last-resort window when nothing on hand reports one and the model id\n * carries no window information. */\nexport const DEFAULT_CONTEXT_WINDOW = 200_000;\n\n/** Strip a bracketed variant suffix: \"glm-5.3[1m]\" → \"glm-5.3\". The suffix\n * selects a variant of the same base model (a context-window tier, a\n * quantization), so the prefix is the id's family. */\nexport function stripModelVariant(model: string): string {\n const stripped = model.replace(/\\[[^\\]]*\\]\\s*$/, \"\").trim();\n return stripped || model;\n}\n\n/**\n * The context window a model id itself declares, or null when the id carries\n * no window information. Only the bracketed variant suffix is read\n * (\"[1m]\" → 1,000,000); a bare id says nothing and stays null so callers can\n * fall back to whatever they measured themselves.\n */\nexport function contextWindowFromModelId(model: string | null | undefined): number | null {\n if (typeof model !== \"string\" || model === \"\") return null;\n const m = /\\[(\\d+)m\\]$/i.exec(model.trim());\n if (!m) return null;\n const millions = Number(m[1]);\n if (!Number.isFinite(millions) || millions < 1) return null;\n return millions * 1_000_000;\n}\n","/**\n * config.ts — the `workers` section of ~/.config/pai/config.json\n *\n * Everything that knows about worker providers reads this module: the CLI\n * (`pai worker …`), the MCP tools (worker_*), and the Agent-routing hook.\n * Keep it free of commander/MCP imports so all three layers stay thin over it.\n *\n * Keys are never stored here — only paths to 0600 files. A provider without a\n * keyFile is a local server and gets the placeholder token \"local\".\n *\n * `protocol: \"openai\"` providers run through the PAI proxy (src/workers/proxy):\n * `upstreamUrl` is their Chat Completions base, and `run` points\n * ANTHROPIC_BASE_URL at the local proxy with the provider name in the path.\n * `engine: \"codex\"` providers run through the Codex CLI instead of Claude\n * Code.\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { readJsonStrict, writeJsonAtomic } from \"../config/json-store.js\";\nimport { CONFIG_FILE } from \"../daemon/config.js\";\nimport { contextWindowFromModelId, DEFAULT_CONTEXT_WINDOW } from \"../utils/model-window.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Wire protocol a provider speaks: \"anthropic\" natively, \"openai\" through the\n * PAI proxy (which translates the Anthropic Messages API to Chat Completions).\n */\nexport type WorkerProtocol = \"anthropic\" | \"openai\";\n\n/** Runner executable behind a provider: Claude Code or the Codex CLI. */\nexport type WorkerEngine = \"claude\" | \"codex\";\n\nexport interface WorkerProvider {\n enabled: boolean;\n /** Wire protocol the baseUrl speaks. */\n protocol: WorkerProtocol;\n /** Anthropic-compatible Messages API base. */\n baseUrl: string;\n /** 0600 file holding the auth token; null for local servers (\"local\"). */\n keyFile: string | null;\n /** Model id per capability; see MODEL_CAPABILITIES. Only default is required. */\n models: { default: string } & Partial<Record<ModelCapability, string>>;\n /**\n * Model id → tier alias, overriding the built-in mapping (models.fast →\n * haiku tier, models.default → sonnet tier). Lets a provider pin an extra\n * model id to a tier, e.g. a heavyweight default to the opus tier.\n */\n modelTiers?: Record<string, ModelTier>;\n /** Extra environment variables for runs through this provider (string values). */\n env: Record<string, string>;\n note?: string;\n /**\n * OpenAI Chat Completions base (e.g. \"https://api.openai.com/v1\"). Required\n * for protocol \"openai\"; read by the PAI proxy, never by the runner.\n */\n upstreamUrl?: string;\n /** Runner for this provider; \"codex\" goes through the Codex CLI. */\n engine?: WorkerEngine;\n /** Optional shell command printing 0–100 (percent of quota used). */\n quotaProbe?: string;\n /** Auto-routing skips the provider at or above this percentage. Default 95. */\n quotaSkipAt?: number;\n /**\n * Context window of the provider's model, for the context meter. No\n * default: the meter only shows a window the init event announced (or\n * this explicit value, for engines without one, e.g. codex).\n */\n contextWindow?: number;\n /** Cost tier 1 (cheapest) … 5 (most expensive); classes cap it via maxCostTier. */\n costTier?: number;\n /** Capability tags; classes filter auto-routing via requireTags. */\n tags?: ProviderTag[];\n}\n\nexport interface WorkersPaneConfig {\n enabled: boolean;\n /** Font size (points) of the follow-pane profile's font. */\n fontSize: number;\n /** Seconds a pane lingers after its worker goes quiet. */\n autoExitSecs: number;\n}\n\nexport interface WorkersRoutingConfig {\n /** Provider names in preference order; first usable one wins. */\n order: string[];\n /** Minutes a provider sits in cooldown after a quota/rate failure. */\n cooldownMinutes: number;\n /** Reroute a failed-before-first-tool run to the next provider. */\n retryOnQuota: boolean;\n}\n\n/** Sub-worker caps: how deep the worker tree may grow, how wide per parent. */\nexport interface WorkersTreeConfig {\n /** Maximum nesting depth of sub-workers (top-level = 0). */\n maxDepth: number;\n /** Maximum concurrently running children per parent. */\n maxChildren: number;\n}\n\n/** Cost/quality tier of a provider's model, 1 (cheapest) … 5 (most expensive). */\nexport type CostTier = 1 | 2 | 3 | 4 | 5;\n\n/** The tier aliases the CLI and daemon tables understand — class proxies, not\n * Anthropic model names: any provider's model maps onto one of these. */\nexport type ModelTier = \"haiku\" | \"sonnet\" | \"opus\";\n\nconst MODEL_TIERS: readonly ModelTier[] = [\"haiku\", \"sonnet\", \"opus\"];\n\nexport const DEFAULT_COST_TIER = 3;\n\n/** Tags a provider may carry; classes filter auto-routing on them. */\nexport const PROVIDER_TAGS = [\n \"code\",\n \"vision\",\n \"image-gen\",\n \"long-context\",\n \"fast\",\n \"reasoning\",\n] as const;\n\nexport type ProviderTag = (typeof PROVIDER_TAGS)[number];\n\n/** The standard task classes; `workers.classes` maps each to a target. */\nexport const WORKER_CLASSES = [\n \"draft\",\n \"plan\",\n \"implement\",\n \"review\",\n \"research\",\n \"spotcheck\",\n \"simple\",\n \"complex\",\n \"image\",\n] as const;\n\nexport type WorkerClassName = (typeof WORKER_CLASSES)[number];\n\n/**\n * The named model capabilities a provider may carry a preference for, set\n * per provider under `workers.providers.<name>.models`. \"default\" is the\n * required catch-all; the others name what a model is *for* — \"fast\" the\n * cheap tier (spotcheck, haiku-tier spawns), \"image\" the image class.\n * Everything resolves through resolveModelCapability, falling back to default.\n */\nexport const MODEL_CAPABILITIES = [\"default\", \"fast\", \"image\"] as const;\n\nexport type ModelCapability = (typeof MODEL_CAPABILITIES)[number];\n\n/** Is this string the name of a known model capability? */\nexport function isModelCapability(v: string): v is ModelCapability {\n return (MODEL_CAPABILITIES as readonly string[]).includes(v);\n}\n\n/**\n * Which model capability a class runs on when its target names no alias: the\n * image class uses the image model; every other class the provider default.\n */\nconst CLASS_MODEL_CAPABILITY: Record<WorkerClassName, ModelCapability> = {\n draft: \"default\",\n plan: \"default\",\n implement: \"default\",\n review: \"default\",\n research: \"default\",\n spotcheck: \"default\",\n simple: \"default\",\n complex: \"default\",\n image: \"image\",\n};\n\n/** Capability for a run's class (default when unset or not a standard class). */\nexport function classModelCapability(className?: string): ModelCapability {\n return (className && CLASS_MODEL_CAPABILITY[className as WorkerClassName]) || \"default\";\n}\n\n/**\n * A class target: \"<provider>\", \"<provider>/<modelAlias>\", or an object. The\n * object either pins a `provider` (plus optional `mcp` allowlist) or only\n * constrains auto-routing (`maxCostTier`, `requireTags`, per-class `order`).\n */\nexport type ClassTarget =\n | string\n | {\n provider?: string;\n mcp?: string[];\n /** Auto-routing may only use providers at or below this cost tier. */\n maxCostTier?: number;\n /** Auto-routing may only use providers carrying all these tags. */\n requireTags?: string[];\n /** Provider order for this class; defaults to routing.order. */\n order?: string[];\n };\n\n/**\n * State of the machine-wide Claude Code fallback (`pai worker fallback on`):\n * every new Claude Code process runs on `provider` until `fallback off`.\n * `saved` holds what ~/.claude/settings.json carried before the switch so\n * `off` can restore it exactly.\n */\nexport interface WorkersFallback {\n /** Provider every new Claude Code process is pointed at. */\n provider: string;\n /**\n * settings.json before the switch. `env` records the previous value of\n * every env key fallback touched (null = the key was absent), `model` the\n * previous top-level model pin (null = none), `envExisted` whether an env\n * block existed at all.\n */\n saved: {\n env: Record<string, string | null>;\n model: string | null;\n envExisted: boolean;\n };\n /** ISO stamp of the switch (shown by `fallback status`). */\n on: string;\n}\n\nexport interface WorkersConfig {\n enabled: boolean;\n /** Provider name, or \"auto\" for routing.order resolution. */\n active: string | null;\n providers: Record<string, WorkerProvider>;\n /** Class → target; see ClassTarget. (Reads the legacy `roles` key.) */\n classes: Record<string, ClassTarget>;\n /** MCP set name → server names; `--mcp <set>` and class `mcp` expand these. */\n mcpSets: Record<string, string[]>;\n pane: WorkersPaneConfig;\n logDir: string;\n routing: WorkersRoutingConfig;\n /** Sub-worker caps (workers.tree). */\n tree: WorkersTreeConfig;\n /** Machine-wide Claude Code fallback; null = off. */\n fallback: WorkersFallback | null;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_LOG_DIR = \"~/.claude/logs/workers\";\n\nexport const DEFAULT_PANE: WorkersPaneConfig = {\n enabled: true,\n fontSize: 13,\n autoExitSecs: 60,\n};\n\nexport const DEFAULT_ROUTING: WorkersRoutingConfig = {\n order: [],\n cooldownMinutes: 30,\n retryOnQuota: true,\n};\n\nexport const DEFAULT_TREE: WorkersTreeConfig = {\n maxDepth: 2,\n maxChildren: 4,\n};\n\n/**\n * The default MCP sets every config starts from. `desktop` names the clickr\n * server so `--mcp desktop` hands a worker the machine controls (read-only\n * tools always; actuating ones after the operator hands the controls over,\n * see `pai worker controls`). A user's mcpSets section is merged over this,\n * so `desktop: []` removes the set deliberately.\n */\nexport const DEFAULT_MCP_SETS: Record<string, string[]> = {\n desktop: [\"clickr\"],\n};\n\nexport function defaultWorkersConfig(): WorkersConfig {\n return {\n enabled: false,\n active: null,\n providers: {},\n classes: {},\n mcpSets: { ...DEFAULT_MCP_SETS },\n pane: { ...DEFAULT_PANE },\n logDir: DEFAULT_LOG_DIR,\n routing: { ...DEFAULT_ROUTING, order: [] },\n tree: { ...DEFAULT_TREE },\n fallback: null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport class WorkersConfigError extends Error {}\n\nfunction bad(path: string, why: string): never {\n throw new WorkersConfigError(`workers${path}: ${why}`);\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : \"\";\n}\n\nfunction parseProvider(name: string, raw: unknown): WorkerProvider {\n if (typeof raw !== \"object\" || raw === null) bad(`.providers.${name}`, \"must be an object\");\n const p = raw as Record<string, unknown>;\n\n const protocol = p.protocol === undefined ? \"anthropic\" : str(p.protocol);\n if (protocol !== \"anthropic\" && protocol !== \"openai\") {\n bad(`.providers.${name}.protocol`, `\"${str(p.protocol)}\" is neither \"anthropic\" nor \"openai\"`);\n }\n const engine = p.engine === undefined ? \"claude\" : str(p.engine);\n if (engine !== \"claude\" && engine !== \"codex\") {\n bad(`.providers.${name}.engine`, `\"${str(p.engine)}\" is neither \"claude\" nor \"codex\"`);\n }\n\n const baseUrl = str(p.baseUrl);\n // openai providers reach the model through the PAI proxy; their baseUrl is\n // the proxy URL, filled in by the runner — only anthropic needs one here.\n if (!baseUrl && protocol !== \"openai\") bad(`.providers.${name}.baseUrl`, \"is required\");\n\n const keyFile =\n p.keyFile === undefined || p.keyFile === null || str(p.keyFile) === \"\"\n ? null\n : str(p.keyFile);\n\n const modelsRaw = p.models === undefined ? {} : p.models;\n if (typeof modelsRaw !== \"object\" || modelsRaw === null) {\n bad(`.providers.${name}.models`, \"must be an object\");\n }\n const m = modelsRaw as Record<string, unknown>;\n const defaultModel = str(m.default);\n if (!defaultModel) bad(`.providers.${name}.models.default`, \"is required\");\n const models: WorkerProvider[\"models\"] = { default: defaultModel };\n for (const key of Object.keys(m)) {\n if (key === \"default\") continue;\n if (!isModelCapability(key)) {\n bad(\n `.providers.${name}.models.${key}`,\n `\"${key}\" is not a model capability (from: ${MODEL_CAPABILITIES.join(\", \")})`\n );\n }\n const id = str(m[key]);\n if (!id) bad(`.providers.${name}.models.${key}`, \"must be a non-empty model id\");\n models[key] = id;\n }\n\n let modelTiers: Record<string, ModelTier> | undefined;\n if (p.modelTiers !== undefined) {\n if (typeof p.modelTiers !== \"object\" || p.modelTiers === null || Array.isArray(p.modelTiers)) {\n bad(`.providers.${name}.modelTiers`, \"must be an object of model id → haiku | sonnet | opus\");\n }\n modelTiers = {};\n for (const [id, tier] of Object.entries(p.modelTiers as Record<string, unknown>)) {\n if (typeof tier !== \"string\" || !MODEL_TIERS.includes(tier as ModelTier)) {\n bad(\n `.providers.${name}.modelTiers.${id}`,\n `must be \"haiku\", \"sonnet\" or \"opus\" (got ${JSON.stringify(tier)})`\n );\n }\n modelTiers[id] = tier as ModelTier;\n }\n }\n\n const env: Record<string, string> = {};\n if (p.env !== undefined) {\n if (typeof p.env !== \"object\" || p.env === null || Array.isArray(p.env)) {\n bad(`.providers.${name}.env`, \"must be an object of string values\");\n }\n for (const [k, v] of Object.entries(p.env as Record<string, unknown>)) {\n if (typeof v !== \"string\") bad(`.providers.${name}.env.${k}`, \"must be a string\");\n env[k] = v;\n }\n }\n\n const quotaSkipAt = p.quotaSkipAt === undefined ? undefined : p.quotaSkipAt;\n if (quotaSkipAt !== undefined) {\n if (typeof quotaSkipAt !== \"number\" || quotaSkipAt < 0 || quotaSkipAt > 100) {\n bad(`.providers.${name}.quotaSkipAt`, \"must be a number between 0 and 100\");\n }\n }\n\n const contextWindow = p.contextWindow === undefined ? undefined : p.contextWindow;\n if (contextWindow !== undefined) {\n if (typeof contextWindow !== \"number\" || contextWindow <= 0) {\n bad(`.providers.${name}.contextWindow`, \"must be a positive number of tokens\");\n }\n }\n\n const upstreamUrl = str(p.upstreamUrl);\n if (protocol === \"openai\" && !upstreamUrl) {\n bad(`.providers.${name}.upstreamUrl`, `is required for protocol \"openai\" (the Chat Completions base, e.g. \"https://api.openai.com/v1\")`);\n }\n\n const costTier = p.costTier === undefined ? undefined : p.costTier;\n if (costTier !== undefined) {\n if (typeof costTier !== \"number\" || !Number.isInteger(costTier) || costTier < 1 || costTier > 5) {\n bad(`.providers.${name}.costTier`, \"must be an integer 1 (cheapest) … 5 (most expensive)\");\n }\n }\n\n let tags: ProviderTag[] | undefined;\n if (p.tags !== undefined) {\n if (!Array.isArray(p.tags) || p.tags.some((x) => typeof x !== \"string\")) {\n bad(`.providers.${name}.tags`, `must be an array of tags from: ${PROVIDER_TAGS.join(\", \")}`);\n }\n for (const t of p.tags as string[]) {\n if (!(PROVIDER_TAGS as readonly string[]).includes(t)) {\n bad(`.providers.${name}.tags`, `\"${t}\" is not a tag (from: ${PROVIDER_TAGS.join(\", \")})`);\n }\n }\n tags = p.tags as ProviderTag[];\n }\n\n return {\n enabled: p.enabled === undefined ? true : p.enabled === true,\n protocol,\n baseUrl,\n keyFile,\n models,\n ...(modelTiers ? { modelTiers } : {}),\n env,\n ...(str(p.note) ? { note: str(p.note) } : {}),\n ...(upstreamUrl ? { upstreamUrl } : {}),\n ...(engine !== \"claude\" ? { engine } : {}),\n ...(str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {}),\n ...(quotaSkipAt !== undefined ? { quotaSkipAt } : {}),\n ...(contextWindow !== undefined ? { contextWindow } : {}),\n ...(costTier !== undefined ? { costTier } : {}),\n ...(tags ? { tags } : {}),\n };\n}\n\n/**\n * Parse and validate a raw `workers` value. Missing section → defaults.\n * Unknown-but-typed garbage → WorkersConfigError naming the offending field.\n */\nexport function parseWorkersConfig(raw: unknown): WorkersConfig {\n const d = defaultWorkersConfig();\n if (raw === undefined || raw === null) return d;\n if (typeof raw !== \"object\" || Array.isArray(raw)) {\n bad(\"\", \"section must be an object\");\n }\n const w = raw as Record<string, unknown>;\n\n const providers: Record<string, WorkerProvider> = {};\n if (w.providers !== undefined) {\n if (typeof w.providers !== \"object\" || w.providers === null || Array.isArray(w.providers)) {\n bad(\".providers\", \"must be an object keyed by provider name\");\n }\n for (const [name, p] of Object.entries(w.providers)) {\n providers[name] = parseProvider(name, p);\n }\n }\n\n // classes is canonical; a config that still carries the pre-classes `roles`\n // key is migrated by reading it here — the next write stores only `classes`.\n const classes: Record<string, ClassTarget> = {};\n const classesRaw = w.classes !== undefined ? w.classes : w.roles;\n if (classesRaw !== undefined) {\n if (typeof classesRaw !== \"object\" || classesRaw === null || Array.isArray(classesRaw)) {\n bad(w.classes !== undefined ? \".classes\" : \".roles\", \"must be an object of class → provider[/alias] or {provider, mcp, maxCostTier, requireTags, order}\");\n }\n for (const [cls, target] of Object.entries(classesRaw)) {\n if (typeof target === \"object\" && target !== null && !Array.isArray(target)) {\n const o = target as Record<string, unknown>;\n const provider = str(o.provider);\n if (o.provider !== undefined && (!provider || provider.includes(\" \"))) {\n bad(`.classes.${cls}.provider`, `invalid provider \"${provider}\"`);\n }\n let mcp: string[] | undefined;\n if (o.mcp !== undefined) {\n if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== \"string\")) {\n bad(`.classes.${cls}.mcp`, \"must be an array of MCP server or set names\");\n }\n mcp = o.mcp as string[];\n }\n let maxCostTier: number | undefined;\n if (o.maxCostTier !== undefined) {\n if (\n typeof o.maxCostTier !== \"number\" ||\n !Number.isInteger(o.maxCostTier) ||\n o.maxCostTier < 1 ||\n o.maxCostTier > 5\n ) {\n bad(`.classes.${cls}.maxCostTier`, \"must be an integer 1 … 5\");\n }\n maxCostTier = o.maxCostTier;\n }\n let requireTags: string[] | undefined;\n if (o.requireTags !== undefined) {\n if (!Array.isArray(o.requireTags) || o.requireTags.some((x) => typeof x !== \"string\")) {\n bad(`.classes.${cls}.requireTags`, `must be an array of tags from: ${PROVIDER_TAGS.join(\", \")}`);\n }\n for (const t of o.requireTags as string[]) {\n if (!(PROVIDER_TAGS as readonly string[]).includes(t)) {\n bad(`.classes.${cls}.requireTags`, `\"${t}\" is not a tag (from: ${PROVIDER_TAGS.join(\", \")})`);\n }\n }\n requireTags = o.requireTags as string[];\n }\n let order: string[] | undefined;\n if (o.order !== undefined) {\n if (!Array.isArray(o.order) || o.order.some((x) => typeof x !== \"string\")) {\n bad(`.classes.${cls}.order`, \"must be an array of provider names\");\n }\n order = o.order as string[];\n }\n const obj: ClassTarget = {\n ...(provider ? { provider } : {}),\n ...(mcp ? { mcp } : {}),\n ...(maxCostTier !== undefined ? { maxCostTier } : {}),\n ...(requireTags ? { requireTags } : {}),\n ...(order ? { order } : {}),\n };\n classes[cls] = Object.keys(obj).length ? obj : {};\n } else {\n const t = str(target);\n if (!t || t.includes(\" \")) bad(`.classes.${cls}`, `invalid target \"${t}\"`);\n classes[cls] = t;\n }\n }\n }\n\n const mcpSets: Record<string, string[]> = { ...DEFAULT_MCP_SETS };\n if (w.mcpSets !== undefined) {\n if (typeof w.mcpSets !== \"object\" || w.mcpSets === null || Array.isArray(w.mcpSets)) {\n bad(\".mcpSets\", \"must be an object of set name → [server names]\");\n }\n for (const [setName, servers] of Object.entries(w.mcpSets)) {\n if (!Array.isArray(servers) || servers.some((x) => typeof x !== \"string\")) {\n bad(`.mcpSets.${setName}`, \"must be an array of MCP server names\");\n }\n mcpSets[setName] = servers as string[];\n }\n }\n\n let pane = { ...DEFAULT_PANE };\n if (w.pane !== undefined) {\n if (typeof w.pane !== \"object\" || w.pane === null) bad(\".pane\", \"must be an object\");\n const pc = w.pane as Record<string, unknown>;\n if (pc.enabled !== undefined && typeof pc.enabled !== \"boolean\") bad(\".pane.enabled\", \"must be boolean\");\n if (pc.fontSize !== undefined && (typeof pc.fontSize !== \"number\" || pc.fontSize <= 0)) {\n bad(\".pane.fontSize\", \"must be a positive number of points\");\n }\n if (pc.autoExitSecs !== undefined && typeof pc.autoExitSecs !== \"number\") {\n bad(\".pane.autoExitSecs\", \"must be a number\");\n }\n // legacy fontScale (a relative scale, superseded by fontSize) is tolerated and ignored\n pane = {\n enabled: pc.enabled === undefined ? DEFAULT_PANE.enabled : pc.enabled === true,\n fontSize: pc.fontSize === undefined ? DEFAULT_PANE.fontSize : pc.fontSize,\n autoExitSecs: pc.autoExitSecs === undefined ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs,\n };\n }\n\n let routing = { ...DEFAULT_ROUTING, order: [] as string[] };\n if (w.routing !== undefined) {\n if (typeof w.routing !== \"object\" || w.routing === null) bad(\".routing\", \"must be an object\");\n const r = w.routing as Record<string, unknown>;\n if (r.order !== undefined) {\n if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== \"string\")) {\n bad(\".routing.order\", \"must be an array of provider names\");\n }\n routing.order = r.order as string[];\n }\n if (r.cooldownMinutes !== undefined && typeof r.cooldownMinutes !== \"number\") {\n bad(\".routing.cooldownMinutes\", \"must be a number\");\n }\n if (r.retryOnQuota !== undefined && typeof r.retryOnQuota !== \"boolean\") {\n bad(\".routing.retryOnQuota\", \"must be boolean\");\n }\n routing = {\n order: routing.order,\n cooldownMinutes: r.cooldownMinutes === undefined ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,\n retryOnQuota: r.retryOnQuota === undefined ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true,\n };\n }\n\n let tree = { ...DEFAULT_TREE };\n if (w.tree !== undefined) {\n if (typeof w.tree !== \"object\" || w.tree === null || Array.isArray(w.tree)) {\n bad(\".tree\", \"must be an object\");\n }\n const t = w.tree as Record<string, unknown>;\n if (t.maxDepth !== undefined) {\n if (typeof t.maxDepth !== \"number\" || !Number.isInteger(t.maxDepth) || t.maxDepth < 0) {\n bad(\".tree.maxDepth\", \"must be a non-negative integer\");\n }\n }\n if (t.maxChildren !== undefined) {\n if (typeof t.maxChildren !== \"number\" || !Number.isInteger(t.maxChildren) || t.maxChildren < 1) {\n bad(\".tree.maxChildren\", \"must be a positive integer\");\n }\n }\n tree = {\n maxDepth: t.maxDepth === undefined ? DEFAULT_TREE.maxDepth : t.maxDepth,\n maxChildren: t.maxChildren === undefined ? DEFAULT_TREE.maxChildren : t.maxChildren,\n };\n }\n\n const active = w.active === undefined || w.active === null ? null : str(w.active);\n if (active !== null && active !== \"auto\" && !(active in providers)) {\n // Tolerated at parse time (a provider may have been removed while active\n // still names it) but every consumer resolves it to a clear error.\n }\n\n let fallback: WorkersFallback | null = null;\n if (w.fallback !== undefined && w.fallback !== null) {\n if (typeof w.fallback !== \"object\" || Array.isArray(w.fallback)) {\n bad(\".fallback\", \"must be an object (written by `pai worker fallback on`)\");\n }\n const f = w.fallback as Record<string, unknown>;\n const provider = str(f.provider);\n if (!provider) bad(\".fallback.provider\", \"is required\");\n const savedRaw = f.saved;\n if (typeof savedRaw !== \"object\" || savedRaw === null || Array.isArray(savedRaw)) {\n bad(\".fallback.saved\", \"must be an object\");\n }\n const s = savedRaw as Record<string, unknown>;\n if (typeof s.env !== \"object\" || s.env === null || Array.isArray(s.env)) {\n bad(\".fallback.saved.env\", \"must be an object of env key → previous value or null\");\n }\n const env: Record<string, string | null> = {};\n for (const [k, v] of Object.entries(s.env as Record<string, unknown>)) {\n env[k] = v === null || typeof v === \"string\" ? (v as string | null) : null;\n }\n if (s.model !== null && s.model !== undefined && typeof s.model !== \"string\") {\n bad(\".fallback.saved.model\", \"must be a string or null\");\n }\n fallback = {\n provider,\n saved: {\n env,\n model: typeof s.model === \"string\" ? s.model : null,\n envExisted: s.envExisted === true,\n },\n on: str(f.on) || new Date(0).toISOString(),\n };\n }\n\n return {\n enabled: w.enabled === undefined ? d.enabled : w.enabled === true,\n active,\n providers,\n classes,\n mcpSets,\n pane,\n logDir: str(w.logDir) || d.logDir,\n routing,\n tree,\n fallback,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\n/**\n * Read the whole config file and return (raw, workers) — the raw record so\n * callers can rewrite it preserving every other section, the parsed+validated\n * workers section. Unreadable config throws (readJsonStrict), missing is fine.\n * `path` overrides the config location (tests, CLAUDE_SETTINGS_PATH-style\n * dry runs); default ~/.config/pai/config.json.\n */\nexport function readWorkersSection(path: string = CONFIG_FILE): {\n raw: Record<string, unknown>;\n workers: WorkersConfig;\n} {\n const raw = readJsonStrict(path, \"~/.config/pai/config.json\");\n return { raw, workers: parseWorkersConfig(raw.workers) };\n}\n\n/** Write the workers section back into the config file, atomically. */\nexport function writeWorkersSection(\n raw: Record<string, unknown>,\n workers: WorkersConfig,\n path: string = CONFIG_FILE\n): void {\n // null fallback (off) is omitted rather than written, so a config that\n // never used it does not gain a `\"fallback\": null` line from an unrelated\n // worker mutation.\n raw.workers = workers.fallback ? workers : { ...workers, fallback: undefined };\n writeJsonAtomic(path, raw, { label: \"~/.config/pai/config.json\" });\n}\n\n/** Expand a leading ~ (config values are written with `~` to stay portable). */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\")) return join(homedir(), p.slice(1));\n return p;\n}\n\n// ---------------------------------------------------------------------------\n// Runnability checks (proxy / codex providers included)\n// ---------------------------------------------------------------------------\n\nexport function assertProviderRunnable(name: string, p: WorkerProvider): void {\n if (p.protocol === \"openai\" && !p.upstreamUrl) {\n throw new WorkersConfigError(\n `provider \"${name}\" uses protocol \"openai\" but has no upstreamUrl — ` +\n `set its Chat Completions base (e.g. \"https://api.openai.com/v1\") ` +\n `so the PAI proxy knows where to translate to.`\n );\n }\n}\n\n/** Where a provider's key file lives, or null. Shared by run + test + add. */\nexport function providerKeyPath(p: WorkerProvider): string | null {\n return p.keyFile ? expandHome(p.keyFile) : null;\n}\n\nexport { DEFAULT_CONTEXT_WINDOW } from \"../utils/model-window.js\";\n\n/** Cost tier of a provider for class filtering (unset = 3, the middle). */\nexport function providerCostTier(p: WorkerProvider): number {\n return p.costTier ?? DEFAULT_COST_TIER;\n}\n\n/**\n * The model id for a capability: the provider's preference for it, else its\n * default model — the one resolution rule every consumer (image class, fast\n * tier spawns, env pins) goes through.\n */\nexport function resolveModelCapability(p: WorkerProvider, capability: ModelCapability): string {\n return p.models[capability] ?? p.models.default;\n}\n\n/**\n * Context window used by the meter when the run reports none: an explicit\n * `contextWindow` first, then whatever the default model's id declares\n * (\"[1m]\" → 1,000,000), then the last-resort default.\n */\nexport function providerContextWindow(p: WorkerProvider): number {\n return (\n p.contextWindow ??\n contextWindowFromModelId(p.models.default) ??\n DEFAULT_CONTEXT_WINDOW\n );\n}\n\n/** Directory under which inline keys (MCP `key` field) are stored, 0600. */\nexport function keysDir(): string {\n return join(homedir(), \".config\", \"pai\", \"keys\");\n}\n"],"mappings":"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"}