@tekmidian/pai 0.49.0 → 0.51.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.
- package/dist/{aibroker-client-D5NT0S1N.mjs → aibroker-client-Bu72yPKj.mjs} +1 -1
- package/dist/{aibroker-client-Bn9SnGMa.mjs → aibroker-client-CDIXmq-e.mjs} +1 -1
- package/dist/{aibroker-client-Bn9SnGMa.mjs.map → aibroker-client-CDIXmq-e.mjs.map} +1 -1
- package/dist/{chain-CNSa0A3G.mjs → chain-BBbVbWVv.mjs} +4 -4
- package/dist/{chain-CNSa0A3G.mjs.map → chain-BBbVbWVv.mjs.map} +1 -1
- package/dist/cli/index.mjs +14 -14
- package/dist/cli/program.mjs +14 -14
- package/dist/config-Bn9jpblO.mjs +653 -0
- package/dist/config-Bn9jpblO.mjs.map +1 -0
- package/dist/{config-DxMZjpvY.mjs → config-DYXARgk8.mjs} +180 -151
- package/dist/config-DYXARgk8.mjs.map +1 -0
- package/dist/config-RjeVGJ7K.mjs +3 -0
- package/dist/{context-handover-cache-B9NKSjwR.mjs → context-handover-cache-yOuK9RIk.mjs} +5 -5
- package/dist/{context-handover-cache-B9NKSjwR.mjs.map → context-handover-cache-yOuK9RIk.mjs.map} +1 -1
- package/dist/daemon/index.mjs +13 -12
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-DZ4Q0jrs.mjs → daemon-CBTQYtHF.mjs} +49 -30
- package/dist/daemon-CBTQYtHF.mjs.map +1 -0
- package/dist/daemon-Vf7pHnjR.mjs +25 -0
- package/dist/daemon-mcp/index.mjs +186 -23
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/factory-Ba8j9K7q.mjs +3 -0
- package/dist/{factory-DNYAqjH0.mjs → factory-Bt7TWQje.mjs} +3 -3
- package/dist/{factory-DNYAqjH0.mjs.map → factory-Bt7TWQje.mjs.map} +1 -1
- package/dist/{fallback-IeR2FrFn.mjs → fallback-CIGspCSi.mjs} +260 -10
- package/dist/fallback-CIGspCSi.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +80 -8
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +54 -39
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs +54 -39
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +204 -109
- package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
- package/dist/hooks/whisper-rules.mjs +7613 -9
- package/dist/hooks/whisper-rules.mjs.map +4 -4
- package/dist/hooks/worker-proxy.mjs +204 -109
- package/dist/hooks/worker-proxy.mjs.map +4 -4
- package/dist/hooks/worker-status-line.mjs +211 -116
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +208 -113
- package/dist/hooks/worker-supervision.mjs.map +4 -4
- package/dist/{kg-entity-BSjM5xZh.mjs → kg-entity-BSW7ra8i.mjs} +1 -1
- package/dist/{kg-entity-BSjM5xZh.mjs.map → kg-entity-BSW7ra8i.mjs.map} +1 -1
- package/dist/main-resolver-BTastpp4.mjs +7 -0
- package/dist/{main-resolver-DvYu9jbl.mjs → main-resolver-BmZPCmgj.mjs} +3 -3
- package/dist/{main-resolver-DvYu9jbl.mjs.map → main-resolver-BmZPCmgj.mjs.map} +1 -1
- package/dist/{planner-Cn8HmT7l.mjs → planner-BiH9msqq.mjs} +6 -6
- package/dist/{planner-Cn8HmT7l.mjs.map → planner-BiH9msqq.mjs.map} +1 -1
- package/dist/{program-D9-wfmkZ.mjs → program-srUeFUfW.mjs} +5806 -5615
- package/dist/program-srUeFUfW.mjs.map +1 -0
- package/dist/query-feedback-Bm6XvCmg.mjs +3 -0
- package/dist/{query-feedback-CYyOGi3y.mjs → query-feedback-CTs_jx6i.mjs} +1 -1
- package/dist/{query-feedback-CYyOGi3y.mjs.map → query-feedback-CTs_jx6i.mjs.map} +1 -1
- package/dist/{run-Cls0YGgV.mjs → run-TChOnMO_.mjs} +1388 -1183
- package/dist/run-TChOnMO_.mjs.map +1 -0
- package/dist/{server-Ch99xt5K.mjs → server-R9UkCn3v.mjs} +42 -3
- package/dist/server-R9UkCn3v.mjs.map +1 -0
- package/dist/session-keepalive-BuG3wvik.mjs +579 -0
- package/dist/session-keepalive-BuG3wvik.mjs.map +1 -0
- package/dist/skills/VaultContext/SKILL.md +1 -1
- package/dist/{state-DnYGRmS7.mjs → state-Ca0ltLgI.mjs} +1 -1
- package/dist/{state-PJrgfDLW.mjs → state-DwrsOoMi.mjs} +1 -1
- package/dist/{state-PJrgfDLW.mjs.map → state-DwrsOoMi.mjs.map} +1 -1
- package/dist/{tools-B77WHeVZ.mjs → tools-BEqiJt_j.mjs} +13 -13
- package/dist/{tools-B77WHeVZ.mjs.map → tools-BEqiJt_j.mjs.map} +1 -1
- package/dist/{tools-NnR_n-Ja.mjs → tools-BwsOTmjh.mjs} +2 -2
- package/dist/{wakeup-UrhiW-KU.mjs → wakeup-Yk5lQ0jb.mjs} +1 -1
- package/dist/{wakeup-UrhiW-KU.mjs.map → wakeup-Yk5lQ0jb.mjs.map} +1 -1
- package/dist/{work-queue-worker-BA5IDBwQ.mjs → work-queue-worker-B9zVWImP.mjs} +3 -3
- package/dist/{work-queue-worker-BA5IDBwQ.mjs.map → work-queue-worker-B9zVWImP.mjs.map} +1 -1
- package/dist/work-queue-worker-D-DnQ7oh.mjs +14 -0
- package/dist/{zettelkasten-BBjA7GcP.mjs → zettelkasten-DezbXghl.mjs} +2 -2
- package/dist/{zettelkasten-BBjA7GcP.mjs.map → zettelkasten-DezbXghl.mjs.map} +1 -1
- package/docs/cache-keepalive.md +138 -0
- package/docs/commands/README.md +7 -0
- package/docs/commands/audit.md +1 -1
- package/docs/commands/config.md +84 -0
- package/docs/commands/daemon.md +6 -0
- package/docs/commands/worker.md +37 -2
- package/docs/config.md +171 -0
- package/docs/model-catalogue.md +502 -0
- package/docs/provider-independence-details.md +1 -1
- package/docs/task-bus.md +1 -1
- package/docs/worker.md +33 -5
- package/docs/workers-config.md +105 -12
- package/package.json +1 -1
- package/src/hooks/ts/capture-all-events.test.ts +71 -0
- package/src/hooks/ts/capture-all-events.ts +10 -3
- package/src/hooks/ts/lib/context-fill.test.ts +124 -46
- package/src/hooks/ts/lib/context-fill.ts +57 -14
- package/src/hooks/ts/user-prompt/whisper-rules.test.ts +56 -1
- package/src/hooks/ts/user-prompt/whisper-rules.ts +24 -0
- package/statusline-command.sh +103 -16
- package/dist/config-DCu8THKi.mjs +0 -228
- package/dist/config-DCu8THKi.mjs.map +0 -1
- package/dist/config-DeqFqa1N.mjs +0 -3
- package/dist/config-DxMZjpvY.mjs.map +0 -1
- package/dist/daemon-CYpVy_Dw.mjs +0 -24
- package/dist/daemon-DZ4Q0jrs.mjs.map +0 -1
- package/dist/factory-47Qf_df7.mjs +0 -3
- package/dist/fallback-IeR2FrFn.mjs.map +0 -1
- package/dist/indexer-C5Le7XE7.mjs +0 -1
- package/dist/main-resolver-CnfnWa8C.mjs +0 -7
- package/dist/program-D9-wfmkZ.mjs.map +0 -1
- package/dist/query-feedback-C7rSfiXU.mjs +0 -3
- package/dist/run-Cls0YGgV.mjs.map +0 -1
- package/dist/server-Ch99xt5K.mjs.map +0 -1
- package/dist/work-queue-worker-DLM2wlqM.mjs +0 -14
|
@@ -1,119 +1,10 @@
|
|
|
1
1
|
import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-ChAOupZc.mjs";
|
|
2
|
-
import { n as CONFIG_FILE } from "./config-
|
|
3
|
-
import { chmodSync,
|
|
2
|
+
import { _ as writeYamlFileAtomic, d as readMainConfigRaw, f as writeMainConfigRaw, g as YamlStoreError, n as CONFIG_FILE, v as readJsonStrict, y as writeJsonAtomic } from "./config-Bn9jpblO.mjs";
|
|
3
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { Document, Scalar, parseDocument } from "yaml";
|
|
7
7
|
|
|
8
|
-
//#region src/config/json-store.ts
|
|
9
|
-
/**
|
|
10
|
-
* json-store.ts — read/write JSON config files without destroying them
|
|
11
|
-
*
|
|
12
|
-
* The failure this exists to prevent:
|
|
13
|
-
*
|
|
14
|
-
* try { return JSON.parse(read(path)); } catch { return {}; }
|
|
15
|
-
* ... later ...
|
|
16
|
-
* write(path, JSON.stringify(ourData));
|
|
17
|
-
*
|
|
18
|
-
* An unreadable file becomes an empty object, and the next write makes that
|
|
19
|
-
* permanent. Silently, exit code 0. This shape appeared three times in this
|
|
20
|
-
* repo against three different files, and twice in AIBroker.
|
|
21
|
-
*
|
|
22
|
-
* The distinction that matters is between *missing* and *unreadable*:
|
|
23
|
-
*
|
|
24
|
-
* missing — legitimate first run. Start fresh; writing is safe.
|
|
25
|
-
* unreadable — the file exists and we could not parse it. Those bytes are
|
|
26
|
-
* the only copy of something. Never overwrite them.
|
|
27
|
-
*
|
|
28
|
-
* Collapsing the second into the first is the bug.
|
|
29
|
-
*
|
|
30
|
-
* NOT everything deserves this guard. For a transient buffer — an undelivered
|
|
31
|
-
* message queue, a cache — starting fresh IS the correct recovery, and
|
|
32
|
-
* refusing to write would disable the feature permanently. Use `writeJsonAtomic`
|
|
33
|
-
* alone there: it still prevents a crash from truncating a good file, without
|
|
34
|
-
* blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* Read a JSON file, distinguishing "absent" from "damaged".
|
|
38
|
-
*
|
|
39
|
-
* @param path file to read
|
|
40
|
-
* @param label how to name it to the user, e.g. "~/.claude.json"
|
|
41
|
-
* @throws if the file exists but cannot be read or parsed
|
|
42
|
-
*/
|
|
43
|
-
function readJsonStrict(path, label = path) {
|
|
44
|
-
if (!existsSync(path)) return {};
|
|
45
|
-
let raw;
|
|
46
|
-
try {
|
|
47
|
-
raw = readFileSync(path, "utf8");
|
|
48
|
-
} catch (e) {
|
|
49
|
-
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.`);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(raw);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
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.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Leaf values that mean "type word", not data.
|
|
59
|
-
*
|
|
60
|
-
* A config whose top-level keys map to bare type words is a serialized schema,
|
|
61
|
-
* not a configuration — the exact shape found over the live config on
|
|
62
|
-
* 2026-09-18 (399 bytes: `socketPath: string`, `indexIntervalSecs: int`, …).
|
|
63
|
-
* No real config carries three of these at its top level; a dump carries
|
|
64
|
-
* nothing else.
|
|
65
|
-
*/
|
|
66
|
-
const SCHEMA_TYPE_WORDS = new Set([
|
|
67
|
-
"string",
|
|
68
|
-
"int",
|
|
69
|
-
"integer",
|
|
70
|
-
"number",
|
|
71
|
-
"bool",
|
|
72
|
-
"boolean",
|
|
73
|
-
"float",
|
|
74
|
-
"double",
|
|
75
|
-
"object",
|
|
76
|
-
"array",
|
|
77
|
-
"any",
|
|
78
|
-
"unknown"
|
|
79
|
-
]);
|
|
80
|
-
function schemaDumpHits(data) {
|
|
81
|
-
let hits = 0;
|
|
82
|
-
for (const v of Object.values(data)) if (typeof v === "string" && SCHEMA_TYPE_WORDS.has(v.trim())) hits += 1;
|
|
83
|
-
return hits;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Write JSON without risking the existing file.
|
|
87
|
-
*
|
|
88
|
-
* Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file
|
|
89
|
-
* and renames. Rename is atomic within a filesystem, so a crash mid-write
|
|
90
|
-
* leaves the original intact rather than truncated — which is how these files
|
|
91
|
-
* become corrupt in the first place.
|
|
92
|
-
*/
|
|
93
|
-
function writeJsonAtomic(path, data, opts = {}) {
|
|
94
|
-
const { backup = true, label = path } = opts;
|
|
95
|
-
if (schemaDumpHits(data) >= 3) throw new Error(`${label}: refusing to write — the object looks like a type schema, not values (several top-level keys map to bare type words). That is the corruption signature of 2026-09-18: the caller serialized the schema instead of the config. Nothing was written.`);
|
|
96
|
-
const serialized = JSON.stringify(data, null, 2) + "\n";
|
|
97
|
-
const dir = dirname(path);
|
|
98
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
99
|
-
if (backup && existsSync(path)) try {
|
|
100
|
-
copyFileSync(path, `${path}.bak-pai`);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
throw new Error(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
|
|
103
|
-
}
|
|
104
|
-
const tmp = `${path}.tmp-pai-${process.pid}`;
|
|
105
|
-
try {
|
|
106
|
-
writeFileSync(tmp, serialized, "utf8");
|
|
107
|
-
renameSync(tmp, path);
|
|
108
|
-
} catch (e) {
|
|
109
|
-
try {
|
|
110
|
-
if (existsSync(tmp)) unlinkSync(tmp);
|
|
111
|
-
} catch {}
|
|
112
|
-
throw new Error(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged.`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
//#endregion
|
|
117
8
|
//#region src/utils/model-window.ts
|
|
118
9
|
/**
|
|
119
10
|
* model-window.ts — facts derivable from a bare model id.
|
|
@@ -321,11 +212,21 @@ function parseWorkersYamlDocument(doc, yamlPath) {
|
|
|
321
212
|
const provider = typeof target === "string" ? target.split("/")[0] : target.provider;
|
|
322
213
|
const alias = typeof target === "string" ? target.split("/")[1] : void 0;
|
|
323
214
|
if (provider !== void 0) {
|
|
324
|
-
|
|
325
|
-
if (
|
|
215
|
+
const native = provider === ANTHROPIC_NATIVE;
|
|
216
|
+
if (!native && !providers[provider]) withLine(doc, ["classes", cls], yamlPath, new WorkersConfigError(`classes.${cls}: no provider named "${provider}" (configured: ${Object.keys(providers).join(", ") || "(none)"})`));
|
|
217
|
+
if (alias !== void 0) {
|
|
218
|
+
if (!isModelCapability(alias)) withLine(doc, ["classes", cls], yamlPath, new WorkersConfigError(`classes.${cls}: "${alias}" is not a valid capability name (must match ^[a-z][a-z0-9-]*$)`));
|
|
219
|
+
else if (!native && providers[provider] && alias !== "default" && !providers[provider].models[alias]) withLine(doc, ["classes", cls], yamlPath, new WorkersConfigError(`classes.${cls}: provider "${provider}" has no "${alias}" model configured`));
|
|
220
|
+
}
|
|
326
221
|
}
|
|
327
222
|
}
|
|
328
223
|
}
|
|
224
|
+
let capabilities = {};
|
|
225
|
+
if (r.capabilities !== void 0) try {
|
|
226
|
+
capabilities = parseCapabilitiesValue(r.capabilities, "capabilities");
|
|
227
|
+
} catch (e) {
|
|
228
|
+
withLine(doc, ["capabilities"], yamlPath, e);
|
|
229
|
+
}
|
|
329
230
|
let mcpSets = {};
|
|
330
231
|
if (r.mcp_sets !== void 0) try {
|
|
331
232
|
mcpSets = parseMcpSetsValue(r.mcp_sets, "mcp_sets");
|
|
@@ -339,6 +240,7 @@ function parseWorkersYamlDocument(doc, yamlPath) {
|
|
|
339
240
|
active,
|
|
340
241
|
providers,
|
|
341
242
|
classes,
|
|
243
|
+
capabilities,
|
|
342
244
|
mcpSets,
|
|
343
245
|
nativeModels
|
|
344
246
|
};
|
|
@@ -392,6 +294,8 @@ function syncWorkersYamlDocument(doc, before, after) {
|
|
|
392
294
|
for (const name of Object.keys(before.providers)) if (!(name in after.providers)) doc.deleteIn(["providers", name]);
|
|
393
295
|
for (const [cls, target] of Object.entries(after.classes)) if (!shallowEqual(before.classes[cls], target)) doc.setIn(["classes", cls], target);
|
|
394
296
|
for (const cls of Object.keys(before.classes)) if (!(cls in after.classes)) doc.deleteIn(["classes", cls]);
|
|
297
|
+
for (const [cap, prefs] of Object.entries(after.capabilities)) if (!shallowEqual(before.capabilities[cap], prefs)) doc.setIn(["capabilities", cap], [...prefs]);
|
|
298
|
+
for (const cap of Object.keys(before.capabilities)) if (!(cap in after.capabilities)) doc.deleteIn(["capabilities", cap]);
|
|
395
299
|
for (const [set, servers] of Object.entries(after.mcpSets)) if (!shallowEqual(before.mcpSets[set], servers)) doc.setIn(["mcp_sets", set], [...servers]);
|
|
396
300
|
for (const set of Object.keys(before.mcpSets)) if (!(set in after.mcpSets)) doc.deleteIn(["mcp_sets", set]);
|
|
397
301
|
if (before.active !== after.active) if (after.active === null) doc.deleteIn(["active"]);
|
|
@@ -410,12 +314,16 @@ function writeWorkersYaml(yamlPath, after) {
|
|
|
410
314
|
active: null,
|
|
411
315
|
providers: {},
|
|
412
316
|
classes: {},
|
|
317
|
+
capabilities: {},
|
|
413
318
|
mcpSets: {},
|
|
414
319
|
nativeModels: { ...NATIVE_ANTHROPIC_MODELS }
|
|
415
320
|
}, after);
|
|
416
321
|
writeWorkersYamlText(yamlPath, String(doc));
|
|
417
322
|
}
|
|
418
|
-
/** Atomic write with a .bak-pai backup and a re-validate-or-restore guard.
|
|
323
|
+
/** Atomic write with a .bak-pai backup and a re-validate-or-restore guard.
|
|
324
|
+
* The file-I/O tail (backup, temp write at 0600, rename, chmod) is shared
|
|
325
|
+
* with every other PAI YAML file via writeYamlFileAtomic; only the
|
|
326
|
+
* workers.yaml-specific validation (parseWorkersYamlDocument) lives here. */
|
|
419
327
|
function writeWorkersYamlText(yamlPath, text) {
|
|
420
328
|
const probe = parseDocument(text);
|
|
421
329
|
if (probe.errors.length) throw new WorkersConfigError(`refusing to write ${yamlPath}: ${probe.errors[0].message} (previous file left unchanged)`);
|
|
@@ -424,26 +332,10 @@ function writeWorkersYamlText(yamlPath, text) {
|
|
|
424
332
|
} catch (e) {
|
|
425
333
|
throw new WorkersConfigError(`refusing to write ${yamlPath}: ${e instanceof Error ? e.message : String(e)} (previous file left unchanged)`);
|
|
426
334
|
}
|
|
427
|
-
const dir = dirname(yamlPath);
|
|
428
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
429
|
-
if (existsSync(yamlPath)) try {
|
|
430
|
-
copyFileSync(yamlPath, `${yamlPath}.bak-pai`);
|
|
431
|
-
} catch (e) {
|
|
432
|
-
throw new WorkersConfigError(`Could not back up ${yamlPath}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
|
|
433
|
-
}
|
|
434
|
-
const tmp = `${yamlPath}.tmp-pai-${process.pid}`;
|
|
435
335
|
try {
|
|
436
|
-
|
|
437
|
-
encoding: "utf8",
|
|
438
|
-
mode: 384
|
|
439
|
-
});
|
|
440
|
-
renameSync(tmp, yamlPath);
|
|
441
|
-
chmodSync(yamlPath, 384);
|
|
336
|
+
writeYamlFileAtomic(yamlPath, text, { label: yamlPath });
|
|
442
337
|
} catch (e) {
|
|
443
|
-
|
|
444
|
-
if (existsSync(tmp)) unlinkSync(tmp);
|
|
445
|
-
} catch {}
|
|
446
|
-
throw new WorkersConfigError(`Failed to write ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
338
|
+
throw new WorkersConfigError(e instanceof YamlStoreError ? e.message : String(e instanceof Error ? e.message : e));
|
|
447
339
|
}
|
|
448
340
|
}
|
|
449
341
|
/** The exact commented starter shipped as examples/workers.yaml and by `init`. */
|
|
@@ -481,6 +373,18 @@ providers:
|
|
|
481
373
|
default: k3[1m]
|
|
482
374
|
fast: kimi-for-coding[1m]
|
|
483
375
|
|
|
376
|
+
# An \`engine: image\` provider does not spawn Claude — \`pai worker run
|
|
377
|
+
# --capability image\` POSTs straight to its OpenAI-compatible images API and
|
|
378
|
+
# writes the PNG it gets back. Uncomment and point it at a real provider
|
|
379
|
+
# (still nested under providers:, above) to enable \`--capability image\`:
|
|
380
|
+
# pictures:
|
|
381
|
+
# engine: image
|
|
382
|
+
# url: https://api.example.com/v1
|
|
383
|
+
# key: "<your-api-key>"
|
|
384
|
+
# models:
|
|
385
|
+
# default: example-image-model
|
|
386
|
+
# image: example-image-model
|
|
387
|
+
|
|
484
388
|
# A class names a provider, or provider/role to pick a non-default model.
|
|
485
389
|
# Workers exist to parallelise and to save cost: the default is Sonnet,
|
|
486
390
|
# Haiku for the mechanical classes, and nothing here inherits the
|
|
@@ -496,6 +400,13 @@ classes:
|
|
|
496
400
|
simple: anthropic/fast
|
|
497
401
|
image: glm/image
|
|
498
402
|
|
|
403
|
+
# Cross-provider capability preference: which provider serves a --capability
|
|
404
|
+
# request, in order, when more than one declares it. Unlisted capabilities
|
|
405
|
+
# fall back to the active provider, then any provider that declares them.
|
|
406
|
+
# capabilities:
|
|
407
|
+
# image: [pictures, glm]
|
|
408
|
+
# fast: [anthropic]
|
|
409
|
+
|
|
499
410
|
mcp_sets:
|
|
500
411
|
desktop: [clickr]
|
|
501
412
|
`;
|
|
@@ -541,6 +452,12 @@ function buildWorkersYamlText(data) {
|
|
|
541
452
|
lines.push(classEntries.length ? "classes:" : "classes: {}");
|
|
542
453
|
for (const [cls, target] of classEntries) lines.push(` ${cls}: ${typeof target === "string" ? target : yamlScalar(target)}`);
|
|
543
454
|
lines.push("");
|
|
455
|
+
const capEntries = Object.entries(data.capabilities);
|
|
456
|
+
if (capEntries.length) {
|
|
457
|
+
lines.push("capabilities:");
|
|
458
|
+
for (const [cap, prefs] of capEntries) lines.push(` ${cap}: [${prefs.join(", ")}]`);
|
|
459
|
+
lines.push("");
|
|
460
|
+
}
|
|
544
461
|
const mcpEntries = Object.entries(data.mcpSets);
|
|
545
462
|
lines.push(mcpEntries.length ? "mcp_sets:" : "mcp_sets: {}");
|
|
546
463
|
for (const [set, servers] of mcpEntries) lines.push(` ${set}: [${servers.join(", ")}]`);
|
|
@@ -572,6 +489,7 @@ function migrateWorkersToYaml(jsonPath, opts = {}) {
|
|
|
572
489
|
active: workers.active,
|
|
573
490
|
providers: workers.providers,
|
|
574
491
|
classes: workers.classes,
|
|
492
|
+
capabilities: workers.capabilities,
|
|
575
493
|
mcpSets: workers.mcpSets,
|
|
576
494
|
nativeModels: workers.nativeModels
|
|
577
495
|
});
|
|
@@ -679,8 +597,8 @@ function inlineWorkersYamlKeys(opts = {}) {
|
|
|
679
597
|
//#region src/workers/config.ts
|
|
680
598
|
/**
|
|
681
599
|
* config.ts — the `workers` section of the PAI config file (CONFIG_FILE,
|
|
682
|
-
* see src/daemon/config.ts's paiConfigFilePath — ~/.claude/pai/config.
|
|
683
|
-
* today,
|
|
600
|
+
* see src/daemon/config.ts's paiConfigFilePath — ~/.claude/pai/config.yaml
|
|
601
|
+
* today, config.json until `pai config yaml` runs).
|
|
684
602
|
*
|
|
685
603
|
* Everything that knows about worker providers reads this module: the CLI
|
|
686
604
|
* (`pai worker …`), the MCP tools (worker_*), and the Agent-routing hook.
|
|
@@ -772,20 +690,26 @@ const WORKER_CLASSES = [
|
|
|
772
690
|
"image"
|
|
773
691
|
];
|
|
774
692
|
/**
|
|
775
|
-
* The
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
693
|
+
* The well-known model capabilities — documented, and what the starter
|
|
694
|
+
* workers.yaml and `pai worker model`'s listing show by name. The set is
|
|
695
|
+
* open, though: a provider's `models` block may carry any capability name
|
|
696
|
+
* matching CAPABILITY_NAME_RE (see isModelCapability), e.g. "vision" or
|
|
697
|
+
* "longcontext" for a provider that serves them. "default" is the required
|
|
698
|
+
* catch-all; the others name what a model is *for* — "fast" the cheap tier
|
|
699
|
+
* (spotcheck, haiku-tier spawns), "image" the image class/capability.
|
|
700
|
+
* Everything resolves through resolveModelCapability, falling back to
|
|
701
|
+
* default; cross-provider preference for a capability is `capabilities:` in
|
|
702
|
+
* workers.yaml (see resolveCapability).
|
|
780
703
|
*/
|
|
781
704
|
const MODEL_CAPABILITIES = [
|
|
782
705
|
"default",
|
|
783
706
|
"fast",
|
|
784
707
|
"image"
|
|
785
708
|
];
|
|
786
|
-
|
|
709
|
+
const CAPABILITY_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
710
|
+
/** Is this string a syntactically valid capability name (open set)? */
|
|
787
711
|
function isModelCapability(v) {
|
|
788
|
-
return
|
|
712
|
+
return CAPABILITY_NAME_RE.test(v);
|
|
789
713
|
}
|
|
790
714
|
/**
|
|
791
715
|
* Which model capability a class runs on when its target names no alias: the
|
|
@@ -849,6 +773,7 @@ function defaultWorkersConfig() {
|
|
|
849
773
|
providers: {},
|
|
850
774
|
classes: {},
|
|
851
775
|
mcpSets: { ...DEFAULT_MCP_SETS },
|
|
776
|
+
capabilities: {},
|
|
852
777
|
pane: { ...DEFAULT_PANE },
|
|
853
778
|
logDir: DEFAULT_LOG_DIR,
|
|
854
779
|
routing: {
|
|
@@ -882,7 +807,7 @@ function parseModelsBlock(pathPrefix, modelsRaw) {
|
|
|
882
807
|
const models = { default: defaultModel };
|
|
883
808
|
for (const key of Object.keys(m)) {
|
|
884
809
|
if (key === "default") continue;
|
|
885
|
-
if (!isModelCapability(key)) bad(`${pathPrefix}.${key}`, `"${key}" is not a
|
|
810
|
+
if (!isModelCapability(key)) bad(`${pathPrefix}.${key}`, `"${key}" is not a valid capability name (must match ^[a-z][a-z0-9-]*$; well-known: ${MODEL_CAPABILITIES.join(", ")}, but any name in that shape is accepted)`);
|
|
886
811
|
const id = str(m[key]);
|
|
887
812
|
if (!id) bad(`${pathPrefix}.${key}`, "must be a non-empty model id");
|
|
888
813
|
models[key] = id;
|
|
@@ -897,7 +822,7 @@ function parseProvider(name, raw) {
|
|
|
897
822
|
const protocol = p.protocol === void 0 ? "anthropic" : str(p.protocol);
|
|
898
823
|
if (protocol !== "anthropic" && protocol !== "openai") bad(`.providers.${name}.protocol`, `"${str(p.protocol)}" is neither "anthropic" nor "openai"`);
|
|
899
824
|
const engine = p.engine === void 0 ? "claude" : str(p.engine);
|
|
900
|
-
if (engine !== "claude" && engine !== "codex") bad(`.providers.${name}.engine`, `"${str(p.engine)}" is
|
|
825
|
+
if (engine !== "claude" && engine !== "codex" && engine !== "image") bad(`.providers.${name}.engine`, `"${str(p.engine)}" is none of "claude", "codex", "image"`);
|
|
901
826
|
const baseUrl = str(p.baseUrl);
|
|
902
827
|
if (!baseUrl && protocol !== "openai") bad(`.providers.${name}.baseUrl`, "is required");
|
|
903
828
|
const keyFile = p.keyFile === void 0 || p.keyFile === null || str(p.keyFile) === "" ? null : str(p.keyFile);
|
|
@@ -1059,6 +984,24 @@ function parseMcpSetsValue(raw, badPathPrefix) {
|
|
|
1059
984
|
return mcpSets;
|
|
1060
985
|
}
|
|
1061
986
|
/**
|
|
987
|
+
* Parse a `capabilities` value shared by JSON and YAML loading: capability
|
|
988
|
+
* name (open set, see isModelCapability) → provider names in preference
|
|
989
|
+
* order. Provider names are not cross-checked here — a name that stops
|
|
990
|
+
* existing is simply skipped by resolveCapability at resolve time, the same
|
|
991
|
+
* way routing.order tolerates a removed provider.
|
|
992
|
+
*/
|
|
993
|
+
function parseCapabilitiesValue(raw, badPathPrefix) {
|
|
994
|
+
const capabilities = {};
|
|
995
|
+
if (raw === void 0) return capabilities;
|
|
996
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) bad(badPathPrefix, "must be an object of capability name → [provider names]");
|
|
997
|
+
for (const [name, order] of Object.entries(raw)) {
|
|
998
|
+
if (!isModelCapability(name)) bad(`${badPathPrefix}.${name}`, `"${name}" is not a valid capability name (must match ^[a-z][a-z0-9-]*$)`);
|
|
999
|
+
if (!Array.isArray(order) || order.length === 0 || order.some((x) => typeof x !== "string" || !x)) bad(`${badPathPrefix}.${name}`, "must be a non-empty array of provider names");
|
|
1000
|
+
capabilities[name] = order;
|
|
1001
|
+
}
|
|
1002
|
+
return capabilities;
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1062
1005
|
* Parse and validate a raw `workers` value. Missing section → defaults.
|
|
1063
1006
|
* Unknown-but-typed garbage → WorkersConfigError naming the offending field.
|
|
1064
1007
|
*/
|
|
@@ -1074,6 +1017,7 @@ function parseWorkersConfig(raw) {
|
|
|
1074
1017
|
}
|
|
1075
1018
|
const classes = parseClassesValue(w.classes !== void 0 ? w.classes : w.roles, w.classes !== void 0 ? ".classes" : ".roles");
|
|
1076
1019
|
const mcpSets = parseMcpSetsValue(w.mcpSets, ".mcpSets");
|
|
1020
|
+
const capabilities = parseCapabilitiesValue(w.capabilities, ".capabilities");
|
|
1077
1021
|
let pane = { ...DEFAULT_PANE };
|
|
1078
1022
|
if (w.pane !== void 0) {
|
|
1079
1023
|
if (typeof w.pane !== "object" || w.pane === null) bad(".pane", "must be an object");
|
|
@@ -1157,6 +1101,7 @@ function parseWorkersConfig(raw) {
|
|
|
1157
1101
|
providers,
|
|
1158
1102
|
classes,
|
|
1159
1103
|
mcpSets,
|
|
1104
|
+
capabilities,
|
|
1160
1105
|
pane,
|
|
1161
1106
|
logDir: str(w.logDir) || d.logDir,
|
|
1162
1107
|
routing,
|
|
@@ -1172,24 +1117,28 @@ function workersYamlDataOf(workers) {
|
|
|
1172
1117
|
providers: workers.providers,
|
|
1173
1118
|
classes: workers.classes,
|
|
1174
1119
|
mcpSets: workers.mcpSets,
|
|
1120
|
+
capabilities: workers.capabilities,
|
|
1175
1121
|
nativeModels: workers.nativeModels
|
|
1176
1122
|
};
|
|
1177
1123
|
}
|
|
1178
1124
|
/**
|
|
1179
1125
|
* Read the whole config file and return (raw, workers) — the raw record so
|
|
1180
1126
|
* callers can rewrite it preserving every other section, the parsed+validated
|
|
1181
|
-
* workers section. Unreadable config throws
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1127
|
+
* workers section. Unreadable config throws, missing is fine. `path`
|
|
1128
|
+
* overrides the config location (tests, CLAUDE_SETTINGS_PATH-style dry runs);
|
|
1129
|
+
* default CONFIG_FILE (see paiConfigFilePath). Reads via readMainConfigRaw,
|
|
1130
|
+
* so a config.yaml next to `path` is preferred over JSON, same as every
|
|
1131
|
+
* other main-config reader.
|
|
1184
1132
|
*
|
|
1185
1133
|
* Providers, classes, mcpSets and active load from workers.yaml (next to
|
|
1186
1134
|
* `path`) when it exists; otherwise they fall back to the JSON `workers`
|
|
1187
1135
|
* section, and then to built-in defaults — nothing breaks before migration
|
|
1188
1136
|
* (`pai worker config migrate`). Everything else (pane, routing, tree,
|
|
1189
|
-
* cacheKeepaliveSecs, fallback, enabled, logDir) always comes from
|
|
1137
|
+
* cacheKeepaliveSecs, fallback, enabled, logDir) always comes from the main
|
|
1138
|
+
* config (JSON or YAML).
|
|
1190
1139
|
*/
|
|
1191
1140
|
function readWorkersSection(path = CONFIG_FILE) {
|
|
1192
|
-
const raw =
|
|
1141
|
+
const raw = readMainConfigRaw(path);
|
|
1193
1142
|
const workers = parseWorkersConfig(raw.workers);
|
|
1194
1143
|
const yaml = readWorkersYaml(workersYamlPath());
|
|
1195
1144
|
if (yaml) {
|
|
@@ -1197,6 +1146,7 @@ function readWorkersSection(path = CONFIG_FILE) {
|
|
|
1197
1146
|
workers.providers = yaml.data.providers;
|
|
1198
1147
|
workers.classes = yaml.data.classes;
|
|
1199
1148
|
workers.mcpSets = yaml.data.mcpSets;
|
|
1149
|
+
workers.capabilities = yaml.data.capabilities;
|
|
1200
1150
|
workers.nativeModels = yaml.data.nativeModels;
|
|
1201
1151
|
}
|
|
1202
1152
|
return {
|
|
@@ -1226,7 +1176,7 @@ function writeWorkersSection(raw, workers, path = CONFIG_FILE) {
|
|
|
1226
1176
|
...workers,
|
|
1227
1177
|
fallback: void 0
|
|
1228
1178
|
};
|
|
1229
|
-
|
|
1179
|
+
writeMainConfigRaw(raw, path);
|
|
1230
1180
|
}
|
|
1231
1181
|
/** Expand a leading ~ (config values are written with `~` to stay portable). */
|
|
1232
1182
|
function expandHome(p) {
|
|
@@ -1279,11 +1229,90 @@ function providerCostTier(p) {
|
|
|
1279
1229
|
function resolveModelCapability(p, capability) {
|
|
1280
1230
|
return p.models[capability] ?? p.models.default;
|
|
1281
1231
|
}
|
|
1232
|
+
/** Runner behind a provider, defaulting to the Claude Code harness. */
|
|
1233
|
+
function providerEngine(p) {
|
|
1234
|
+
return p.engine ?? "claude";
|
|
1235
|
+
}
|
|
1236
|
+
/** Does this provider name a model for the capability ("default" always counts)? */
|
|
1237
|
+
function providerDeclaresCapability(p, capability) {
|
|
1238
|
+
return capability === "default" || p.models[capability] !== void 0;
|
|
1239
|
+
}
|
|
1240
|
+
/** A provider usable for this capability: declares it, enabled, and passes assertProviderRunnable. */
|
|
1241
|
+
function providerUsableForCapability(p, capability) {
|
|
1242
|
+
if (!p || !p.enabled || !providerDeclaresCapability(p, capability)) return false;
|
|
1243
|
+
try {
|
|
1244
|
+
assertProviderRunnable("", p);
|
|
1245
|
+
return true;
|
|
1246
|
+
} catch {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Cross-provider capability resolution (`workers.yaml`'s `capabilities:`
|
|
1252
|
+
* map): which provider+model serves a named capability (e.g. "image"),
|
|
1253
|
+
* independent of any one provider's own `models` table.
|
|
1254
|
+
*
|
|
1255
|
+
* Resolution order:
|
|
1256
|
+
* 1. `capabilities.<name>` (explicit preference list) — first provider
|
|
1257
|
+
* that declares the capability, is enabled, and passes
|
|
1258
|
+
* assertProviderRunnable.
|
|
1259
|
+
* 2. No preference list: the active provider, if it declares the
|
|
1260
|
+
* capability.
|
|
1261
|
+
* 3. Still nothing: any enabled provider that declares the capability
|
|
1262
|
+
* (stable order — provider names sorted, "anthropic" included).
|
|
1263
|
+
* 4. Nothing anywhere: the active provider's default model, `fellBack: true`.
|
|
1264
|
+
*/
|
|
1265
|
+
function resolveCapability(workers, capability) {
|
|
1266
|
+
const order = workers.capabilities[capability];
|
|
1267
|
+
if (order?.length) {
|
|
1268
|
+
for (const name of order) {
|
|
1269
|
+
const p = getProviderOrNative(workers, name);
|
|
1270
|
+
if (providerUsableForCapability(p, capability)) return {
|
|
1271
|
+
provider: name,
|
|
1272
|
+
model: resolveModelCapability(p, capability),
|
|
1273
|
+
engine: providerEngine(p),
|
|
1274
|
+
fellBack: false
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
throw new WorkersConfigError(`no configured provider for capability "${capability}" is usable right now (checked, in order: ${order.join(", ")}) — set one with: pai worker capability ${capability} <provider>`);
|
|
1278
|
+
}
|
|
1279
|
+
const activeName = workers.active && workers.active !== "auto" ? workers.active : null;
|
|
1280
|
+
if (activeName) {
|
|
1281
|
+
const p = getProviderOrNative(workers, activeName);
|
|
1282
|
+
if (providerUsableForCapability(p, capability)) return {
|
|
1283
|
+
provider: activeName,
|
|
1284
|
+
model: resolveModelCapability(p, capability),
|
|
1285
|
+
engine: providerEngine(p),
|
|
1286
|
+
fellBack: false
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const candidates = {
|
|
1290
|
+
[ANTHROPIC_NATIVE]: nativeAnthropicProvider(workers.nativeModels),
|
|
1291
|
+
...workers.providers
|
|
1292
|
+
};
|
|
1293
|
+
for (const name of Object.keys(candidates).sort()) {
|
|
1294
|
+
const p = candidates[name];
|
|
1295
|
+
if (providerUsableForCapability(p, capability)) return {
|
|
1296
|
+
provider: name,
|
|
1297
|
+
model: resolveModelCapability(p, capability),
|
|
1298
|
+
engine: providerEngine(p),
|
|
1299
|
+
fellBack: false
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const fallbackName = activeName ?? ANTHROPIC_NATIVE;
|
|
1303
|
+
const fallback = getProviderOrNative(workers, fallbackName) ?? nativeAnthropicProvider(workers.nativeModels);
|
|
1304
|
+
return {
|
|
1305
|
+
provider: fallbackName,
|
|
1306
|
+
model: fallback.models.default,
|
|
1307
|
+
engine: providerEngine(fallback),
|
|
1308
|
+
fellBack: true
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1282
1311
|
/** Directory under which inline keys (MCP `key` field) are stored, 0600. */
|
|
1283
1312
|
function keysDir() {
|
|
1284
1313
|
return paiHomePath("keys");
|
|
1285
1314
|
}
|
|
1286
1315
|
|
|
1287
1316
|
//#endregion
|
|
1288
|
-
export {
|
|
1289
|
-
//# sourceMappingURL=config-
|
|
1317
|
+
export { migrateWorkersToYaml as A, readWorkersSection as C, writeWorkersSection as D, resolveProviderKey as E, workersYamlPath as F, DEFAULT_CONTEXT_WINDOW as I, contextWindowFromModelId as L, parseWorkersYamlDocument as M, relocateWorkersYaml as N, initWorkersYaml as O, workersYamlLegacyNotice as P, stripModelVariant as R, providerKeyPath as S, resolveModelCapability as T, isModelCapability as _, DEFAULT_PANE as a, nativeAnthropicProvider as b, MODEL_CAPABILITIES as c, WorkersConfigError as d, assertProviderRunnable as f, getProviderOrNative as g, expandHome as h, DEFAULT_LOG_DIR as i, needsWorkersYamlRelocation as j, inlineWorkersYamlKeys as k, PROVIDER_TAGS as l, defaultWorkersConfig as m, CAPABILITY_NAME_RE as n, DEFAULT_ROUTING as o, classModelCapability as p, DEFAULT_CACHE_KEEPALIVE_SECS as r, DEFAULT_TREE as s, ANTHROPIC_NATIVE as t, WORKER_CLASSES as u, keysDir as v, resolveCapability as w, providerCostTier as x, maskKey as y };
|
|
1318
|
+
//# sourceMappingURL=config-DYXARgk8.mjs.map
|