@tekmidian/pai 0.42.5 → 0.42.6
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/ARCHITECTURE.md +9 -9
- package/README.md +26 -8
- package/dist/{aibroker-client-BP74gGwD.mjs → aibroker-client-Bn9SnGMa.mjs} +3 -3
- package/dist/{aibroker-client-BP74gGwD.mjs.map → aibroker-client-Bn9SnGMa.mjs.map} +1 -1
- package/dist/{aibroker-client-C6m71p6O.mjs → aibroker-client-D5NT0S1N.mjs} +1 -1
- package/dist/{auto-route-_5zzJz5q.mjs → auto-route-CcD0OWWB.mjs} +4 -4
- package/dist/{auto-route-_5zzJz5q.mjs.map → auto-route-CcD0OWWB.mjs.map} +1 -1
- package/dist/{chain-DTII7xeL.mjs → chain-BbcfP77K.mjs} +5 -4
- package/dist/{chain-DTII7xeL.mjs.map → chain-BbcfP77K.mjs.map} +1 -1
- package/dist/cli/index.mjs +23 -21
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +23 -21
- package/dist/{clusters-j0ZuNYp9.mjs → clusters-Cw_P6lO-.mjs} +2 -2
- package/dist/{clusters-j0ZuNYp9.mjs.map → clusters-Cw_P6lO-.mjs.map} +1 -1
- package/dist/{config-B64vFg14.mjs → config-DCu8THKi.mjs} +41 -9
- package/dist/{config-B64vFg14.mjs.map → config-DCu8THKi.mjs.map} +1 -1
- package/dist/config-DeqFqa1N.mjs +3 -0
- package/dist/config-DxMZjpvY.mjs +1289 -0
- package/dist/config-DxMZjpvY.mjs.map +1 -0
- package/dist/context-handover-cache-B9NKSjwR.mjs +3242 -0
- package/dist/context-handover-cache-B9NKSjwR.mjs.map +1 -0
- package/dist/daemon/index.mjs +23 -22
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BNjbVsQU.mjs +24 -0
- package/dist/{daemon-B8N1egl0.mjs → daemon-BppziPi6.mjs} +52 -38
- package/dist/daemon-BppziPi6.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +144 -117
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/db-BOBfewFH.mjs +3 -0
- package/dist/{db-a1ixZQjr.mjs → db-DJ8rk0VM.mjs} +15 -6
- package/dist/{db-a1ixZQjr.mjs.map → db-DJ8rk0VM.mjs.map} +1 -1
- package/dist/db-DbY2O8QO.mjs +3 -0
- package/dist/{db-Ca5qfsMC.mjs → db-Ddns80yf.mjs} +15 -6
- package/dist/{db-Ca5qfsMC.mjs.map → db-Ddns80yf.mjs.map} +1 -1
- package/dist/{detect-CdaA48EI.mjs → detect-Bf2z-oKB.mjs} +1 -1
- package/dist/{detect-CdaA48EI.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
- package/dist/detector-CkwkXuGh.mjs +5 -0
- package/dist/{detector-C73Nxm94.mjs → detector-DWXkeTlk.mjs} +2 -2
- package/dist/{detector-C73Nxm94.mjs.map → detector-DWXkeTlk.mjs.map} +1 -1
- package/dist/{embeddings-CEBGrzwu.mjs → embeddings-ClqKtLZ8.mjs} +1 -1
- package/dist/{embeddings-DOLZnT1X.mjs → embeddings-DvRKcQyt.mjs} +1 -1
- package/dist/{embeddings-DOLZnT1X.mjs.map → embeddings-DvRKcQyt.mjs.map} +1 -1
- package/dist/factory-47Qf_df7.mjs +3 -0
- package/dist/{factory-DEb9NmHJ.mjs → factory-DNYAqjH0.mjs} +8 -8
- package/dist/{factory-DEb9NmHJ.mjs.map → factory-DNYAqjH0.mjs.map} +1 -1
- package/dist/{fallback-oJl3BaIN.mjs → fallback-Cwt4hmh5.mjs} +182 -122
- package/dist/fallback-Cwt4hmh5.mjs.map +1 -0
- package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
- package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
- package/dist/hooks/capture-all-events.mjs +117 -34
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +110 -33
- package/dist/hooks/capture-session-summary.mjs.map +4 -4
- package/dist/hooks/capture-tool-output.mjs +105 -28
- package/dist/hooks/capture-tool-output.mjs.map +4 -4
- package/dist/hooks/cleanup-session-files.mjs +54 -13
- package/dist/hooks/cleanup-session-files.mjs.map +2 -2
- package/dist/hooks/context-compression-hook.mjs +7565 -122
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +55 -14
- package/dist/hooks/initialize-session.mjs.map +2 -2
- package/dist/hooks/inject-observations.mjs +79 -18
- package/dist/hooks/inject-observations.mjs.map +3 -3
- package/dist/hooks/load-core-context.mjs +55 -14
- package/dist/hooks/load-core-context.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +185 -103
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/mcp-deferred-gate.mjs +54 -13
- package/dist/hooks/mcp-deferred-gate.mjs.map +2 -2
- package/dist/hooks/mcp-deferred-reminder.mjs +9 -0
- package/dist/hooks/mcp-deferred-reminder.mjs.map +3 -3
- package/dist/hooks/observe.mjs +54 -13
- package/dist/hooks/observe.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs +7419 -16
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +7778 -171
- package/dist/hooks/route-agents-to-worker.mjs.map +4 -4
- package/dist/hooks/route-edits-to-worker.mjs +90 -0
- package/dist/hooks/route-edits-to-worker.mjs.map +7 -0
- package/dist/hooks/security-validator.mjs +147 -6
- package/dist/hooks/security-validator.mjs.map +4 -4
- package/dist/hooks/session-commands.mjs +9 -0
- package/dist/hooks/session-commands.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +182 -105
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/subagent-stop-hook.mjs +3 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
- package/dist/hooks/sync-todo-to-md.mjs +54 -13
- package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
- package/dist/hooks/update-tab-on-action.mjs +3 -0
- package/dist/hooks/update-tab-on-action.mjs.map +1 -1
- package/dist/hooks/update-tab-titles.mjs +3 -0
- package/dist/hooks/update-tab-titles.mjs.map +1 -1
- package/dist/hooks/whisper-reinject.mjs +3 -0
- package/dist/hooks/whisper-reinject.mjs.map +1 -1
- package/dist/hooks/whisper-rules.mjs +134 -29
- package/dist/hooks/whisper-rules.mjs.map +4 -4
- package/dist/hooks/worker-proxy.mjs +7737 -117
- package/dist/hooks/worker-proxy.mjs.map +4 -4
- package/dist/hooks/worker-status-line.mjs +7791 -144
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/hooks/worker-supervision.mjs +7721 -115
- package/dist/hooks/worker-supervision.mjs.map +4 -4
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +10 -10
- package/dist/{indexer-backend-BPTzKFZU.mjs → indexer-backend-CdvCJCkD.mjs} +3 -3
- package/dist/{indexer-backend-BPTzKFZU.mjs.map → indexer-backend-CdvCJCkD.mjs.map} +1 -1
- package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C6NryMMb.mjs} +2 -2
- package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C6NryMMb.mjs.map} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-BSjM5xZh.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-BSjM5xZh.mjs.map} +1 -1
- package/dist/{latent-ideas-CtSca4qG.mjs → latent-ideas-BfL4nCQ3.mjs} +4 -4
- package/dist/{latent-ideas-CtSca4qG.mjs.map → latent-ideas-BfL4nCQ3.mjs.map} +1 -1
- package/dist/{link-boost-Bucnz3n8.mjs → link-boost-C5PaNdzm.mjs} +1 -1
- package/dist/{link-boost-Bucnz3n8.mjs.map → link-boost-C5PaNdzm.mjs.map} +1 -1
- package/dist/main-resolver-CnfnWa8C.mjs +7 -0
- package/dist/{main-resolver-DnkxPYkr.mjs → main-resolver-DvYu9jbl.mjs} +21 -11
- package/dist/main-resolver-DvYu9jbl.mjs.map +1 -0
- package/dist/{migrate-Cjzeefn9.mjs → migrate-CfERwXhY.mjs} +2 -2
- package/dist/{migrate-Cjzeefn9.mjs.map → migrate-CfERwXhY.mjs.map} +1 -1
- package/dist/{neighborhood-CM7Jgb51.mjs → neighborhood-CETR_P4y.mjs} +2 -2
- package/dist/{neighborhood-CM7Jgb51.mjs.map → neighborhood-CETR_P4y.mjs.map} +1 -1
- package/dist/{note-context-DCwE2A-M.mjs → note-context-DCcRWBoN.mjs} +1 -1
- package/dist/{note-context-DCwE2A-M.mjs.map → note-context-DCcRWBoN.mjs.map} +1 -1
- package/dist/pai-home-ChAOupZc.mjs +164 -0
- package/dist/pai-home-ChAOupZc.mjs.map +1 -0
- package/dist/{pai-marker-CHtbJMwJ.mjs → pai-marker-B20KqhA8.mjs} +1 -1
- package/dist/{pai-marker-CHtbJMwJ.mjs.map → pai-marker-B20KqhA8.mjs.map} +1 -1
- package/dist/{planner-D5Ui7DAm.mjs → planner-pXRc-8yt.mjs} +7 -6
- package/dist/{planner-D5Ui7DAm.mjs.map → planner-pXRc-8yt.mjs.map} +1 -1
- package/dist/{postgres-CtOzfoQU.mjs → postgres-CUSXWgTG.mjs} +2 -2
- package/dist/{postgres-CtOzfoQU.mjs.map → postgres-CUSXWgTG.mjs.map} +1 -1
- package/dist/{program-zpoYnJGz.mjs → program-LLNPpxlj.mjs} +8308 -6065
- package/dist/program-LLNPpxlj.mjs.map +1 -0
- package/dist/query-feedback-C7rSfiXU.mjs +3 -0
- package/dist/{query-feedback-Dqz-HC__.mjs → query-feedback-CYyOGi3y.mjs} +20 -6
- package/dist/query-feedback-CYyOGi3y.mjs.map +1 -0
- package/dist/{reranker-CwTCNsgA.mjs → reranker-B5qUTamW.mjs} +1 -1
- package/dist/{reranker-xPm04PXx.mjs → reranker-HHmhZk6v.mjs} +1 -1
- package/dist/{reranker-xPm04PXx.mjs.map → reranker-HHmhZk6v.mjs.map} +1 -1
- package/dist/router-CremHKTI.mjs +3 -0
- package/dist/{router-DD6D4j31.mjs → router-DBAZfE7X.mjs} +2 -2
- package/dist/{router-DD6D4j31.mjs.map → router-DBAZfE7X.mjs.map} +1 -1
- package/dist/{run-D7K5UALn.mjs → run-DDdT9teG.mjs} +530 -210
- package/dist/run-DDdT9teG.mjs.map +1 -0
- package/dist/{runtime-paths-rni52zHX.mjs → runtime-paths-B0P1TvUr.mjs} +1 -1
- package/dist/{runtime-paths-rni52zHX.mjs.map → runtime-paths-B0P1TvUr.mjs.map} +1 -1
- package/dist/{search-Rpk1cSBC.mjs → search-B69cQwDB.mjs} +3 -3
- package/dist/{search-Rpk1cSBC.mjs.map → search-B69cQwDB.mjs.map} +1 -1
- package/dist/{search-CfPpJAWQ.mjs → search-fb-5ovj4.mjs} +2 -2
- package/dist/{server-D4vAieF4.mjs → server-Ch99xt5K.mjs} +15 -14
- package/dist/server-Ch99xt5K.mjs.map +1 -0
- package/dist/skills/Advisor/SKILL.md +3 -3
- package/dist/skills/Providers/SKILL.md +30 -0
- package/dist/skills/Reconstruct/SKILL.md +1 -1
- package/dist/skills/VaultContext/SKILL.md +1 -1
- package/dist/skills/Whisper/SKILL.md +3 -3
- package/dist/skills/Worker/SKILL.md +1 -1
- package/dist/{sources-CS43CpuS.mjs → sources-KBGEA_E6.mjs} +2 -2
- package/dist/{sources-CS43CpuS.mjs.map → sources-KBGEA_E6.mjs.map} +1 -1
- package/dist/{sqlite-Cq3gJcq9.mjs → sqlite-By6r2YhZ.mjs} +3 -3
- package/dist/{sqlite-Cq3gJcq9.mjs.map → sqlite-By6r2YhZ.mjs.map} +1 -1
- package/dist/{state-CY6I-Hnc.mjs → state-DnYGRmS7.mjs} +1 -1
- package/dist/{state-BRdB-Bf1.mjs → state-PJrgfDLW.mjs} +1 -1
- package/dist/{state-BRdB-Bf1.mjs.map → state-PJrgfDLW.mjs.map} +1 -1
- package/dist/{stop-words-Hfu8u22w.mjs → stop-words-BaMEGVeY.mjs} +1 -1
- package/dist/{stop-words-Hfu8u22w.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
- package/dist/{sync-BWbe8JTg.mjs → sync-Btcb6xA4.mjs} +3 -3
- package/dist/{sync-BWbe8JTg.mjs.map → sync-Btcb6xA4.mjs.map} +1 -1
- package/dist/{themes-B7h9YDiy.mjs → themes-BVoAnLZj.mjs} +3 -3
- package/dist/{themes-B7h9YDiy.mjs.map → themes-BVoAnLZj.mjs.map} +1 -1
- package/dist/{tools-BW3NLMPk.mjs → tools-BradfQk7.mjs} +4 -3
- package/dist/{tools-LlzcV_eK.mjs → tools-UrSHIe29.mjs} +30 -233
- package/dist/tools-UrSHIe29.mjs.map +1 -0
- package/dist/{trace-u4hT-pLD.mjs → trace-Cy8quQTm.mjs} +1 -1
- package/dist/{trace-u4hT-pLD.mjs.map → trace-Cy8quQTm.mjs.map} +1 -1
- package/dist/{utils-DhMex3Ox.mjs → utils-BD8g8oSf.mjs} +1 -1
- package/dist/{utils-9Err2RBW.mjs → utils-CJymHkYm.mjs} +1 -1
- package/dist/{utils-9Err2RBW.mjs.map → utils-CJymHkYm.mjs.map} +1 -1
- package/dist/{vault-indexer-mata9S7b.mjs → vault-indexer-C5i-OAyr.mjs} +2 -2
- package/dist/{vault-indexer-mata9S7b.mjs.map → vault-indexer-C5i-OAyr.mjs.map} +1 -1
- package/dist/wakeup-TnGjJY3I.mjs +218 -0
- package/dist/wakeup-TnGjJY3I.mjs.map +1 -0
- package/dist/work-queue-worker-BA5IDBwQ.mjs +567 -0
- package/dist/work-queue-worker-BA5IDBwQ.mjs.map +1 -0
- package/dist/work-queue-worker-DLM2wlqM.mjs +14 -0
- package/dist/{zettelkasten-F0D_XfQ4.mjs → zettelkasten-BBjA7GcP.mjs} +4 -4
- package/dist/{zettelkasten-F0D_XfQ4.mjs.map → zettelkasten-BBjA7GcP.mjs.map} +1 -1
- package/docs/commands/README.md +30 -3
- package/docs/commands/audit.md +88 -0
- package/docs/commands/backup.md +2 -2
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/config.md +59 -0
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/help.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/launch.md +45 -0
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +3 -3
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/project.md +46 -1
- package/docs/commands/projects.md +46 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +95 -6
- package/docs/commands/zettel.md +1 -1
- package/docs/provider-abstraction.md +1 -1
- package/docs/provider-independence-details.md +8 -5
- package/docs/task-bus.md +1 -1
- package/docs/worker.md +136 -83
- package/docs/workers-config.md +446 -0
- package/package.json +4 -2
- package/scripts/build-hooks.mjs +56 -3
- package/src/hooks/pre-compact.sh +5 -2
- package/src/hooks/session-stop.sh +29 -4
- package/src/hooks/ts/capture-all-events.ts +3 -3
- package/src/hooks/ts/lib/advisor-budget.test.ts +98 -0
- package/src/hooks/ts/lib/advisor-budget.ts +90 -0
- package/src/hooks/ts/lib/agent-gate.test.ts +101 -0
- package/src/hooks/ts/lib/agent-gate.ts +112 -0
- package/src/hooks/ts/lib/context-fill.ts +2 -2
- package/src/hooks/ts/lib/edit-gate.test.ts +52 -0
- package/src/hooks/ts/lib/edit-gate.ts +51 -0
- package/src/hooks/ts/lib/mcp-deferred-reminder.test.ts +8 -0
- package/src/hooks/ts/lib/pai-paths.test.ts +93 -0
- package/src/hooks/ts/lib/pai-paths.ts +168 -24
- package/src/hooks/ts/lib/project-utils/index.ts +1 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +5 -4
- package/src/hooks/ts/lib/project-utils.ts +1 -0
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +2 -2
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +46 -99
- package/src/hooks/ts/pre-tool-use/route-edits-to-worker.test.ts +47 -0
- package/src/hooks/ts/pre-tool-use/route-edits-to-worker.ts +96 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +2 -1
- package/src/hooks/ts/session-end/capture-session-summary.ts +4 -4
- package/src/hooks/ts/session-start/initialize-session.ts +3 -3
- package/src/hooks/ts/session-start/inject-observations.test.ts +56 -0
- package/src/hooks/ts/session-start/inject-observations.ts +28 -8
- package/src/hooks/ts/session-start/load-core-context.ts +4 -4
- package/src/hooks/ts/session-start/load-project-context.ts +2 -2
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -0
- package/src/hooks/ts/session-start/session-commands.ts +4 -0
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +6 -11
- package/src/hooks/ts/user-prompt/whisper-rules.test.ts +49 -0
- package/src/hooks/ts/user-prompt/whisper-rules.ts +88 -47
- package/statusline-command.sh +277 -116
- package/templates/agent-prefs.example.md +8 -8
- package/templates/claude-md.template.md +1 -1
- package/templates/voices.example.json +3 -3
- package/dist/config-C_ErGddD.mjs +0 -3
- package/dist/config-_lQI_W08.mjs +0 -640
- package/dist/config-_lQI_W08.mjs.map +0 -1
- package/dist/context-handover-cache-pNFdBNrU.mjs +0 -1548
- package/dist/context-handover-cache-pNFdBNrU.mjs.map +0 -1
- package/dist/daemon-B8N1egl0.mjs.map +0 -1
- package/dist/daemon-DHfyoTlk.mjs +0 -23
- package/dist/db-O-cyAPfS.mjs +0 -3
- package/dist/db-XEwJbuGO.mjs +0 -3
- package/dist/detector-C-7KRi42.mjs +0 -5
- package/dist/factory-Dbo8yNPi.mjs +0 -3
- package/dist/fallback-oJl3BaIN.mjs.map +0 -1
- package/dist/main-resolver-BAfOMppW.mjs +0 -7
- package/dist/main-resolver-DnkxPYkr.mjs.map +0 -1
- package/dist/program-zpoYnJGz.mjs.map +0 -1
- package/dist/query-feedback-DGoVRaWD.mjs +0 -3
- package/dist/query-feedback-Dqz-HC__.mjs.map +0 -1
- package/dist/router-BTIGZR2_.mjs +0 -3
- package/dist/run-D7K5UALn.mjs.map +0 -1
- package/dist/server-D4vAieF4.mjs.map +0 -1
- package/dist/tools-LlzcV_eK.mjs.map +0 -1
- package/dist/work-queue-worker-AEmfVE0F.mjs +0 -14
- package/dist/work-queue-worker-DIT-VLSh.mjs +0 -2136
- package/dist/work-queue-worker-DIT-VLSh.mjs.map +0 -1
- /package/dist/{indexer-D7MvSQPY.mjs → indexer-C5Le7XE7.mjs} +0 -0
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
import { a as paiHomePath, o as resolvePaiFile, r as migratePaiFile } from "./pai-home-ChAOupZc.mjs";
|
|
2
|
+
import { n as CONFIG_FILE } from "./config-DCu8THKi.mjs";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { Document, Scalar, parseDocument } from "yaml";
|
|
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
|
+
//#region src/utils/model-window.ts
|
|
118
|
+
/**
|
|
119
|
+
* model-window.ts — facts derivable from a bare model id.
|
|
120
|
+
*
|
|
121
|
+
* Shared by the worker stack (provider registry) and the session hooks
|
|
122
|
+
* (context-fill). Zero imports on purpose: this leaf is bundled into hooks
|
|
123
|
+
* (scripts/build-hooks.mjs) and must never drag config or path machinery
|
|
124
|
+
* into a hook process.
|
|
125
|
+
*/
|
|
126
|
+
/** Last-resort window when nothing on hand reports one and the model id
|
|
127
|
+
* carries no window information. */
|
|
128
|
+
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
129
|
+
/** Strip a bracketed variant suffix: "glm-5.3[1m]" → "glm-5.3". The suffix
|
|
130
|
+
* selects a variant of the same base model (a context-window tier, a
|
|
131
|
+
* quantization), so the prefix is the id's family. */
|
|
132
|
+
function stripModelVariant(model) {
|
|
133
|
+
return model.replace(/\[[^\]]*\]\s*$/, "").trim() || model;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The context window a model id itself declares, or null when the id carries
|
|
137
|
+
* no window information. Only the bracketed variant suffix is read
|
|
138
|
+
* ("[1m]" → 1,000,000); a bare id says nothing and stays null so callers can
|
|
139
|
+
* fall back to whatever they measured themselves.
|
|
140
|
+
*/
|
|
141
|
+
function contextWindowFromModelId(model) {
|
|
142
|
+
if (typeof model !== "string" || model === "") return null;
|
|
143
|
+
const m = /\[(\d+)m\]$/i.exec(model.trim());
|
|
144
|
+
if (!m) return null;
|
|
145
|
+
const millions = Number(m[1]);
|
|
146
|
+
if (!Number.isFinite(millions) || millions < 1) return null;
|
|
147
|
+
return millions * 1e6;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/workers/workers-config.ts
|
|
152
|
+
/**
|
|
153
|
+
* workers-config.ts — workers.yaml: providers, model roles, class routing,
|
|
154
|
+
* mcp_sets.
|
|
155
|
+
*
|
|
156
|
+
* config.ts's readWorkersSection/writeWorkersSection are the only callers —
|
|
157
|
+
* every other module keeps talking to the WorkersConfig shape from config.ts
|
|
158
|
+
* and never touches this file. That is deliberate: adding a provider used to
|
|
159
|
+
* mean editing JSON inside an unrelated config blob; this module makes
|
|
160
|
+
* providers/classes/mcp_sets/active a single human-editable file, documented
|
|
161
|
+
* in docs/workers-config.md, while everything downstream is unchanged.
|
|
162
|
+
*
|
|
163
|
+
* Writes go through the `yaml` package's Document API and only touch the
|
|
164
|
+
* entries that actually changed, so hand-written comments elsewhere in the
|
|
165
|
+
* file — including a comment directly above an untouched provider — survive
|
|
166
|
+
* byte-for-byte across `add`, `use`, `disable`, and every other mutation.
|
|
167
|
+
*/
|
|
168
|
+
const WORKERS_YAML_FILENAME = "workers.yaml";
|
|
169
|
+
/**
|
|
170
|
+
* Where workers.yaml lives since 2026-09-19: under the PAI_HOME namespace
|
|
171
|
+
* dir (~/.claude/pai by default) — this file is per-user state that can
|
|
172
|
+
* carry API keys and must never be committed, same as the rest of PAI_HOME.
|
|
173
|
+
*/
|
|
174
|
+
function defaultWorkersYamlPath() {
|
|
175
|
+
return paiHomePath(WORKERS_YAML_FILENAME);
|
|
176
|
+
}
|
|
177
|
+
/** Briefly the canonical location between 2026-09-19's two migrations —
|
|
178
|
+
* read during the transition, never written to again. */
|
|
179
|
+
function oldWorkersYamlPath() {
|
|
180
|
+
return join(homedir(), ".claude", WORKERS_YAML_FILENAME);
|
|
181
|
+
}
|
|
182
|
+
/** Where workers.yaml lived before 2026-09-19 — read during the transition,
|
|
183
|
+
* never written to (see `pai worker config migrate`). */
|
|
184
|
+
function legacyWorkersYamlPath() {
|
|
185
|
+
return join(homedir(), ".config", "pai", WORKERS_YAML_FILENAME);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The path any read/write of workers.yaml actually uses: PAI_WORKERS_YAML
|
|
189
|
+
* (tests, power users) first, else the new PAI_HOME location if it exists,
|
|
190
|
+
* else the most recent old location that is actually on disk (printing a
|
|
191
|
+
* one-time notice), else the new location (the target a first write creates).
|
|
192
|
+
*/
|
|
193
|
+
function workersYamlPath() {
|
|
194
|
+
const override = process.env.PAI_WORKERS_YAML;
|
|
195
|
+
if (override) return override;
|
|
196
|
+
return resolvePaiFile(defaultWorkersYamlPath(), [oldWorkersYamlPath(), legacyWorkersYamlPath()], "pai worker config migrate");
|
|
197
|
+
}
|
|
198
|
+
/** Where a fresh write (init, migrate, relocate's destination) always
|
|
199
|
+
* targets — the new location, or PAI_WORKERS_YAML for test isolation.
|
|
200
|
+
* Never an old path: nothing is ever written there again. */
|
|
201
|
+
function writeTargetWorkersYamlPath() {
|
|
202
|
+
return process.env.PAI_WORKERS_YAML ?? defaultWorkersYamlPath();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* One-line notice for `pai worker config check` and `pai worker providers`
|
|
206
|
+
* when workers.yaml is still sitting at an old location. Null once it has
|
|
207
|
+
* moved (or PAI_WORKERS_YAML is set — that always wins, nothing to migrate).
|
|
208
|
+
*/
|
|
209
|
+
function workersYamlLegacyNotice() {
|
|
210
|
+
if (process.env.PAI_WORKERS_YAML) return null;
|
|
211
|
+
if (existsSync(defaultWorkersYamlPath())) return null;
|
|
212
|
+
const found = [oldWorkersYamlPath(), legacyWorkersYamlPath()].find((p) => existsSync(p));
|
|
213
|
+
if (!found) return null;
|
|
214
|
+
return `workers.yaml is still at the old location (${found}) — run \`pai worker config migrate\` to move it to ${defaultWorkersYamlPath()}`;
|
|
215
|
+
}
|
|
216
|
+
/** snake_case on-disk keys → the camelCase raw shape parseProvider expects. */
|
|
217
|
+
function yamlProviderToRaw(y) {
|
|
218
|
+
const raw = {};
|
|
219
|
+
if (y.enabled !== void 0) raw.enabled = y.enabled;
|
|
220
|
+
if (y.url !== void 0) raw.baseUrl = y.url;
|
|
221
|
+
if (y.key_file !== void 0) raw.keyFile = y.key_file;
|
|
222
|
+
if (y.key !== void 0) raw.key = y.key;
|
|
223
|
+
if (y.tier !== void 0) raw.costTier = y.tier;
|
|
224
|
+
if (y.models !== void 0) raw.models = y.models;
|
|
225
|
+
if (y.protocol !== void 0) raw.protocol = y.protocol;
|
|
226
|
+
if (y.engine !== void 0) raw.engine = y.engine;
|
|
227
|
+
if (y.upstream_url !== void 0) raw.upstreamUrl = y.upstream_url;
|
|
228
|
+
if (y.env !== void 0) raw.env = y.env;
|
|
229
|
+
if (y.note !== void 0) raw.note = y.note;
|
|
230
|
+
if (y.quota_probe !== void 0) raw.quotaProbe = y.quota_probe;
|
|
231
|
+
if (y.quota_skip_at !== void 0) raw.quotaSkipAt = y.quota_skip_at;
|
|
232
|
+
if (y.context_window !== void 0) raw.contextWindow = y.context_window;
|
|
233
|
+
if (y.tags !== void 0) raw.tags = y.tags;
|
|
234
|
+
if (y.usage !== void 0) raw.usage = y.usage;
|
|
235
|
+
if (y.model_tiers !== void 0) raw.modelTiers = y.model_tiers;
|
|
236
|
+
return raw;
|
|
237
|
+
}
|
|
238
|
+
/** Internal WorkerProvider → the friendly on-disk plain object for writing. */
|
|
239
|
+
function providerToYamlPlain(p) {
|
|
240
|
+
const y = {};
|
|
241
|
+
if (!p.enabled) y.enabled = false;
|
|
242
|
+
if (p.baseUrl) y.url = p.baseUrl;
|
|
243
|
+
if (p.keyFile) y.key_file = p.keyFile;
|
|
244
|
+
if (p.key) y.key = p.key;
|
|
245
|
+
if (p.costTier !== void 0) y.tier = p.costTier;
|
|
246
|
+
y.models = { ...p.models };
|
|
247
|
+
if (p.protocol && p.protocol !== "anthropic") y.protocol = p.protocol;
|
|
248
|
+
if (p.engine && p.engine !== "claude") y.engine = p.engine;
|
|
249
|
+
if (p.upstreamUrl) y.upstream_url = p.upstreamUrl;
|
|
250
|
+
if (p.env && Object.keys(p.env).length) y.env = { ...p.env };
|
|
251
|
+
if (p.note) y.note = p.note;
|
|
252
|
+
if (p.quotaProbe) y.quota_probe = p.quotaProbe;
|
|
253
|
+
if (p.quotaSkipAt !== void 0) y.quota_skip_at = p.quotaSkipAt;
|
|
254
|
+
if (p.contextWindow !== void 0) y.context_window = p.contextWindow;
|
|
255
|
+
if (p.tags?.length) y.tags = [...p.tags];
|
|
256
|
+
if (p.usage) y.usage = p.usage;
|
|
257
|
+
if (p.modelTiers) y.model_tiers = { ...p.modelTiers };
|
|
258
|
+
return y;
|
|
259
|
+
}
|
|
260
|
+
/** The builtin `anthropic` block only ever carries `builtin: true` + `models`. */
|
|
261
|
+
function parseBuiltinAnthropic(raw, pathPrefix) {
|
|
262
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new WorkersConfigError(`${pathPrefix}: must be an object`);
|
|
263
|
+
const p = raw;
|
|
264
|
+
if (p.builtin !== true) throw new WorkersConfigError(`${pathPrefix}: "${ANTHROPIC_NATIVE}" is reserved for the built-in Claude Code login — set "builtin: true" (optionally with a "models" override) or rename this provider`);
|
|
265
|
+
const disallowed = [
|
|
266
|
+
"url",
|
|
267
|
+
"key_file",
|
|
268
|
+
"key",
|
|
269
|
+
"engine",
|
|
270
|
+
"protocol",
|
|
271
|
+
"upstream_url"
|
|
272
|
+
].filter((k) => p[k] !== void 0);
|
|
273
|
+
if (disallowed.length) throw new WorkersConfigError(`${pathPrefix}: a builtin provider cannot set ${disallowed.join(", ")} — it runs on Claude Code's own login`);
|
|
274
|
+
if (p.models === void 0) return { ...NATIVE_ANTHROPIC_MODELS };
|
|
275
|
+
return parseModelsBlock(`${pathPrefix}.models`, p.models);
|
|
276
|
+
}
|
|
277
|
+
function lineOf(doc, path) {
|
|
278
|
+
try {
|
|
279
|
+
const node = doc.getIn(path, true);
|
|
280
|
+
const range = node && typeof node === "object" && "range" in node ? node.range : void 0;
|
|
281
|
+
if (!range) return null;
|
|
282
|
+
return String(doc).slice(0, range[0]).split("\n").length;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function withLine(doc, path, yamlPath, e) {
|
|
288
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
289
|
+
const line = lineOf(doc, path);
|
|
290
|
+
throw new WorkersConfigError(line ? `${yamlPath}:${line}: ${msg}` : `${yamlPath}: ${msg}`);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Parse and fully validate a workers.yaml Document. Throws WorkersConfigError
|
|
294
|
+
* naming `<path>:<line>` for anything wrong — an unknown provider referenced
|
|
295
|
+
* by a class is a load error here, not a spawn-time surprise.
|
|
296
|
+
*/
|
|
297
|
+
function parseWorkersYamlDocument(doc, yamlPath) {
|
|
298
|
+
const root = doc.toJS() ?? {};
|
|
299
|
+
if (typeof root !== "object" || Array.isArray(root)) throw new WorkersConfigError(`${yamlPath}: top level must be a mapping`);
|
|
300
|
+
const r = root;
|
|
301
|
+
const providers = {};
|
|
302
|
+
let nativeModels = { ...NATIVE_ANTHROPIC_MODELS };
|
|
303
|
+
const providersRaw = r.providers;
|
|
304
|
+
if (providersRaw !== void 0) {
|
|
305
|
+
if (typeof providersRaw !== "object" || providersRaw === null || Array.isArray(providersRaw)) withLine(doc, ["providers"], yamlPath, new WorkersConfigError("providers: must be a mapping of name → provider"));
|
|
306
|
+
for (const [name, raw] of Object.entries(providersRaw)) try {
|
|
307
|
+
if (name === ANTHROPIC_NATIVE) nativeModels = parseBuiltinAnthropic(raw, `providers.${name}`);
|
|
308
|
+
else providers[name] = parseProvider(name, yamlProviderToRaw(raw));
|
|
309
|
+
} catch (e) {
|
|
310
|
+
withLine(doc, ["providers", name], yamlPath, e);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let classes = {};
|
|
314
|
+
if (r.classes !== void 0) {
|
|
315
|
+
try {
|
|
316
|
+
classes = parseClassesValue(r.classes, "classes");
|
|
317
|
+
} catch (e) {
|
|
318
|
+
withLine(doc, ["classes"], yamlPath, e);
|
|
319
|
+
}
|
|
320
|
+
for (const [cls, target] of Object.entries(classes)) {
|
|
321
|
+
const provider = typeof target === "string" ? target.split("/")[0] : target.provider;
|
|
322
|
+
const alias = typeof target === "string" ? target.split("/")[1] : void 0;
|
|
323
|
+
if (provider !== void 0) {
|
|
324
|
+
if (provider !== ANTHROPIC_NATIVE && !providers[provider]) withLine(doc, ["classes", cls], yamlPath, new WorkersConfigError(`classes.${cls}: no provider named "${provider}" (configured: ${Object.keys(providers).join(", ") || "(none)"})`));
|
|
325
|
+
if (alias !== void 0 && !isModelCapability(alias)) withLine(doc, ["classes", cls], yamlPath, new WorkersConfigError(`classes.${cls}: unknown model role "${alias}" (from: ${MODEL_CAPABILITIES.join(", ")})`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
let mcpSets = {};
|
|
330
|
+
if (r.mcp_sets !== void 0) try {
|
|
331
|
+
mcpSets = parseMcpSetsValue(r.mcp_sets, "mcp_sets");
|
|
332
|
+
} catch (e) {
|
|
333
|
+
withLine(doc, ["mcp_sets"], yamlPath, e);
|
|
334
|
+
}
|
|
335
|
+
else mcpSets = parseMcpSetsValue(void 0, "mcp_sets");
|
|
336
|
+
const active = r.active === void 0 || r.active === null ? null : String(r.active);
|
|
337
|
+
if (active !== null && active !== ANTHROPIC_NATIVE && active !== "auto" && !providers[active]) withLine(doc, ["active"], yamlPath, new WorkersConfigError(`active: no provider named "${active}" (configured: ${Object.keys(providers).join(", ") || "(none)"})`));
|
|
338
|
+
return {
|
|
339
|
+
active,
|
|
340
|
+
providers,
|
|
341
|
+
classes,
|
|
342
|
+
mcpSets,
|
|
343
|
+
nativeModels
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/** Read + parse workers.yaml. Returns null if the file does not exist. */
|
|
347
|
+
function readWorkersYaml(yamlPath) {
|
|
348
|
+
if (!existsSync(yamlPath)) return null;
|
|
349
|
+
let text;
|
|
350
|
+
try {
|
|
351
|
+
text = readFileSync(yamlPath, "utf8");
|
|
352
|
+
} catch (e) {
|
|
353
|
+
throw new WorkersConfigError(`Could not read ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
354
|
+
}
|
|
355
|
+
const doc = parseDocument(text);
|
|
356
|
+
if (doc.errors.length) throw new WorkersConfigError(`${yamlPath}: ${doc.errors[0].message}`);
|
|
357
|
+
return {
|
|
358
|
+
doc,
|
|
359
|
+
data: parseWorkersYamlDocument(doc, yamlPath)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function shallowEqual(a, b) {
|
|
363
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A `key:` value as an explicit double-quoted YAML scalar. A key is an
|
|
367
|
+
* opaque token, not YAML-authored text — plain-scalar auto-styling could
|
|
368
|
+
* read one back as a number or boolean (an all-digit token, "true", "no", …)
|
|
369
|
+
* if it were ever left unquoted, so every write forces the quoted form
|
|
370
|
+
* regardless of what the plain style would otherwise pick.
|
|
371
|
+
*/
|
|
372
|
+
function quotedKeyNode(value) {
|
|
373
|
+
const s = new Scalar(value);
|
|
374
|
+
s.type = Scalar.QUOTE_DOUBLE;
|
|
375
|
+
return s;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Apply the changes between `before` and `after` onto `doc`, touching only
|
|
379
|
+
* the providers/classes/mcp_sets/active entries that actually differ. This is
|
|
380
|
+
* what makes comment preservation possible: an untouched provider's node
|
|
381
|
+
* (and any comment above it) is never revisited.
|
|
382
|
+
*/
|
|
383
|
+
function syncWorkersYamlDocument(doc, before, after) {
|
|
384
|
+
for (const [name, p] of Object.entries(after.providers)) {
|
|
385
|
+
const beforeP = before.providers[name];
|
|
386
|
+
if (!beforeP || !shallowEqual(providerToYamlPlain(beforeP), providerToYamlPlain(p))) {
|
|
387
|
+
const y = providerToYamlPlain(p);
|
|
388
|
+
if (typeof y.key === "string") y.key = quotedKeyNode(y.key);
|
|
389
|
+
doc.setIn(["providers", name], y);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const name of Object.keys(before.providers)) if (!(name in after.providers)) doc.deleteIn(["providers", name]);
|
|
393
|
+
for (const [cls, target] of Object.entries(after.classes)) if (!shallowEqual(before.classes[cls], target)) doc.setIn(["classes", cls], target);
|
|
394
|
+
for (const cls of Object.keys(before.classes)) if (!(cls in after.classes)) doc.deleteIn(["classes", cls]);
|
|
395
|
+
for (const [set, servers] of Object.entries(after.mcpSets)) if (!shallowEqual(before.mcpSets[set], servers)) doc.setIn(["mcp_sets", set], [...servers]);
|
|
396
|
+
for (const set of Object.keys(before.mcpSets)) if (!(set in after.mcpSets)) doc.deleteIn(["mcp_sets", set]);
|
|
397
|
+
if (before.active !== after.active) if (after.active === null) doc.deleteIn(["active"]);
|
|
398
|
+
else doc.setIn(["active"], after.active);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Write `after` into workers.yaml, preserving comments on everything that did
|
|
402
|
+
* not change. Re-reads the file fresh (so the diff is against what is really
|
|
403
|
+
* on disk, not a stale in-memory copy) and re-validates the result before
|
|
404
|
+
* committing; on failure the previous bytes are left untouched.
|
|
405
|
+
*/
|
|
406
|
+
function writeWorkersYaml(yamlPath, after) {
|
|
407
|
+
const existing = readWorkersYaml(yamlPath);
|
|
408
|
+
const doc = existing ? existing.doc : new Document({});
|
|
409
|
+
syncWorkersYamlDocument(doc, existing ? existing.data : {
|
|
410
|
+
active: null,
|
|
411
|
+
providers: {},
|
|
412
|
+
classes: {},
|
|
413
|
+
mcpSets: {},
|
|
414
|
+
nativeModels: { ...NATIVE_ANTHROPIC_MODELS }
|
|
415
|
+
}, after);
|
|
416
|
+
writeWorkersYamlText(yamlPath, String(doc));
|
|
417
|
+
}
|
|
418
|
+
/** Atomic write with a .bak-pai backup and a re-validate-or-restore guard. */
|
|
419
|
+
function writeWorkersYamlText(yamlPath, text) {
|
|
420
|
+
const probe = parseDocument(text);
|
|
421
|
+
if (probe.errors.length) throw new WorkersConfigError(`refusing to write ${yamlPath}: ${probe.errors[0].message} (previous file left unchanged)`);
|
|
422
|
+
try {
|
|
423
|
+
parseWorkersYamlDocument(probe, yamlPath);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
throw new WorkersConfigError(`refusing to write ${yamlPath}: ${e instanceof Error ? e.message : String(e)} (previous file left unchanged)`);
|
|
426
|
+
}
|
|
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
|
+
try {
|
|
436
|
+
writeFileSync(tmp, text, {
|
|
437
|
+
encoding: "utf8",
|
|
438
|
+
mode: 384
|
|
439
|
+
});
|
|
440
|
+
renameSync(tmp, yamlPath);
|
|
441
|
+
chmodSync(yamlPath, 384);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
try {
|
|
444
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
445
|
+
} catch {}
|
|
446
|
+
throw new WorkersConfigError(`Failed to write ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** The exact commented starter shipped as examples/workers.yaml and by `init`. */
|
|
450
|
+
function starterWorkersYamlText() {
|
|
451
|
+
return `# PAI worker configuration.
|
|
452
|
+
# Providers PAI can run workers on, the model each role uses, and which
|
|
453
|
+
# provider every --class goes to. Edit by hand; \`pai worker providers\` shows
|
|
454
|
+
# the effective result. Comments are preserved when PAI writes this file.
|
|
455
|
+
#
|
|
456
|
+
# Per-user state, kept 0600: this file can hold API keys (\`key:\`, below).
|
|
457
|
+
# Never commit it or share it. \`key_file: <path>\` keeps a secret in its own
|
|
458
|
+
# 0600 file instead, if you'd rather not put it here.
|
|
459
|
+
|
|
460
|
+
active: anthropic # provider for \`pai worker run\` without --provider or --class
|
|
461
|
+
|
|
462
|
+
providers:
|
|
463
|
+
anthropic:
|
|
464
|
+
builtin: true # Claude Code's own login: no url, no key file
|
|
465
|
+
models:
|
|
466
|
+
default: ${NATIVE_ANTHROPIC_MODELS.default}
|
|
467
|
+
fast: ${NATIVE_ANTHROPIC_MODELS.fast}
|
|
468
|
+
glm:
|
|
469
|
+
url: https://api.z.ai/api/anthropic
|
|
470
|
+
key: "<your-api-key>" # or key_file: <path to a 0600 file>
|
|
471
|
+
tier: 3
|
|
472
|
+
models:
|
|
473
|
+
default: glm-5.3[1m]
|
|
474
|
+
fast: glm-5.3-flash
|
|
475
|
+
image: example-paint
|
|
476
|
+
kimi:
|
|
477
|
+
url: https://api.kimi.ai/coding/
|
|
478
|
+
key: "<your-api-key>" # or key_file: <path to a 0600 file>
|
|
479
|
+
tier: 3
|
|
480
|
+
models:
|
|
481
|
+
default: k3[1m]
|
|
482
|
+
fast: kimi-for-coding[1m]
|
|
483
|
+
|
|
484
|
+
# A class names a provider, or provider/role to pick a non-default model.
|
|
485
|
+
# Workers exist to parallelise and to save cost: the default is Sonnet,
|
|
486
|
+
# Haiku for the mechanical classes, and nothing here inherits the
|
|
487
|
+
# orchestrating session's model.
|
|
488
|
+
classes:
|
|
489
|
+
implement: anthropic
|
|
490
|
+
draft: anthropic
|
|
491
|
+
research: anthropic
|
|
492
|
+
complex: anthropic
|
|
493
|
+
plan: anthropic
|
|
494
|
+
review: anthropic
|
|
495
|
+
spotcheck: anthropic/fast
|
|
496
|
+
simple: anthropic/fast
|
|
497
|
+
image: glm/image
|
|
498
|
+
|
|
499
|
+
mcp_sets:
|
|
500
|
+
desktop: [clickr]
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Build a workers.yaml document from real (migrated) data, keeping the same
|
|
505
|
+
* header/section comments as the starter. Used by `pai worker config
|
|
506
|
+
* migrate` — `init` writes the literal starter instead, since it has no data
|
|
507
|
+
* to carry over.
|
|
508
|
+
*/
|
|
509
|
+
function buildWorkersYamlText(data) {
|
|
510
|
+
const lines = [];
|
|
511
|
+
lines.push("# PAI worker configuration.");
|
|
512
|
+
lines.push("# Providers PAI can run workers on, the model each role uses, and which");
|
|
513
|
+
lines.push("# provider every --class goes to. Edit by hand; `pai worker providers` shows");
|
|
514
|
+
lines.push("# the effective result. Comments are preserved when PAI writes this file.");
|
|
515
|
+
lines.push("");
|
|
516
|
+
lines.push(`active: ${data.active ?? "null"} # provider for \`pai worker run\` without --provider or --class`);
|
|
517
|
+
lines.push("");
|
|
518
|
+
lines.push("providers:");
|
|
519
|
+
lines.push(" anthropic:");
|
|
520
|
+
lines.push(" builtin: true # Claude Code's own login: no url, no key file");
|
|
521
|
+
lines.push(" models:");
|
|
522
|
+
lines.push(` default: ${data.nativeModels.default}`);
|
|
523
|
+
if (data.nativeModels.fast) lines.push(` fast: ${data.nativeModels.fast}`);
|
|
524
|
+
if (data.nativeModels.image) lines.push(` image: ${data.nativeModels.image}`);
|
|
525
|
+
for (const [name, p] of Object.entries(data.providers)) {
|
|
526
|
+
const y = providerToYamlPlain(p);
|
|
527
|
+
lines.push(` ${name}:`);
|
|
528
|
+
for (const [k, v] of Object.entries(y)) {
|
|
529
|
+
if (k === "models") continue;
|
|
530
|
+
lines.push(k === "key" ? ` key: ${JSON.stringify(String(v))}` : ` ${k}: ${yamlScalar(v)}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push(" models:");
|
|
533
|
+
for (const [k, v] of Object.entries(p.models)) lines.push(` ${k}: ${yamlScalar(v)}`);
|
|
534
|
+
}
|
|
535
|
+
lines.push("");
|
|
536
|
+
lines.push("# A class names a provider, or provider/role to pick a non-default model.");
|
|
537
|
+
lines.push("# Workers exist to parallelise and to save cost: the default is Sonnet,");
|
|
538
|
+
lines.push("# Haiku for the mechanical classes, and nothing here inherits the");
|
|
539
|
+
lines.push("# orchestrating session's model.");
|
|
540
|
+
const classEntries = Object.entries(data.classes);
|
|
541
|
+
lines.push(classEntries.length ? "classes:" : "classes: {}");
|
|
542
|
+
for (const [cls, target] of classEntries) lines.push(` ${cls}: ${typeof target === "string" ? target : yamlScalar(target)}`);
|
|
543
|
+
lines.push("");
|
|
544
|
+
const mcpEntries = Object.entries(data.mcpSets);
|
|
545
|
+
lines.push(mcpEntries.length ? "mcp_sets:" : "mcp_sets: {}");
|
|
546
|
+
for (const [set, servers] of mcpEntries) lines.push(` ${set}: [${servers.join(", ")}]`);
|
|
547
|
+
lines.push("");
|
|
548
|
+
return lines.join("\n");
|
|
549
|
+
}
|
|
550
|
+
/** Minimal scalar formatter for buildWorkersYamlText's flat key: value lines. */
|
|
551
|
+
function yamlScalar(v) {
|
|
552
|
+
if (typeof v === "string") return /^[\w./~\[\]-]+$/.test(v) ? v : JSON.stringify(v);
|
|
553
|
+
if (Array.isArray(v)) return `[${v.map(yamlScalar).join(", ")}]`;
|
|
554
|
+
if (v && typeof v === "object") return JSON.stringify(v);
|
|
555
|
+
return String(v);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* `pai worker config migrate`: reads the JSON `workers` section, writes
|
|
559
|
+
* workers.yaml with the starter's comments, backs the JSON section up to
|
|
560
|
+
* `workers.json.migrated-<date>` next to it, and strips providers/classes
|
|
561
|
+
* (and the legacy `roles`)/mcpSets/active from the JSON. Idempotent: refuses
|
|
562
|
+
* when workers.yaml already exists unless `force`. `dryRun` computes and
|
|
563
|
+
* returns the would-be YAML text without touching either file.
|
|
564
|
+
*/
|
|
565
|
+
function migrateWorkersToYaml(jsonPath, opts = {}) {
|
|
566
|
+
const existing = workersYamlPath();
|
|
567
|
+
if (existsSync(existing) && !opts.force) throw new WorkersConfigError(`${existing} already exists — refusing to overwrite without --force`);
|
|
568
|
+
const yamlPath = writeTargetWorkersYamlPath();
|
|
569
|
+
const raw = readJsonStrict(jsonPath, jsonPath);
|
|
570
|
+
const workers = parseWorkersConfig(raw.workers);
|
|
571
|
+
const yamlText = buildWorkersYamlText({
|
|
572
|
+
active: workers.active,
|
|
573
|
+
providers: workers.providers,
|
|
574
|
+
classes: workers.classes,
|
|
575
|
+
mcpSets: workers.mcpSets,
|
|
576
|
+
nativeModels: workers.nativeModels
|
|
577
|
+
});
|
|
578
|
+
if (opts.dryRun) return {
|
|
579
|
+
yamlPath,
|
|
580
|
+
yamlText,
|
|
581
|
+
backupPath: null,
|
|
582
|
+
dryRun: true
|
|
583
|
+
};
|
|
584
|
+
writeWorkersYamlText(yamlPath, yamlText);
|
|
585
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
586
|
+
const backupPath = join(dirname(jsonPath), `workers.json.migrated-${stamp}`);
|
|
587
|
+
writeFileSync(backupPath, JSON.stringify(raw.workers ?? {}, null, 2) + "\n", "utf8");
|
|
588
|
+
const { providers: _p, classes: _c, roles: _r, mcpSets: _m, active: _a, ...rest } = raw.workers ?? {};
|
|
589
|
+
raw.workers = rest;
|
|
590
|
+
writeJsonAtomic(jsonPath, raw, { label: jsonPath });
|
|
591
|
+
return {
|
|
592
|
+
yamlPath,
|
|
593
|
+
yamlText,
|
|
594
|
+
backupPath,
|
|
595
|
+
dryRun: false
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* `pai worker config init`: write the literal commented starter (no data to
|
|
600
|
+
* carry over — that is what `migrate` is for). Refuses if workers.yaml
|
|
601
|
+
* already exists.
|
|
602
|
+
*/
|
|
603
|
+
function initWorkersYaml() {
|
|
604
|
+
const existing = workersYamlPath();
|
|
605
|
+
if (existsSync(existing)) throw new WorkersConfigError(`${existing} already exists — edit it directly, or run \`pai worker config migrate --force\` to regenerate it from the JSON config`);
|
|
606
|
+
const yamlPath = writeTargetWorkersYamlPath();
|
|
607
|
+
writeWorkersYamlText(yamlPath, starterWorkersYamlText());
|
|
608
|
+
return yamlPath;
|
|
609
|
+
}
|
|
610
|
+
/** True when an old-location workers.yaml exists and nothing is at the new one yet. */
|
|
611
|
+
function needsWorkersYamlRelocation() {
|
|
612
|
+
return !process.env.PAI_WORKERS_YAML && !existsSync(defaultWorkersYamlPath()) && [oldWorkersYamlPath(), legacyWorkersYamlPath()].some((p) => existsSync(p));
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* `pai worker config migrate` when workers.yaml is still at an old location
|
|
616
|
+
* (~/.claude/workers.yaml or, older still, ~/.config/pai/workers.yaml): a
|
|
617
|
+
* byte-for-byte copy to the new path (comments, keys, everything — this only
|
|
618
|
+
* changes where the file lives), verified, then the old file is renamed
|
|
619
|
+
* aside as workers.yaml.migrated-<YYYYMMDD> (never deleted). No JSON
|
|
620
|
+
* involved, and no secrets are read or transformed beyond copying bytes.
|
|
621
|
+
*/
|
|
622
|
+
function relocateWorkersYaml(opts = {}) {
|
|
623
|
+
const toPath = writeTargetWorkersYamlPath();
|
|
624
|
+
const result = migratePaiFile(toPath, [oldWorkersYamlPath(), legacyWorkersYamlPath()], opts);
|
|
625
|
+
if (!result.dryRun && result.fromPath) chmodSync(toPath, 384);
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* `pai worker config inline-keys`: for every provider with a `key_file` and
|
|
630
|
+
* no `key`, read the file, write its trimmed contents as a quoted `key:`,
|
|
631
|
+
* and remove `key_file`. The key file itself is left on disk — this only
|
|
632
|
+
* changes what workers.yaml reads from, never deletes a credential the
|
|
633
|
+
* operator might still want. `dryRun` computes the plan (provider names and
|
|
634
|
+
* key file paths only — never the key values) without writing anything.
|
|
635
|
+
*/
|
|
636
|
+
function inlineWorkersYamlKeys(opts = {}) {
|
|
637
|
+
const yamlPath = workersYamlPath();
|
|
638
|
+
const existing = readWorkersYaml(yamlPath);
|
|
639
|
+
if (!existing) throw new WorkersConfigError(`${yamlPath} does not exist — run \`pai worker config init\` first`);
|
|
640
|
+
const { doc, data } = existing;
|
|
641
|
+
const inlined = [];
|
|
642
|
+
for (const [name, p] of Object.entries(data.providers)) {
|
|
643
|
+
if (p.key || !p.keyFile) continue;
|
|
644
|
+
const keyPath = expandHome(p.keyFile);
|
|
645
|
+
let content;
|
|
646
|
+
try {
|
|
647
|
+
content = readFileSync(keyPath, "utf8");
|
|
648
|
+
} catch (e) {
|
|
649
|
+
throw new WorkersConfigError(`providers.${name}.key_file: could not read ${keyPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
650
|
+
}
|
|
651
|
+
const token = content.trim();
|
|
652
|
+
if (!token) throw new WorkersConfigError(`providers.${name}.key_file: ${keyPath} is empty`);
|
|
653
|
+
inlined.push({
|
|
654
|
+
provider: name,
|
|
655
|
+
keyFilePath: p.keyFile
|
|
656
|
+
});
|
|
657
|
+
if (!opts.dryRun) {
|
|
658
|
+
doc.setIn([
|
|
659
|
+
"providers",
|
|
660
|
+
name,
|
|
661
|
+
"key"
|
|
662
|
+
], quotedKeyNode(token));
|
|
663
|
+
doc.deleteIn([
|
|
664
|
+
"providers",
|
|
665
|
+
name,
|
|
666
|
+
"key_file"
|
|
667
|
+
]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!opts.dryRun && inlined.length) writeWorkersYamlText(yamlPath, String(doc));
|
|
671
|
+
return {
|
|
672
|
+
yamlPath,
|
|
673
|
+
inlined,
|
|
674
|
+
dryRun: !!opts.dryRun
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/workers/config.ts
|
|
680
|
+
/**
|
|
681
|
+
* config.ts — the `workers` section of the PAI config file (CONFIG_FILE,
|
|
682
|
+
* see src/daemon/config.ts's paiConfigFilePath — ~/.claude/pai/config.json
|
|
683
|
+
* today, pre-2026-09-19 ~/.config/pai/config.json).
|
|
684
|
+
*
|
|
685
|
+
* Everything that knows about worker providers reads this module: the CLI
|
|
686
|
+
* (`pai worker …`), the MCP tools (worker_*), and the Agent-routing hook.
|
|
687
|
+
* Keep it free of commander/MCP imports so all three layers stay thin over it.
|
|
688
|
+
*
|
|
689
|
+
* Keys are never stored here — only paths to 0600 files. A provider without a
|
|
690
|
+
* keyFile is a local server and gets the placeholder token "local".
|
|
691
|
+
*
|
|
692
|
+
* `protocol: "openai"` providers run through the PAI proxy (src/workers/proxy):
|
|
693
|
+
* `upstreamUrl` is their Chat Completions base, and `run` points
|
|
694
|
+
* ANTHROPIC_BASE_URL at the local proxy with the provider name in the path.
|
|
695
|
+
* `engine: "codex"` providers run through the Codex CLI instead of Claude
|
|
696
|
+
* Code.
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Reserved provider name for plain Anthropic: Claude Code's own OAuth/Max-plan
|
|
700
|
+
* login, no base URL override and no API key. It never needs (or accepts) a
|
|
701
|
+
* `providers.anthropic` config entry — resolveTarget/mustExist synthesize it
|
|
702
|
+
* on demand via `nativeAnthropicProvider()`.
|
|
703
|
+
*/
|
|
704
|
+
const ANTHROPIC_NATIVE = "anthropic";
|
|
705
|
+
/**
|
|
706
|
+
* Models the built-in provider runs on. This is the only place a model id is
|
|
707
|
+
* named for it: run.ts resolves the class capability against this table like
|
|
708
|
+
* it does for any configured provider and passes `--model` explicitly.
|
|
709
|
+
*
|
|
710
|
+
* Workers exist to parallelise and to save cost, so they must never inherit
|
|
711
|
+
* the orchestrator's model. A bare headless `claude` takes the interactive
|
|
712
|
+
* session default, and one probe came up on the most expensive tier because
|
|
713
|
+
* the chat session had been switched to it. Sonnet is the working tier;
|
|
714
|
+
* haiku serves the fast classes (spotcheck, simple).
|
|
715
|
+
*/
|
|
716
|
+
const NATIVE_ANTHROPIC_MODELS = {
|
|
717
|
+
default: "claude-sonnet-5",
|
|
718
|
+
fast: "claude-haiku-4-5-20251001"
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* The synthetic provider object for ANTHROPIC_NATIVE. `baseUrl` is empty and
|
|
722
|
+
* there is no key file, so buildRunEnv strips every provider env override;
|
|
723
|
+
* the models come from NATIVE_ANTHROPIC_MODELS so a run always names its
|
|
724
|
+
* model on the command line.
|
|
725
|
+
*/
|
|
726
|
+
function nativeAnthropicProvider(models = NATIVE_ANTHROPIC_MODELS) {
|
|
727
|
+
return {
|
|
728
|
+
enabled: true,
|
|
729
|
+
protocol: "anthropic",
|
|
730
|
+
baseUrl: "",
|
|
731
|
+
keyFile: null,
|
|
732
|
+
models: { ...models },
|
|
733
|
+
env: {},
|
|
734
|
+
native: true
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Provider by name, synthesizing ANTHROPIC_NATIVE when it is asked for. Reads
|
|
739
|
+
* `config.nativeModels` (workers.yaml's documented `providers.anthropic.models`
|
|
740
|
+
* override, if any) so a config that pins a different default/fast id for the
|
|
741
|
+
* built-in provider is honored everywhere a provider is resolved.
|
|
742
|
+
*/
|
|
743
|
+
function getProviderOrNative(config, name) {
|
|
744
|
+
if (name === ANTHROPIC_NATIVE) return nativeAnthropicProvider(config.nativeModels);
|
|
745
|
+
return config.providers[name];
|
|
746
|
+
}
|
|
747
|
+
const MODEL_TIERS = [
|
|
748
|
+
"haiku",
|
|
749
|
+
"sonnet",
|
|
750
|
+
"opus"
|
|
751
|
+
];
|
|
752
|
+
const DEFAULT_COST_TIER = 3;
|
|
753
|
+
/** Tags a provider may carry; classes filter auto-routing on them. */
|
|
754
|
+
const PROVIDER_TAGS = [
|
|
755
|
+
"code",
|
|
756
|
+
"vision",
|
|
757
|
+
"image-gen",
|
|
758
|
+
"long-context",
|
|
759
|
+
"fast",
|
|
760
|
+
"reasoning"
|
|
761
|
+
];
|
|
762
|
+
/** The standard task classes; `workers.classes` maps each to a target. */
|
|
763
|
+
const WORKER_CLASSES = [
|
|
764
|
+
"draft",
|
|
765
|
+
"plan",
|
|
766
|
+
"implement",
|
|
767
|
+
"review",
|
|
768
|
+
"research",
|
|
769
|
+
"spotcheck",
|
|
770
|
+
"simple",
|
|
771
|
+
"complex",
|
|
772
|
+
"image"
|
|
773
|
+
];
|
|
774
|
+
/**
|
|
775
|
+
* The named model capabilities a provider may carry a preference for, set
|
|
776
|
+
* per provider under `workers.providers.<name>.models`. "default" is the
|
|
777
|
+
* required catch-all; the others name what a model is *for* — "fast" the
|
|
778
|
+
* cheap tier (spotcheck, haiku-tier spawns), "image" the image class.
|
|
779
|
+
* Everything resolves through resolveModelCapability, falling back to default.
|
|
780
|
+
*/
|
|
781
|
+
const MODEL_CAPABILITIES = [
|
|
782
|
+
"default",
|
|
783
|
+
"fast",
|
|
784
|
+
"image"
|
|
785
|
+
];
|
|
786
|
+
/** Is this string the name of a known model capability? */
|
|
787
|
+
function isModelCapability(v) {
|
|
788
|
+
return MODEL_CAPABILITIES.includes(v);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Which model capability a class runs on when its target names no alias: the
|
|
792
|
+
* image class uses the image model, the cheap classes (spotcheck, simple) the
|
|
793
|
+
* fast model, every other class the provider default. A provider without a
|
|
794
|
+
* fast model falls back to its default (resolveModelCapability).
|
|
795
|
+
*/
|
|
796
|
+
const CLASS_MODEL_CAPABILITY = {
|
|
797
|
+
draft: "default",
|
|
798
|
+
plan: "default",
|
|
799
|
+
implement: "default",
|
|
800
|
+
review: "default",
|
|
801
|
+
research: "default",
|
|
802
|
+
spotcheck: "fast",
|
|
803
|
+
simple: "fast",
|
|
804
|
+
complex: "default",
|
|
805
|
+
image: "image"
|
|
806
|
+
};
|
|
807
|
+
/** Capability for a run's class (default when unset or not a standard class). */
|
|
808
|
+
function classModelCapability(className) {
|
|
809
|
+
return className && CLASS_MODEL_CAPABILITY[className] || "default";
|
|
810
|
+
}
|
|
811
|
+
const DEFAULT_LOG_DIR = "~/.claude/logs/workers";
|
|
812
|
+
const DEFAULT_PANE = {
|
|
813
|
+
enabled: true,
|
|
814
|
+
fontSize: 13,
|
|
815
|
+
autoExitSecs: 60
|
|
816
|
+
};
|
|
817
|
+
const DEFAULT_ROUTING = {
|
|
818
|
+
order: [],
|
|
819
|
+
cooldownMinutes: 30,
|
|
820
|
+
retryOnQuota: true
|
|
821
|
+
};
|
|
822
|
+
const DEFAULT_TREE = {
|
|
823
|
+
maxDepth: 2,
|
|
824
|
+
maxChildren: 4
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Cache-keepalive cadence when the config names none. Measured 2026-09-18
|
|
828
|
+
* (pair probes through the real worker spawn path, numbers in
|
|
829
|
+
* docs/cache-keepalive.md): the implicit provider cache only serves a fresh
|
|
830
|
+
* worker warm within roughly the first minute (positive back-to-back, gone
|
|
831
|
+
* at 2 min), and it covers only ~2.6k of the ~18k-token prefix. Holding it
|
|
832
|
+
* needs a beat every <2 min — a continuous bill for a small saving — so the
|
|
833
|
+
* default is OFF and arming it is the operator's explicit call (set e.g.
|
|
834
|
+
* "cacheKeepaliveSecs": 60).
|
|
835
|
+
*/
|
|
836
|
+
const DEFAULT_CACHE_KEEPALIVE_SECS = 0;
|
|
837
|
+
/**
|
|
838
|
+
* The default MCP sets every config starts from. `desktop` names the clickr
|
|
839
|
+
* server so `--mcp desktop` hands a worker the machine controls (read-only
|
|
840
|
+
* tools always; actuating ones after the operator hands the controls over,
|
|
841
|
+
* see `pai worker controls`). A user's mcpSets section is merged over this,
|
|
842
|
+
* so `desktop: []` removes the set deliberately.
|
|
843
|
+
*/
|
|
844
|
+
const DEFAULT_MCP_SETS = { desktop: ["clickr"] };
|
|
845
|
+
function defaultWorkersConfig() {
|
|
846
|
+
return {
|
|
847
|
+
enabled: false,
|
|
848
|
+
active: null,
|
|
849
|
+
providers: {},
|
|
850
|
+
classes: {},
|
|
851
|
+
mcpSets: { ...DEFAULT_MCP_SETS },
|
|
852
|
+
pane: { ...DEFAULT_PANE },
|
|
853
|
+
logDir: DEFAULT_LOG_DIR,
|
|
854
|
+
routing: {
|
|
855
|
+
...DEFAULT_ROUTING,
|
|
856
|
+
order: []
|
|
857
|
+
},
|
|
858
|
+
tree: { ...DEFAULT_TREE },
|
|
859
|
+
cacheKeepaliveSecs: DEFAULT_CACHE_KEEPALIVE_SECS,
|
|
860
|
+
fallback: null,
|
|
861
|
+
nativeModels: { ...NATIVE_ANTHROPIC_MODELS }
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var WorkersConfigError = class extends Error {};
|
|
865
|
+
function bad(path, why) {
|
|
866
|
+
throw new WorkersConfigError(`workers${path}: ${why}`);
|
|
867
|
+
}
|
|
868
|
+
function str(v) {
|
|
869
|
+
return typeof v === "string" ? v : "";
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Parse a `models` block (capability → model id, "default" required). Shared
|
|
873
|
+
* by parseProvider and the YAML loader's builtin-anthropic override, which
|
|
874
|
+
* has no other provider fields to validate.
|
|
875
|
+
*/
|
|
876
|
+
function parseModelsBlock(pathPrefix, modelsRaw) {
|
|
877
|
+
const raw = modelsRaw === void 0 ? {} : modelsRaw;
|
|
878
|
+
if (typeof raw !== "object" || raw === null) bad(pathPrefix, "must be an object");
|
|
879
|
+
const m = raw;
|
|
880
|
+
const defaultModel = str(m.default);
|
|
881
|
+
if (!defaultModel) bad(`${pathPrefix}.default`, "is required");
|
|
882
|
+
const models = { default: defaultModel };
|
|
883
|
+
for (const key of Object.keys(m)) {
|
|
884
|
+
if (key === "default") continue;
|
|
885
|
+
if (!isModelCapability(key)) bad(`${pathPrefix}.${key}`, `"${key}" is not a model capability (from: ${MODEL_CAPABILITIES.join(", ")})`);
|
|
886
|
+
const id = str(m[key]);
|
|
887
|
+
if (!id) bad(`${pathPrefix}.${key}`, "must be a non-empty model id");
|
|
888
|
+
models[key] = id;
|
|
889
|
+
}
|
|
890
|
+
return models;
|
|
891
|
+
}
|
|
892
|
+
/** Parse and validate one provider entry (shared by JSON and YAML loading). */
|
|
893
|
+
function parseProvider(name, raw) {
|
|
894
|
+
if (name === ANTHROPIC_NATIVE) bad(`.providers.${name}`, `"${ANTHROPIC_NATIVE}" is reserved for plain Anthropic (Claude Code's own OAuth/Max-plan login) and cannot be configured here — remove this entry; use --provider ${ANTHROPIC_NATIVE} or "pai worker providers use ${ANTHROPIC_NATIVE}" instead`);
|
|
895
|
+
if (typeof raw !== "object" || raw === null) bad(`.providers.${name}`, "must be an object");
|
|
896
|
+
const p = raw;
|
|
897
|
+
const protocol = p.protocol === void 0 ? "anthropic" : str(p.protocol);
|
|
898
|
+
if (protocol !== "anthropic" && protocol !== "openai") bad(`.providers.${name}.protocol`, `"${str(p.protocol)}" is neither "anthropic" nor "openai"`);
|
|
899
|
+
const engine = p.engine === void 0 ? "claude" : str(p.engine);
|
|
900
|
+
if (engine !== "claude" && engine !== "codex") bad(`.providers.${name}.engine`, `"${str(p.engine)}" is neither "claude" nor "codex"`);
|
|
901
|
+
const baseUrl = str(p.baseUrl);
|
|
902
|
+
if (!baseUrl && protocol !== "openai") bad(`.providers.${name}.baseUrl`, "is required");
|
|
903
|
+
const keyFile = p.keyFile === void 0 || p.keyFile === null || str(p.keyFile) === "" ? null : str(p.keyFile);
|
|
904
|
+
const key = str(p.key) || void 0;
|
|
905
|
+
const models = parseModelsBlock(`.providers.${name}.models`, p.models);
|
|
906
|
+
let modelTiers;
|
|
907
|
+
if (p.modelTiers !== void 0) {
|
|
908
|
+
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");
|
|
909
|
+
modelTiers = {};
|
|
910
|
+
for (const [id, tier] of Object.entries(p.modelTiers)) {
|
|
911
|
+
if (typeof tier !== "string" || !MODEL_TIERS.includes(tier)) bad(`.providers.${name}.modelTiers.${id}`, `must be "haiku", "sonnet" or "opus" (got ${JSON.stringify(tier)})`);
|
|
912
|
+
modelTiers[id] = tier;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const env = {};
|
|
916
|
+
if (p.env !== void 0) {
|
|
917
|
+
if (typeof p.env !== "object" || p.env === null || Array.isArray(p.env)) bad(`.providers.${name}.env`, "must be an object of string values");
|
|
918
|
+
for (const [k, v] of Object.entries(p.env)) {
|
|
919
|
+
if (typeof v !== "string") bad(`.providers.${name}.env.${k}`, "must be a string");
|
|
920
|
+
env[k] = v;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const quotaSkipAt = p.quotaSkipAt === void 0 ? void 0 : p.quotaSkipAt;
|
|
924
|
+
if (quotaSkipAt !== void 0) {
|
|
925
|
+
if (typeof quotaSkipAt !== "number" || quotaSkipAt < 0 || quotaSkipAt > 100) bad(`.providers.${name}.quotaSkipAt`, "must be a number between 0 and 100");
|
|
926
|
+
}
|
|
927
|
+
const contextWindow = p.contextWindow === void 0 ? void 0 : p.contextWindow;
|
|
928
|
+
if (contextWindow !== void 0) {
|
|
929
|
+
if (typeof contextWindow !== "number" || contextWindow <= 0) bad(`.providers.${name}.contextWindow`, "must be a positive number of tokens");
|
|
930
|
+
}
|
|
931
|
+
const upstreamUrl = str(p.upstreamUrl);
|
|
932
|
+
if (protocol === "openai" && !upstreamUrl) bad(`.providers.${name}.upstreamUrl`, `is required for protocol "openai" (the Chat Completions base, e.g. "https://api.openai.com/v1")`);
|
|
933
|
+
const costTier = p.costTier === void 0 ? void 0 : p.costTier;
|
|
934
|
+
if (costTier !== void 0) {
|
|
935
|
+
if (typeof costTier !== "number" || !Number.isInteger(costTier) || costTier < 1 || costTier > 5) bad(`.providers.${name}.costTier`, "must be an integer 1 (cheapest) … 5 (most expensive)");
|
|
936
|
+
}
|
|
937
|
+
let tags;
|
|
938
|
+
if (p.tags !== void 0) {
|
|
939
|
+
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(", ")}`);
|
|
940
|
+
for (const t of p.tags) if (!PROVIDER_TAGS.includes(t)) bad(`.providers.${name}.tags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
|
|
941
|
+
tags = p.tags;
|
|
942
|
+
}
|
|
943
|
+
let usage;
|
|
944
|
+
if (p.usage !== void 0) {
|
|
945
|
+
if (typeof p.usage !== "object" || p.usage === null || Array.isArray(p.usage)) bad(`.providers.${name}.usage`, "must be an object");
|
|
946
|
+
const u = p.usage;
|
|
947
|
+
const usageUrl = str(u.url);
|
|
948
|
+
if (!usageUrl) bad(`.providers.${name}.usage.url`, "is required");
|
|
949
|
+
if (!Array.isArray(u.windows) || u.windows.length === 0) bad(`.providers.${name}.usage.windows`, "must be a non-empty array");
|
|
950
|
+
const windows = [];
|
|
951
|
+
for (const [i, wRaw] of u.windows.entries()) {
|
|
952
|
+
const wpath = `.providers.${name}.usage.windows[${i}]`;
|
|
953
|
+
if (typeof wRaw !== "object" || wRaw === null || Array.isArray(wRaw)) bad(wpath, "must be an object");
|
|
954
|
+
const w = wRaw;
|
|
955
|
+
const wname = str(w.name);
|
|
956
|
+
if (!wname) bad(`${wpath}.name`, "is required");
|
|
957
|
+
const percent = str(w.percent);
|
|
958
|
+
if (!percent) bad(`${wpath}.percent`, "is required (a jq expression yielding 0–100)");
|
|
959
|
+
const resetUnit = w.resetUnit === void 0 ? void 0 : str(w.resetUnit);
|
|
960
|
+
if (resetUnit !== void 0 && resetUnit !== "ms" && resetUnit !== "s" && resetUnit !== "iso") bad(`${wpath}.resetUnit`, `must be "ms", "s" or "iso"`);
|
|
961
|
+
windows.push({
|
|
962
|
+
name: wname,
|
|
963
|
+
percent,
|
|
964
|
+
...str(w.resetAt) ? { resetAt: str(w.resetAt) } : {},
|
|
965
|
+
...resetUnit ? { resetUnit } : {}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
const ttlSeconds = u.ttlSeconds === void 0 ? void 0 : u.ttlSeconds;
|
|
969
|
+
if (ttlSeconds !== void 0 && (typeof ttlSeconds !== "number" || ttlSeconds <= 0)) bad(`.providers.${name}.usage.ttlSeconds`, "must be a positive number of seconds");
|
|
970
|
+
usage = {
|
|
971
|
+
url: usageUrl,
|
|
972
|
+
...str(u.authHeader) ? { authHeader: str(u.authHeader) } : {},
|
|
973
|
+
...str(u.label) ? { label: str(u.label) } : {},
|
|
974
|
+
windows,
|
|
975
|
+
...ttlSeconds !== void 0 ? { ttlSeconds } : {}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
enabled: p.enabled === void 0 ? true : p.enabled === true,
|
|
980
|
+
protocol,
|
|
981
|
+
baseUrl,
|
|
982
|
+
keyFile,
|
|
983
|
+
...key ? { key } : {},
|
|
984
|
+
models,
|
|
985
|
+
...modelTiers ? { modelTiers } : {},
|
|
986
|
+
env,
|
|
987
|
+
...str(p.note) ? { note: str(p.note) } : {},
|
|
988
|
+
...upstreamUrl ? { upstreamUrl } : {},
|
|
989
|
+
...engine !== "claude" ? { engine } : {},
|
|
990
|
+
...str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {},
|
|
991
|
+
...quotaSkipAt !== void 0 ? { quotaSkipAt } : {},
|
|
992
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
993
|
+
...costTier !== void 0 ? { costTier } : {},
|
|
994
|
+
...tags ? { tags } : {},
|
|
995
|
+
...usage ? { usage } : {}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Parse a `classes` (or legacy `roles`) value shared by JSON and YAML
|
|
1000
|
+
* loading. `badPathPrefix` names the field in error messages, e.g. ".classes".
|
|
1001
|
+
*/
|
|
1002
|
+
function parseClassesValue(classesRaw, badPathPrefix) {
|
|
1003
|
+
const classes = {};
|
|
1004
|
+
if (classesRaw === void 0) return classes;
|
|
1005
|
+
if (typeof classesRaw !== "object" || classesRaw === null || Array.isArray(classesRaw)) bad(badPathPrefix, "must be an object of class → provider[/alias] or {provider, mcp, maxCostTier, requireTags, order}");
|
|
1006
|
+
for (const [cls, target] of Object.entries(classesRaw)) if (typeof target === "object" && target !== null && !Array.isArray(target)) {
|
|
1007
|
+
const o = target;
|
|
1008
|
+
const provider = str(o.provider);
|
|
1009
|
+
if (o.provider !== void 0 && (!provider || provider.includes(" "))) bad(`${badPathPrefix}.${cls}.provider`, `invalid provider "${provider}"`);
|
|
1010
|
+
let mcp;
|
|
1011
|
+
if (o.mcp !== void 0) {
|
|
1012
|
+
if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) bad(`${badPathPrefix}.${cls}.mcp`, "must be an array of MCP server or set names");
|
|
1013
|
+
mcp = o.mcp;
|
|
1014
|
+
}
|
|
1015
|
+
let maxCostTier;
|
|
1016
|
+
if (o.maxCostTier !== void 0) {
|
|
1017
|
+
if (typeof o.maxCostTier !== "number" || !Number.isInteger(o.maxCostTier) || o.maxCostTier < 1 || o.maxCostTier > 5) bad(`${badPathPrefix}.${cls}.maxCostTier`, "must be an integer 1 … 5");
|
|
1018
|
+
maxCostTier = o.maxCostTier;
|
|
1019
|
+
}
|
|
1020
|
+
let requireTags;
|
|
1021
|
+
if (o.requireTags !== void 0) {
|
|
1022
|
+
if (!Array.isArray(o.requireTags) || o.requireTags.some((x) => typeof x !== "string")) bad(`${badPathPrefix}.${cls}.requireTags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
|
|
1023
|
+
for (const t of o.requireTags) if (!PROVIDER_TAGS.includes(t)) bad(`${badPathPrefix}.${cls}.requireTags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
|
|
1024
|
+
requireTags = o.requireTags;
|
|
1025
|
+
}
|
|
1026
|
+
let order;
|
|
1027
|
+
if (o.order !== void 0) {
|
|
1028
|
+
if (!Array.isArray(o.order) || o.order.some((x) => typeof x !== "string")) bad(`${badPathPrefix}.${cls}.order`, "must be an array of provider names");
|
|
1029
|
+
order = o.order;
|
|
1030
|
+
}
|
|
1031
|
+
const obj = {
|
|
1032
|
+
...provider ? { provider } : {},
|
|
1033
|
+
...mcp ? { mcp } : {},
|
|
1034
|
+
...maxCostTier !== void 0 ? { maxCostTier } : {},
|
|
1035
|
+
...requireTags ? { requireTags } : {},
|
|
1036
|
+
...order ? { order } : {}
|
|
1037
|
+
};
|
|
1038
|
+
classes[cls] = Object.keys(obj).length ? obj : {};
|
|
1039
|
+
} else {
|
|
1040
|
+
const t = str(target);
|
|
1041
|
+
if (!t || t.includes(" ")) bad(`${badPathPrefix}.${cls}`, `invalid target "${t}"`);
|
|
1042
|
+
classes[cls] = t;
|
|
1043
|
+
}
|
|
1044
|
+
return classes;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Parse an `mcpSets` (or `mcp_sets`) value shared by JSON and YAML loading.
|
|
1048
|
+
* Always merged over DEFAULT_MCP_SETS, so an omitted `desktop` key keeps the
|
|
1049
|
+
* built-in clickr set (see DEFAULT_MCP_SETS).
|
|
1050
|
+
*/
|
|
1051
|
+
function parseMcpSetsValue(raw, badPathPrefix) {
|
|
1052
|
+
const mcpSets = { ...DEFAULT_MCP_SETS };
|
|
1053
|
+
if (raw === void 0) return mcpSets;
|
|
1054
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) bad(badPathPrefix, "must be an object of set name → [server names]");
|
|
1055
|
+
for (const [setName, servers] of Object.entries(raw)) {
|
|
1056
|
+
if (!Array.isArray(servers) || servers.some((x) => typeof x !== "string")) bad(`${badPathPrefix}.${setName}`, "must be an array of MCP server names");
|
|
1057
|
+
mcpSets[setName] = servers;
|
|
1058
|
+
}
|
|
1059
|
+
return mcpSets;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Parse and validate a raw `workers` value. Missing section → defaults.
|
|
1063
|
+
* Unknown-but-typed garbage → WorkersConfigError naming the offending field.
|
|
1064
|
+
*/
|
|
1065
|
+
function parseWorkersConfig(raw) {
|
|
1066
|
+
const d = defaultWorkersConfig();
|
|
1067
|
+
if (raw === void 0 || raw === null) return d;
|
|
1068
|
+
if (typeof raw !== "object" || Array.isArray(raw)) bad("", "section must be an object");
|
|
1069
|
+
const w = raw;
|
|
1070
|
+
const providers = {};
|
|
1071
|
+
if (w.providers !== void 0) {
|
|
1072
|
+
if (typeof w.providers !== "object" || w.providers === null || Array.isArray(w.providers)) bad(".providers", "must be an object keyed by provider name");
|
|
1073
|
+
for (const [name, p] of Object.entries(w.providers)) providers[name] = parseProvider(name, p);
|
|
1074
|
+
}
|
|
1075
|
+
const classes = parseClassesValue(w.classes !== void 0 ? w.classes : w.roles, w.classes !== void 0 ? ".classes" : ".roles");
|
|
1076
|
+
const mcpSets = parseMcpSetsValue(w.mcpSets, ".mcpSets");
|
|
1077
|
+
let pane = { ...DEFAULT_PANE };
|
|
1078
|
+
if (w.pane !== void 0) {
|
|
1079
|
+
if (typeof w.pane !== "object" || w.pane === null) bad(".pane", "must be an object");
|
|
1080
|
+
const pc = w.pane;
|
|
1081
|
+
if (pc.enabled !== void 0 && typeof pc.enabled !== "boolean") bad(".pane.enabled", "must be boolean");
|
|
1082
|
+
if (pc.fontSize !== void 0 && (typeof pc.fontSize !== "number" || pc.fontSize <= 0)) bad(".pane.fontSize", "must be a positive number of points");
|
|
1083
|
+
if (pc.autoExitSecs !== void 0 && typeof pc.autoExitSecs !== "number") bad(".pane.autoExitSecs", "must be a number");
|
|
1084
|
+
pane = {
|
|
1085
|
+
enabled: pc.enabled === void 0 ? DEFAULT_PANE.enabled : pc.enabled === true,
|
|
1086
|
+
fontSize: pc.fontSize === void 0 ? DEFAULT_PANE.fontSize : pc.fontSize,
|
|
1087
|
+
autoExitSecs: pc.autoExitSecs === void 0 ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
let routing = {
|
|
1091
|
+
...DEFAULT_ROUTING,
|
|
1092
|
+
order: []
|
|
1093
|
+
};
|
|
1094
|
+
if (w.routing !== void 0) {
|
|
1095
|
+
if (typeof w.routing !== "object" || w.routing === null) bad(".routing", "must be an object");
|
|
1096
|
+
const r = w.routing;
|
|
1097
|
+
if (r.order !== void 0) {
|
|
1098
|
+
if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== "string")) bad(".routing.order", "must be an array of provider names");
|
|
1099
|
+
routing.order = r.order;
|
|
1100
|
+
}
|
|
1101
|
+
if (r.cooldownMinutes !== void 0 && typeof r.cooldownMinutes !== "number") bad(".routing.cooldownMinutes", "must be a number");
|
|
1102
|
+
if (r.retryOnQuota !== void 0 && typeof r.retryOnQuota !== "boolean") bad(".routing.retryOnQuota", "must be boolean");
|
|
1103
|
+
routing = {
|
|
1104
|
+
order: routing.order,
|
|
1105
|
+
cooldownMinutes: r.cooldownMinutes === void 0 ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,
|
|
1106
|
+
retryOnQuota: r.retryOnQuota === void 0 ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
let tree = { ...DEFAULT_TREE };
|
|
1110
|
+
if (w.tree !== void 0) {
|
|
1111
|
+
if (typeof w.tree !== "object" || w.tree === null || Array.isArray(w.tree)) bad(".tree", "must be an object");
|
|
1112
|
+
const t = w.tree;
|
|
1113
|
+
if (t.maxDepth !== void 0) {
|
|
1114
|
+
if (typeof t.maxDepth !== "number" || !Number.isInteger(t.maxDepth) || t.maxDepth < 0) bad(".tree.maxDepth", "must be a non-negative integer");
|
|
1115
|
+
}
|
|
1116
|
+
if (t.maxChildren !== void 0) {
|
|
1117
|
+
if (typeof t.maxChildren !== "number" || !Number.isInteger(t.maxChildren) || t.maxChildren < 1) bad(".tree.maxChildren", "must be a positive integer");
|
|
1118
|
+
}
|
|
1119
|
+
tree = {
|
|
1120
|
+
maxDepth: t.maxDepth === void 0 ? DEFAULT_TREE.maxDepth : t.maxDepth,
|
|
1121
|
+
maxChildren: t.maxChildren === void 0 ? DEFAULT_TREE.maxChildren : t.maxChildren
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
let cacheKeepaliveSecs = DEFAULT_CACHE_KEEPALIVE_SECS;
|
|
1125
|
+
if (w.cacheKeepaliveSecs !== void 0) {
|
|
1126
|
+
if (typeof w.cacheKeepaliveSecs !== "number" || !Number.isInteger(w.cacheKeepaliveSecs) || w.cacheKeepaliveSecs < 0) bad(".cacheKeepaliveSecs", "must be a non-negative integer (seconds, 0 = off)");
|
|
1127
|
+
cacheKeepaliveSecs = w.cacheKeepaliveSecs;
|
|
1128
|
+
}
|
|
1129
|
+
const active = w.active === void 0 || w.active === null ? null : str(w.active);
|
|
1130
|
+
if (active !== null && active !== "auto" && !(active in providers)) {}
|
|
1131
|
+
let fallback = null;
|
|
1132
|
+
if (w.fallback !== void 0 && w.fallback !== null) {
|
|
1133
|
+
if (typeof w.fallback !== "object" || Array.isArray(w.fallback)) bad(".fallback", "must be an object (written by `pai worker fallback on`)");
|
|
1134
|
+
const f = w.fallback;
|
|
1135
|
+
const provider = str(f.provider);
|
|
1136
|
+
if (!provider) bad(".fallback.provider", "is required");
|
|
1137
|
+
const savedRaw = f.saved;
|
|
1138
|
+
if (typeof savedRaw !== "object" || savedRaw === null || Array.isArray(savedRaw)) bad(".fallback.saved", "must be an object");
|
|
1139
|
+
const s = savedRaw;
|
|
1140
|
+
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");
|
|
1141
|
+
const env = {};
|
|
1142
|
+
for (const [k, v] of Object.entries(s.env)) env[k] = v === null || typeof v === "string" ? v : null;
|
|
1143
|
+
if (s.model !== null && s.model !== void 0 && typeof s.model !== "string") bad(".fallback.saved.model", "must be a string or null");
|
|
1144
|
+
fallback = {
|
|
1145
|
+
provider,
|
|
1146
|
+
saved: {
|
|
1147
|
+
env,
|
|
1148
|
+
model: typeof s.model === "string" ? s.model : null,
|
|
1149
|
+
envExisted: s.envExisted === true
|
|
1150
|
+
},
|
|
1151
|
+
on: str(f.on) || (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
enabled: w.enabled === void 0 ? d.enabled : w.enabled === true,
|
|
1156
|
+
active,
|
|
1157
|
+
providers,
|
|
1158
|
+
classes,
|
|
1159
|
+
mcpSets,
|
|
1160
|
+
pane,
|
|
1161
|
+
logDir: str(w.logDir) || d.logDir,
|
|
1162
|
+
routing,
|
|
1163
|
+
tree,
|
|
1164
|
+
cacheKeepaliveSecs,
|
|
1165
|
+
fallback,
|
|
1166
|
+
nativeModels: { ...NATIVE_ANTHROPIC_MODELS }
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function workersYamlDataOf(workers) {
|
|
1170
|
+
return {
|
|
1171
|
+
active: workers.active,
|
|
1172
|
+
providers: workers.providers,
|
|
1173
|
+
classes: workers.classes,
|
|
1174
|
+
mcpSets: workers.mcpSets,
|
|
1175
|
+
nativeModels: workers.nativeModels
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Read the whole config file and return (raw, workers) — the raw record so
|
|
1180
|
+
* callers can rewrite it preserving every other section, the parsed+validated
|
|
1181
|
+
* workers section. Unreadable config throws (readJsonStrict), missing is fine.
|
|
1182
|
+
* `path` overrides the config location (tests, CLAUDE_SETTINGS_PATH-style
|
|
1183
|
+
* dry runs); default CONFIG_FILE (see paiConfigFilePath).
|
|
1184
|
+
*
|
|
1185
|
+
* Providers, classes, mcpSets and active load from workers.yaml (next to
|
|
1186
|
+
* `path`) when it exists; otherwise they fall back to the JSON `workers`
|
|
1187
|
+
* section, and then to built-in defaults — nothing breaks before migration
|
|
1188
|
+
* (`pai worker config migrate`). Everything else (pane, routing, tree,
|
|
1189
|
+
* cacheKeepaliveSecs, fallback, enabled, logDir) always comes from JSON.
|
|
1190
|
+
*/
|
|
1191
|
+
function readWorkersSection(path = CONFIG_FILE) {
|
|
1192
|
+
const raw = readJsonStrict(path, path);
|
|
1193
|
+
const workers = parseWorkersConfig(raw.workers);
|
|
1194
|
+
const yaml = readWorkersYaml(workersYamlPath());
|
|
1195
|
+
if (yaml) {
|
|
1196
|
+
workers.active = yaml.data.active;
|
|
1197
|
+
workers.providers = yaml.data.providers;
|
|
1198
|
+
workers.classes = yaml.data.classes;
|
|
1199
|
+
workers.mcpSets = yaml.data.mcpSets;
|
|
1200
|
+
workers.nativeModels = yaml.data.nativeModels;
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
raw,
|
|
1204
|
+
workers
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Write the workers section back, atomically. When workers.yaml exists, its
|
|
1209
|
+
* providers/classes/mcpSets/active are synced there (comment-preserving,
|
|
1210
|
+
* only the entries that changed are touched) and stripped from the JSON
|
|
1211
|
+
* `workers` section; everything else still writes to JSON as before.
|
|
1212
|
+
*/
|
|
1213
|
+
function writeWorkersSection(raw, workers, path = CONFIG_FILE) {
|
|
1214
|
+
const yamlPath = workersYamlPath();
|
|
1215
|
+
const usingYaml = existsSync(yamlPath);
|
|
1216
|
+
if (usingYaml) writeWorkersYaml(yamlPath, workersYamlDataOf(workers));
|
|
1217
|
+
raw.workers = usingYaml ? {
|
|
1218
|
+
enabled: workers.enabled,
|
|
1219
|
+
pane: workers.pane,
|
|
1220
|
+
logDir: workers.logDir,
|
|
1221
|
+
routing: workers.routing,
|
|
1222
|
+
tree: workers.tree,
|
|
1223
|
+
cacheKeepaliveSecs: workers.cacheKeepaliveSecs,
|
|
1224
|
+
...workers.fallback ? { fallback: workers.fallback } : {}
|
|
1225
|
+
} : workers.fallback ? workers : {
|
|
1226
|
+
...workers,
|
|
1227
|
+
fallback: void 0
|
|
1228
|
+
};
|
|
1229
|
+
writeJsonAtomic(path, raw, { label: path });
|
|
1230
|
+
}
|
|
1231
|
+
/** Expand a leading ~ (config values are written with `~` to stay portable). */
|
|
1232
|
+
function expandHome(p) {
|
|
1233
|
+
if (p === "~" || p.startsWith("~/")) return join(homedir(), p.slice(1));
|
|
1234
|
+
return p;
|
|
1235
|
+
}
|
|
1236
|
+
function assertProviderRunnable(name, p) {
|
|
1237
|
+
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.`);
|
|
1238
|
+
}
|
|
1239
|
+
/** Where a provider's key file lives, or null. Shared by run + test + add. */
|
|
1240
|
+
function providerKeyPath(p) {
|
|
1241
|
+
return p.keyFile ? expandHome(p.keyFile) : null;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* The provider's auth token: inline `key` wins when set, else `key_file`
|
|
1245
|
+
* read and trimmed, else null (native/local providers use the "local"
|
|
1246
|
+
* placeholder). The one place a provider's key value is resolved — every
|
|
1247
|
+
* spawn path (buildRunEnv, the codex runner, the openai proxy, the
|
|
1248
|
+
* machine-wide fallback) reads through this instead of its own
|
|
1249
|
+
* readFileSync, so inline keys work everywhere a key file already did.
|
|
1250
|
+
*/
|
|
1251
|
+
function resolveProviderKey(p) {
|
|
1252
|
+
if (p.key) return p.key;
|
|
1253
|
+
const keyPath = providerKeyPath(p);
|
|
1254
|
+
if (!keyPath) return null;
|
|
1255
|
+
let content;
|
|
1256
|
+
try {
|
|
1257
|
+
content = readFileSync(keyPath, "utf8");
|
|
1258
|
+
} catch {
|
|
1259
|
+
throw new WorkersConfigError(`key file not readable: ${keyPath}`);
|
|
1260
|
+
}
|
|
1261
|
+
const token = content.trim();
|
|
1262
|
+
if (!token) throw new WorkersConfigError(`key file is empty: ${keyPath}`);
|
|
1263
|
+
return token;
|
|
1264
|
+
}
|
|
1265
|
+
/** `key: ****` + last 4 chars — never the full value. Used everywhere a
|
|
1266
|
+
* provider's key would otherwise be echoed (`providers`, `config check`). */
|
|
1267
|
+
function maskKey(key) {
|
|
1268
|
+
return `****${key.slice(-4)}`;
|
|
1269
|
+
}
|
|
1270
|
+
/** Cost tier of a provider for class filtering (unset = 3, the middle). */
|
|
1271
|
+
function providerCostTier(p) {
|
|
1272
|
+
return p.costTier ?? DEFAULT_COST_TIER;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* The model id for a capability: the provider's preference for it, else its
|
|
1276
|
+
* default model — the one resolution rule every consumer (image class, fast
|
|
1277
|
+
* tier spawns, env pins) goes through.
|
|
1278
|
+
*/
|
|
1279
|
+
function resolveModelCapability(p, capability) {
|
|
1280
|
+
return p.models[capability] ?? p.models.default;
|
|
1281
|
+
}
|
|
1282
|
+
/** Directory under which inline keys (MCP `key` field) are stored, 0600. */
|
|
1283
|
+
function keysDir() {
|
|
1284
|
+
return paiHomePath("keys");
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
//#endregion
|
|
1288
|
+
export { DEFAULT_CONTEXT_WINDOW as A, inlineWorkersYamlKeys as C, relocateWorkersYaml as D, parseWorkersYamlDocument as E, stripModelVariant as M, readJsonStrict as N, workersYamlLegacyNotice as O, writeJsonAtomic as P, initWorkersYaml as S, needsWorkersYamlRelocation as T, providerKeyPath as _, WORKER_CLASSES as a, resolveProviderKey as b, classModelCapability as c, getProviderOrNative as d, isModelCapability as f, providerCostTier as g, nativeAnthropicProvider as h, PROVIDER_TAGS as i, contextWindowFromModelId as j, workersYamlPath as k, defaultWorkersConfig as l, maskKey as m, DEFAULT_LOG_DIR as n, WorkersConfigError as o, keysDir as p, MODEL_CAPABILITIES as r, assertProviderRunnable as s, ANTHROPIC_NATIVE as t, expandHome as u, readWorkersSection as v, migrateWorkersToYaml as w, writeWorkersSection as x, resolveModelCapability as y };
|
|
1289
|
+
//# sourceMappingURL=config-DxMZjpvY.mjs.map
|