@tekmidian/pai 0.36.2 → 0.37.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/{auto-route-Byf8ENXj.mjs → auto-route-DM7GhJ8y.mjs} +2 -2
- package/dist/{auto-route-Byf8ENXj.mjs.map → auto-route-DM7GhJ8y.mjs.map} +1 -1
- package/dist/cli/index.mjs +5 -4
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +5 -4
- package/dist/{clusters-wZgTCYCB.mjs → clusters-Do4tEGyc.mjs} +1 -1
- package/dist/{clusters-wZgTCYCB.mjs.map → clusters-Do4tEGyc.mjs.map} +1 -1
- package/dist/{context-handover-cache-PtNvj_8D.mjs → context-handover-cache-BpUojjsi.mjs} +2 -2
- package/dist/{context-handover-cache-PtNvj_8D.mjs.map → context-handover-cache-BpUojjsi.mjs.map} +1 -1
- package/dist/daemon/index.mjs +8 -8
- package/dist/{daemon-BZ93KRBo.mjs → daemon-CGg1VCbA.mjs} +25 -25
- package/dist/{daemon-BZ93KRBo.mjs.map → daemon-CGg1VCbA.mjs.map} +1 -1
- package/dist/daemon-DsGGiIJM.mjs +20 -0
- package/dist/daemon-mcp/index.mjs +428 -1
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector--Gg5JRN5.mjs → detector-CMap-9vw.mjs} +1 -1
- package/dist/{detector--Gg5JRN5.mjs.map → detector-CMap-9vw.mjs.map} +1 -1
- package/dist/detector-DtLExmHN.mjs +5 -0
- package/dist/factory-BXzqRYVZ.mjs +3 -0
- package/dist/{factory-Bsp7xOpO.mjs → factory-DD2T33C9.mjs} +5 -5
- package/dist/{factory-Bsp7xOpO.mjs.map → factory-DD2T33C9.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +164 -14
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +6 -0
- package/dist/hooks/initialize-session.mjs.map +3 -3
- package/dist/hooks/load-core-context.mjs +6 -0
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +6 -0
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/post-compact-inject.mjs +6 -0
- package/dist/hooks/post-compact-inject.mjs.map +3 -3
- package/dist/hooks/route-agents-to-worker.mjs +456 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +7 -0
- package/dist/hooks/status-line.mjs +400 -0
- package/dist/hooks/status-line.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +6 -0
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/subagent-stop-hook.mjs +6 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs +6 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +3 -3
- package/dist/hooks/update-tab-on-action.mjs +6 -0
- package/dist/hooks/update-tab-on-action.mjs.map +3 -3
- package/dist/hooks/update-tab-titles.mjs +6 -0
- package/dist/hooks/update-tab-titles.mjs.map +3 -3
- package/dist/hooks/worker-proxy.mjs +796 -0
- package/dist/hooks/worker-proxy.mjs.map +7 -0
- package/dist/hooks/worker-status-line.mjs +483 -0
- package/dist/hooks/worker-status-line.mjs.map +7 -0
- package/dist/{indexer-backend-nQZuEx6N.mjs → indexer-backend-Cox9BCo-.mjs} +1 -1
- package/dist/{indexer-backend-nQZuEx6N.mjs.map → indexer-backend-Cox9BCo-.mjs.map} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
- package/dist/{latent-ideas-Bn6A5-5P.mjs → latent-ideas-BC1oINZ-.mjs} +2 -2
- package/dist/{latent-ideas-Bn6A5-5P.mjs.map → latent-ideas-BC1oINZ-.mjs.map} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs → link-boost-QFLrJwD6.mjs} +1 -1
- package/dist/{link-boost-fYjUnxCN.mjs.map → link-boost-QFLrJwD6.mjs.map} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs → main-resolver-DlaLOFBA.mjs} +1 -1
- package/dist/{main-resolver-BAbhKpeX.mjs.map → main-resolver-DlaLOFBA.mjs.map} +1 -1
- package/dist/{main-resolver-Dxh444GO.mjs → main-resolver-IhZo4pI0.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs → neighborhood-D9MJ1c8f.mjs} +1 -1
- package/dist/{neighborhood-DpaEM991.mjs.map → neighborhood-D9MJ1c8f.mjs.map} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs → note-context-d1wT_-GA.mjs} +1 -1
- package/dist/{note-context-DrcY4cWm.mjs.map → note-context-d1wT_-GA.mjs.map} +1 -1
- package/dist/{postgres-BVme6qX0.mjs → postgres--BjPtLa0.mjs} +1 -1
- package/dist/{postgres-BVme6qX0.mjs.map → postgres--BjPtLa0.mjs.map} +1 -1
- package/dist/{program-C-fUghPv.mjs → program-CEIHn_Ma.mjs} +693 -101
- package/dist/program-CEIHn_Ma.mjs.map +1 -0
- package/dist/providers-sXcK5bDZ.mjs +3067 -0
- package/dist/providers-sXcK5bDZ.mjs.map +1 -0
- package/dist/{query-feedback-C1T6kS18.mjs → query-feedback-BUJxgw5B.mjs} +1 -1
- package/dist/{query-feedback-C1T6kS18.mjs.map → query-feedback-BUJxgw5B.mjs.map} +1 -1
- package/dist/query-feedback-DhyLOe5S.mjs +3 -0
- package/dist/router-1zi8jiNF.mjs +3 -0
- package/dist/{router-CsDm7HvK.mjs → router-DK_sLsUL.mjs} +1 -1
- package/dist/{router-CsDm7HvK.mjs.map → router-DK_sLsUL.mjs.map} +1 -1
- package/dist/skills/Worker/SKILL.md +41 -0
- package/dist/{sources-D8ZdNfvK.mjs → sources-Bi7--33T.mjs} +1 -1
- package/dist/{sources-D8ZdNfvK.mjs.map → sources-Bi7--33T.mjs.map} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs → sqlite-DtaL1glm.mjs} +1 -1
- package/dist/{sqlite-D1IaR8Am.mjs.map → sqlite-DtaL1glm.mjs.map} +1 -1
- package/dist/{state-qtmrBWCm.mjs → state-8Hm9E4tW.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs → state-BY2L6-vX.mjs} +1 -1
- package/dist/{state-WaXhLr6R.mjs.map → state-BY2L6-vX.mjs.map} +1 -1
- package/dist/{themes-XPkj_bfP.mjs → themes-BN0a2duq.mjs} +1 -1
- package/dist/{themes-XPkj_bfP.mjs.map → themes-BN0a2duq.mjs.map} +1 -1
- package/dist/{tools-DEt6YPfc.mjs → tools-CGPqpU3A.mjs} +1 -1
- package/dist/{tools-ceiy7ANX.mjs → tools-y2bJpKom.mjs} +14 -14
- package/dist/{tools-ceiy7ANX.mjs.map → tools-y2bJpKom.mjs.map} +1 -1
- package/dist/{trace-DfyGmMG_.mjs → trace-h23JCcFD.mjs} +1 -1
- package/dist/{trace-DfyGmMG_.mjs.map → trace-h23JCcFD.mjs.map} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs → vault-indexer-DgsPjMgs.mjs} +1 -1
- package/dist/{vault-indexer-CFvlPUMB.mjs.map → vault-indexer-DgsPjMgs.mjs.map} +1 -1
- package/dist/{work-queue-worker-B8W8_3Rn.mjs → work-queue-worker-Dva_v_pI.mjs} +4 -4
- package/dist/{work-queue-worker-B8W8_3Rn.mjs.map → work-queue-worker-Dva_v_pI.mjs.map} +1 -1
- package/dist/{work-queue-worker-HN2Ufg-L.mjs → work-queue-worker-gsKd2LJa.mjs} +4 -4
- package/dist/{zettelkasten-CvjmMghT.mjs → zettelkasten-vo7psPdT.mjs} +3 -3
- package/dist/{zettelkasten-CvjmMghT.mjs.map → zettelkasten-vo7psPdT.mjs.map} +1 -1
- package/docs/commands/README.md +28 -0
- package/docs/commands/backup.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- 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/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- 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 +1 -1
- package/docs/commands/projects.md +1 -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 +361 -0
- package/docs/commands/zettel.md +1 -1
- package/docs/worker.md +303 -0
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +46 -1
- package/src/hooks/pre-compact.sh +1 -0
- package/src/hooks/session-autosave.sh +1 -0
- package/src/hooks/session-stop.sh +1 -0
- package/src/hooks/ts/lib/context-fill.test.ts +104 -0
- package/src/hooks/ts/lib/context-fill.ts +73 -0
- package/src/hooks/ts/lib/handover-evidence.ts +227 -0
- package/src/hooks/ts/lib/worker-session.test.ts +24 -0
- package/src/hooks/ts/lib/worker-session.ts +23 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +2 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +2 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +17 -0
- package/src/hooks/ts/pre-tool-use/route-agents-to-worker.ts +128 -0
- package/src/hooks/ts/session-start/initialize-session.ts +2 -0
- package/src/hooks/ts/session-start/load-core-context.ts +2 -0
- package/src/hooks/ts/session-start/load-project-context.ts +2 -0
- package/src/hooks/ts/session-start/post-compact-inject.ts +2 -0
- package/src/hooks/ts/stop/stop-hook.ts +2 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +2 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -0
- package/statusline-command.sh +17 -0
- package/dist/daemon-DRdoA489.mjs +0 -20
- package/dist/detector-DGAk1iBR.mjs +0 -5
- package/dist/factory-CrokPMk2.mjs +0 -3
- package/dist/program-C-fUghPv.mjs.map +0 -1
- package/dist/query-feedback-BBMBp96K.mjs +0 -3
- package/dist/router-BMkOb62X.mjs +0 -3
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/workers/config.ts
|
|
4
|
+
import { homedir as homedir2 } from "node:os";
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/config/json-store.ts
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
copyFileSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
mkdirSync
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
function readJsonStrict(path, label = path) {
|
|
18
|
+
if (!existsSync(path)) return {};
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = readFileSync(path, "utf8");
|
|
22
|
+
} catch (e) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Could not read ${label}: ${e instanceof Error ? e.message : String(e)}
|
|
25
|
+
Refusing to continue \u2014 writing now would replace its contents with ours alone.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}
|
|
33
|
+
Refusing to continue \u2014 overwriting it would destroy whatever it holds.
|
|
34
|
+
Repair the file, or move it aside and re-run this command.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/daemon/config.ts
|
|
40
|
+
import { homedir, userInfo } from "node:os";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
|
|
43
|
+
// src/notifications/types.ts
|
|
44
|
+
var DEFAULT_ROUTING = {
|
|
45
|
+
error: ["whatsapp", "macos", "ntfy", "cli"],
|
|
46
|
+
completion: ["whatsapp", "macos", "ntfy", "cli"],
|
|
47
|
+
info: ["cli"],
|
|
48
|
+
progress: ["cli"],
|
|
49
|
+
debug: []
|
|
50
|
+
};
|
|
51
|
+
var DEFAULT_CHANNELS = {
|
|
52
|
+
ntfy: {
|
|
53
|
+
enabled: false,
|
|
54
|
+
url: void 0,
|
|
55
|
+
priority: "default"
|
|
56
|
+
},
|
|
57
|
+
whatsapp: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
recipient: void 0
|
|
60
|
+
},
|
|
61
|
+
macos: {
|
|
62
|
+
enabled: true
|
|
63
|
+
},
|
|
64
|
+
voice: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
voiceName: "bm_george"
|
|
67
|
+
},
|
|
68
|
+
cli: {
|
|
69
|
+
enabled: true
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var DEFAULT_NOTIFICATION_CONFIG = {
|
|
73
|
+
mode: "auto",
|
|
74
|
+
channels: DEFAULT_CHANNELS,
|
|
75
|
+
routing: DEFAULT_ROUTING
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/tasks/types.ts
|
|
79
|
+
var DEFAULT_TASK_CONFIG = {
|
|
80
|
+
enabled: false,
|
|
81
|
+
providers: {
|
|
82
|
+
todoist: {
|
|
83
|
+
enabled: false
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
autoDispatch: false
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/runtime-paths.ts
|
|
90
|
+
function paiSocketPath() {
|
|
91
|
+
return process.env.PAI_SOCKET_PATH ?? "/tmp/pai.sock";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/daemon/config.ts
|
|
95
|
+
function perUserDbName() {
|
|
96
|
+
const username = userInfo().username;
|
|
97
|
+
const safe = username.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
98
|
+
return `pai_${safe}`;
|
|
99
|
+
}
|
|
100
|
+
function perUserConnectionString() {
|
|
101
|
+
const db = perUserDbName();
|
|
102
|
+
return `postgresql://pai:pai@localhost:5432/${db}`;
|
|
103
|
+
}
|
|
104
|
+
var DEFAULTS = {
|
|
105
|
+
socketPath: paiSocketPath(),
|
|
106
|
+
indexIntervalSecs: 300,
|
|
107
|
+
embedIntervalSecs: 600,
|
|
108
|
+
embedOnStartup: false,
|
|
109
|
+
storageBackend: "sqlite",
|
|
110
|
+
postgres: {
|
|
111
|
+
connectionString: perUserConnectionString(),
|
|
112
|
+
maxConnections: 5,
|
|
113
|
+
connectionTimeoutMs: 5e3
|
|
114
|
+
},
|
|
115
|
+
embeddingModel: "Snowflake/snowflake-arctic-embed-m-v1.5",
|
|
116
|
+
logLevel: "info",
|
|
117
|
+
notifications: DEFAULT_NOTIFICATION_CONFIG,
|
|
118
|
+
tasks: DEFAULT_TASK_CONFIG,
|
|
119
|
+
// Deliberately empty. An install must not guess who the user is: a wrong
|
|
120
|
+
// guess here is an address that can be mailed without review.
|
|
121
|
+
identity: { selfEmails: [] },
|
|
122
|
+
search: {
|
|
123
|
+
mode: "keyword",
|
|
124
|
+
rerank: true,
|
|
125
|
+
recencyBoostDays: 90,
|
|
126
|
+
defaultLimit: 10,
|
|
127
|
+
snippetLength: 200
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var CONFIG_DIR = join(homedir(), ".config", "pai");
|
|
131
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
132
|
+
|
|
133
|
+
// src/workers/config.ts
|
|
134
|
+
var DEFAULT_LOG_DIR = "~/.claude/logs/workers";
|
|
135
|
+
var DEFAULT_PANE = {
|
|
136
|
+
enabled: true,
|
|
137
|
+
fontSize: 13,
|
|
138
|
+
autoExitSecs: 60
|
|
139
|
+
};
|
|
140
|
+
var DEFAULT_ROUTING2 = {
|
|
141
|
+
order: [],
|
|
142
|
+
cooldownMinutes: 30,
|
|
143
|
+
retryOnQuota: true
|
|
144
|
+
};
|
|
145
|
+
function defaultWorkersConfig() {
|
|
146
|
+
return {
|
|
147
|
+
enabled: false,
|
|
148
|
+
active: null,
|
|
149
|
+
providers: {},
|
|
150
|
+
roles: {},
|
|
151
|
+
mcpSets: {},
|
|
152
|
+
pane: { ...DEFAULT_PANE },
|
|
153
|
+
logDir: DEFAULT_LOG_DIR,
|
|
154
|
+
routing: { ...DEFAULT_ROUTING2, order: [] }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
var WorkersConfigError = class extends Error {
|
|
158
|
+
};
|
|
159
|
+
function bad(path, why) {
|
|
160
|
+
throw new WorkersConfigError(`workers${path}: ${why}`);
|
|
161
|
+
}
|
|
162
|
+
function str(v) {
|
|
163
|
+
return typeof v === "string" ? v : "";
|
|
164
|
+
}
|
|
165
|
+
function parseProvider(name, raw) {
|
|
166
|
+
if (typeof raw !== "object" || raw === null) bad(`.providers.${name}`, "must be an object");
|
|
167
|
+
const p = raw;
|
|
168
|
+
const protocol = p.protocol === void 0 ? "anthropic" : str(p.protocol);
|
|
169
|
+
if (protocol !== "anthropic" && protocol !== "openai") {
|
|
170
|
+
bad(`.providers.${name}.protocol`, `"${str(p.protocol)}" is neither "anthropic" nor "openai"`);
|
|
171
|
+
}
|
|
172
|
+
const engine = p.engine === void 0 ? "claude" : str(p.engine);
|
|
173
|
+
if (engine !== "claude" && engine !== "codex") {
|
|
174
|
+
bad(`.providers.${name}.engine`, `"${str(p.engine)}" is neither "claude" nor "codex"`);
|
|
175
|
+
}
|
|
176
|
+
const baseUrl = str(p.baseUrl);
|
|
177
|
+
if (!baseUrl && protocol !== "openai") bad(`.providers.${name}.baseUrl`, "is required");
|
|
178
|
+
const keyFile = p.keyFile === void 0 || p.keyFile === null || str(p.keyFile) === "" ? null : str(p.keyFile);
|
|
179
|
+
const modelsRaw = p.models === void 0 ? {} : p.models;
|
|
180
|
+
if (typeof modelsRaw !== "object" || modelsRaw === null) {
|
|
181
|
+
bad(`.providers.${name}.models`, "must be an object");
|
|
182
|
+
}
|
|
183
|
+
const m = modelsRaw;
|
|
184
|
+
const defaultModel = str(m.default);
|
|
185
|
+
if (!defaultModel) bad(`.providers.${name}.models.default`, "is required");
|
|
186
|
+
const fast = m.fast === void 0 ? void 0 : str(m.fast);
|
|
187
|
+
const env = {};
|
|
188
|
+
if (p.env !== void 0) {
|
|
189
|
+
if (typeof p.env !== "object" || p.env === null || Array.isArray(p.env)) {
|
|
190
|
+
bad(`.providers.${name}.env`, "must be an object of string values");
|
|
191
|
+
}
|
|
192
|
+
for (const [k, v] of Object.entries(p.env)) {
|
|
193
|
+
if (typeof v !== "string") bad(`.providers.${name}.env.${k}`, "must be a string");
|
|
194
|
+
env[k] = v;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const quotaSkipAt = p.quotaSkipAt === void 0 ? void 0 : p.quotaSkipAt;
|
|
198
|
+
if (quotaSkipAt !== void 0) {
|
|
199
|
+
if (typeof quotaSkipAt !== "number" || quotaSkipAt < 0 || quotaSkipAt > 100) {
|
|
200
|
+
bad(`.providers.${name}.quotaSkipAt`, "must be a number between 0 and 100");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const contextWindow = p.contextWindow === void 0 ? void 0 : p.contextWindow;
|
|
204
|
+
if (contextWindow !== void 0) {
|
|
205
|
+
if (typeof contextWindow !== "number" || contextWindow <= 0) {
|
|
206
|
+
bad(`.providers.${name}.contextWindow`, "must be a positive number of tokens");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const upstreamUrl = str(p.upstreamUrl);
|
|
210
|
+
if (protocol === "openai" && !upstreamUrl) {
|
|
211
|
+
bad(`.providers.${name}.upstreamUrl`, `is required for protocol "openai" (the Chat Completions base, e.g. "https://api.openai.com/v1")`);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
enabled: p.enabled === void 0 ? true : p.enabled === true,
|
|
215
|
+
protocol,
|
|
216
|
+
baseUrl,
|
|
217
|
+
keyFile,
|
|
218
|
+
models: fast ? { default: defaultModel, fast } : { default: defaultModel },
|
|
219
|
+
env,
|
|
220
|
+
...str(p.note) ? { note: str(p.note) } : {},
|
|
221
|
+
...upstreamUrl ? { upstreamUrl } : {},
|
|
222
|
+
...engine !== "claude" ? { engine } : {},
|
|
223
|
+
...str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {},
|
|
224
|
+
...quotaSkipAt !== void 0 ? { quotaSkipAt } : {},
|
|
225
|
+
...contextWindow !== void 0 ? { contextWindow } : {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function parseWorkersConfig(raw) {
|
|
229
|
+
const d = defaultWorkersConfig();
|
|
230
|
+
if (raw === void 0 || raw === null) return d;
|
|
231
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
232
|
+
bad("", "section must be an object");
|
|
233
|
+
}
|
|
234
|
+
const w = raw;
|
|
235
|
+
const providers = {};
|
|
236
|
+
if (w.providers !== void 0) {
|
|
237
|
+
if (typeof w.providers !== "object" || w.providers === null || Array.isArray(w.providers)) {
|
|
238
|
+
bad(".providers", "must be an object keyed by provider name");
|
|
239
|
+
}
|
|
240
|
+
for (const [name, p] of Object.entries(w.providers)) {
|
|
241
|
+
providers[name] = parseProvider(name, p);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const roles = {};
|
|
245
|
+
if (w.roles !== void 0) {
|
|
246
|
+
if (typeof w.roles !== "object" || w.roles === null || Array.isArray(w.roles)) {
|
|
247
|
+
bad(".roles", "must be an object of role \u2192 provider[/alias] or {provider, mcp}");
|
|
248
|
+
}
|
|
249
|
+
for (const [role, target] of Object.entries(w.roles)) {
|
|
250
|
+
if (typeof target === "object" && target !== null && !Array.isArray(target)) {
|
|
251
|
+
const o = target;
|
|
252
|
+
const provider = str(o.provider);
|
|
253
|
+
if (!provider || provider.includes(" ")) {
|
|
254
|
+
bad(`.roles.${role}.provider`, `invalid provider "${provider}"`);
|
|
255
|
+
}
|
|
256
|
+
let mcp;
|
|
257
|
+
if (o.mcp !== void 0) {
|
|
258
|
+
if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) {
|
|
259
|
+
bad(`.roles.${role}.mcp`, "must be an array of MCP server or set names");
|
|
260
|
+
}
|
|
261
|
+
mcp = o.mcp;
|
|
262
|
+
}
|
|
263
|
+
roles[role] = mcp ? { provider, mcp } : { provider };
|
|
264
|
+
} else {
|
|
265
|
+
const t = str(target);
|
|
266
|
+
if (!t || t.includes(" ")) bad(`.roles.${role}`, `invalid target "${t}"`);
|
|
267
|
+
roles[role] = t;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const mcpSets = {};
|
|
272
|
+
if (w.mcpSets !== void 0) {
|
|
273
|
+
if (typeof w.mcpSets !== "object" || w.mcpSets === null || Array.isArray(w.mcpSets)) {
|
|
274
|
+
bad(".mcpSets", "must be an object of set name \u2192 [server names]");
|
|
275
|
+
}
|
|
276
|
+
for (const [setName, servers] of Object.entries(w.mcpSets)) {
|
|
277
|
+
if (!Array.isArray(servers) || servers.some((x) => typeof x !== "string")) {
|
|
278
|
+
bad(`.mcpSets.${setName}`, "must be an array of MCP server names");
|
|
279
|
+
}
|
|
280
|
+
mcpSets[setName] = servers;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
let pane = { ...DEFAULT_PANE };
|
|
284
|
+
if (w.pane !== void 0) {
|
|
285
|
+
if (typeof w.pane !== "object" || w.pane === null) bad(".pane", "must be an object");
|
|
286
|
+
const pc = w.pane;
|
|
287
|
+
if (pc.enabled !== void 0 && typeof pc.enabled !== "boolean") bad(".pane.enabled", "must be boolean");
|
|
288
|
+
if (pc.fontSize !== void 0 && (typeof pc.fontSize !== "number" || pc.fontSize <= 0)) {
|
|
289
|
+
bad(".pane.fontSize", "must be a positive number of points");
|
|
290
|
+
}
|
|
291
|
+
if (pc.autoExitSecs !== void 0 && typeof pc.autoExitSecs !== "number") {
|
|
292
|
+
bad(".pane.autoExitSecs", "must be a number");
|
|
293
|
+
}
|
|
294
|
+
pane = {
|
|
295
|
+
enabled: pc.enabled === void 0 ? DEFAULT_PANE.enabled : pc.enabled === true,
|
|
296
|
+
fontSize: pc.fontSize === void 0 ? DEFAULT_PANE.fontSize : pc.fontSize,
|
|
297
|
+
autoExitSecs: pc.autoExitSecs === void 0 ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
let routing = { ...DEFAULT_ROUTING2, order: [] };
|
|
301
|
+
if (w.routing !== void 0) {
|
|
302
|
+
if (typeof w.routing !== "object" || w.routing === null) bad(".routing", "must be an object");
|
|
303
|
+
const r = w.routing;
|
|
304
|
+
if (r.order !== void 0) {
|
|
305
|
+
if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== "string")) {
|
|
306
|
+
bad(".routing.order", "must be an array of provider names");
|
|
307
|
+
}
|
|
308
|
+
routing.order = r.order;
|
|
309
|
+
}
|
|
310
|
+
if (r.cooldownMinutes !== void 0 && typeof r.cooldownMinutes !== "number") {
|
|
311
|
+
bad(".routing.cooldownMinutes", "must be a number");
|
|
312
|
+
}
|
|
313
|
+
if (r.retryOnQuota !== void 0 && typeof r.retryOnQuota !== "boolean") {
|
|
314
|
+
bad(".routing.retryOnQuota", "must be boolean");
|
|
315
|
+
}
|
|
316
|
+
routing = {
|
|
317
|
+
order: routing.order,
|
|
318
|
+
cooldownMinutes: r.cooldownMinutes === void 0 ? DEFAULT_ROUTING2.cooldownMinutes : r.cooldownMinutes,
|
|
319
|
+
retryOnQuota: r.retryOnQuota === void 0 ? DEFAULT_ROUTING2.retryOnQuota : r.retryOnQuota === true
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const active = w.active === void 0 || w.active === null ? null : str(w.active);
|
|
323
|
+
if (active !== null && active !== "auto" && !(active in providers)) {
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
enabled: w.enabled === void 0 ? d.enabled : w.enabled === true,
|
|
327
|
+
active,
|
|
328
|
+
providers,
|
|
329
|
+
roles,
|
|
330
|
+
mcpSets,
|
|
331
|
+
pane,
|
|
332
|
+
logDir: str(w.logDir) || d.logDir,
|
|
333
|
+
routing
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function readWorkersSection() {
|
|
337
|
+
const raw = readJsonStrict(CONFIG_FILE, "~/.config/pai/config.json");
|
|
338
|
+
return { raw, workers: parseWorkersConfig(raw.workers) };
|
|
339
|
+
}
|
|
340
|
+
function expandHome(p) {
|
|
341
|
+
if (p === "~" || p.startsWith("~/")) return join2(homedir2(), p.slice(1));
|
|
342
|
+
return p;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/workers/paths.ts
|
|
346
|
+
import { join as join3 } from "node:path";
|
|
347
|
+
function workersLogDir(config) {
|
|
348
|
+
return expandHome(config.logDir);
|
|
349
|
+
}
|
|
350
|
+
function ledgerPath(logDir) {
|
|
351
|
+
return join3(logDir, "ledger.log");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/workers/ledger.ts
|
|
355
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
356
|
+
import { dirname } from "node:path";
|
|
357
|
+
function ledgerStamp(d = /* @__PURE__ */ new Date()) {
|
|
358
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
359
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
360
|
+
}
|
|
361
|
+
function appendLedger(path, event, kv, now = /* @__PURE__ */ new Date()) {
|
|
362
|
+
const parts = [];
|
|
363
|
+
for (const [k, v] of Object.entries(kv)) {
|
|
364
|
+
if (v === void 0 || v === null) continue;
|
|
365
|
+
parts.push(`${k}=${String(v).replace(/\s+/g, " ")}`);
|
|
366
|
+
}
|
|
367
|
+
const dir = dirname(path);
|
|
368
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
369
|
+
const tail = parts.length ? " " + parts.join(" ") : "";
|
|
370
|
+
appendFileSync(path, `${ledgerStamp(now)} ${event}${tail}
|
|
371
|
+
`, "utf8");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/hooks/ts/pre-tool-use/route-agents-to-worker.ts
|
|
375
|
+
var ALLOW = JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } });
|
|
376
|
+
function deny(reason) {
|
|
377
|
+
return JSON.stringify({
|
|
378
|
+
hookSpecificOutput: {
|
|
379
|
+
hookEventName: "PreToolUse",
|
|
380
|
+
permissionDecision: "deny",
|
|
381
|
+
permissionDecisionReason: reason
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function shortLabel(input) {
|
|
386
|
+
const desc = typeof input.tool_input === "object" && input.tool_input !== null ? input.tool_input.description ?? "" : "";
|
|
387
|
+
return desc.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
388
|
+
}
|
|
389
|
+
async function main() {
|
|
390
|
+
let text = "";
|
|
391
|
+
try {
|
|
392
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
393
|
+
} catch {
|
|
394
|
+
process.stdout.write(ALLOW);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!text.trim()) {
|
|
398
|
+
process.stdout.write(ALLOW);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let input;
|
|
402
|
+
try {
|
|
403
|
+
input = JSON.parse(text);
|
|
404
|
+
} catch {
|
|
405
|
+
process.stdout.write(ALLOW);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (input.tool_name && input.tool_name !== "Agent") {
|
|
409
|
+
process.stdout.write(ALLOW);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const label = shortLabel(input);
|
|
413
|
+
const cwd = input.cwd ?? process.cwd();
|
|
414
|
+
const ledger = (event) => {
|
|
415
|
+
try {
|
|
416
|
+
appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), event, {
|
|
417
|
+
cwd,
|
|
418
|
+
...label ? { label } : {}
|
|
419
|
+
});
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
if (process.env.ALLOW_ANTHROPIC_AGENTS === "1") {
|
|
424
|
+
ledger("ALLOWED-ANTHROPIC-AGENT");
|
|
425
|
+
process.stdout.write(ALLOW);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
let workers;
|
|
429
|
+
try {
|
|
430
|
+
workers = readWorkersSection().workers;
|
|
431
|
+
} catch {
|
|
432
|
+
process.stdout.write(ALLOW);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!workers.enabled || Object.keys(workers.providers).length === 0) {
|
|
436
|
+
process.stdout.write(ALLOW);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
ledger("DENIED-ANTHROPIC-AGENT");
|
|
440
|
+
process.stdout.write(
|
|
441
|
+
deny(
|
|
442
|
+
`Agent tool is disabled: subagents run on the configured worker provider, not Anthropic. Delegate with Bash instead, in the background:
|
|
443
|
+
|
|
444
|
+
pai worker run --label "${label || "task"}" --role research -p '<full, self-contained task spec>' --allowedTools 'Read,Edit,Write,Bash,Grep,Glob' --output-format json
|
|
445
|
+
|
|
446
|
+
- Run it with run_in_background: true and always with a timeout.
|
|
447
|
+
- Use --role spotcheck (or implement) as the task demands.
|
|
448
|
+
- Web research: add WebSearch,WebFetch to --allowedTools.
|
|
449
|
+
- The answer is in the \`result\` field of the JSON it prints. Review the diff yourself.
|
|
450
|
+
- pai worker ps lists running workers; pai worker follow <id> shows one live.
|
|
451
|
+
Keep only orchestration, review and synthesis in this session. To run subagents on Anthropic for one session: start it with ALLOW_ANTHROPIC_AGENTS=1.`
|
|
452
|
+
)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
main().catch(() => process.stdout.write(ALLOW));
|
|
456
|
+
//# sourceMappingURL=route-agents-to-worker.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/workers/config.ts", "../../../src/config/json-store.ts", "../../../src/daemon/config.ts", "../../../src/notifications/types.ts", "../../../src/tasks/types.ts", "../../../src/runtime-paths.ts", "../../../src/workers/paths.ts", "../../../src/workers/ledger.ts", "../../../src/hooks/ts/pre-tool-use/route-agents-to-worker.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * config.ts \u2014 the `workers` section of ~/.config/pai/config.json\n *\n * Everything that knows about worker providers reads this module: the CLI\n * (`pai worker \u2026`), the MCP tools (worker_*), and the Agent-routing hook.\n * Keep it free of commander/MCP imports so all three layers stay thin over it.\n *\n * Keys are never stored here \u2014 only paths to 0600 files. A provider without a\n * keyFile is a local server and gets the placeholder token \"local\".\n *\n * `protocol: \"openai\"` providers run through the PAI proxy (src/workers/proxy):\n * `upstreamUrl` is their Chat Completions base, and `run` points\n * ANTHROPIC_BASE_URL at the local proxy with the provider name in the path.\n * `engine: \"codex\"` providers run through the Codex CLI instead of Claude\n * Code.\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { readJsonStrict, writeJsonAtomic } from \"../config/json-store.js\";\nimport { CONFIG_FILE } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Wire protocol a provider speaks: \"anthropic\" natively, \"openai\" through the\n * PAI proxy (which translates the Anthropic Messages API to Chat Completions).\n */\nexport type WorkerProtocol = \"anthropic\" | \"openai\";\n\n/** Runner executable behind a provider: Claude Code or the Codex CLI. */\nexport type WorkerEngine = \"claude\" | \"codex\";\n\nexport interface WorkerProvider {\n enabled: boolean;\n /** Wire protocol the baseUrl speaks. */\n protocol: WorkerProtocol;\n /** Anthropic-compatible Messages API base. */\n baseUrl: string;\n /** 0600 file holding the auth token; null for local servers (\"local\"). */\n keyFile: string | null;\n models: {\n default: string;\n /** Alias used for cheap/fast work (spotcheck, research). */\n fast?: string;\n };\n /** Extra environment variables for runs through this provider (string values). */\n env: Record<string, string>;\n note?: string;\n /**\n * OpenAI Chat Completions base (e.g. \"https://api.openai.com/v1\"). Required\n * for protocol \"openai\"; read by the PAI proxy, never by the runner.\n */\n upstreamUrl?: string;\n /** Runner for this provider; \"codex\" goes through the Codex CLI. */\n engine?: WorkerEngine;\n /** Optional shell command printing 0\u2013100 (percent of quota used). */\n quotaProbe?: string;\n /** Auto-routing skips the provider at or above this percentage. Default 95. */\n quotaSkipAt?: number;\n /** Context window of the provider's model, for the context meter. Default 200000. */\n contextWindow?: number;\n}\n\nexport interface WorkersPaneConfig {\n enabled: boolean;\n /** Font size (points) of the follow-pane profile's font. */\n fontSize: number;\n /** Seconds a pane lingers after its worker goes quiet. */\n autoExitSecs: number;\n}\n\nexport interface WorkersRoutingConfig {\n /** Provider names in preference order; first usable one wins. */\n order: string[];\n /** Minutes a provider sits in cooldown after a quota/rate failure. */\n cooldownMinutes: number;\n /** Reroute a failed-before-first-tool run to the next provider. */\n retryOnQuota: boolean;\n}\n\n/**\n * A role target: \"<provider>\", \"<provider>/<modelAlias>\", or an object with a\n * provider and the MCP servers (or mcpSets names) its workers start with.\n */\nexport type RoleTarget = string | { provider: string; mcp?: string[] };\n\nexport interface WorkersConfig {\n enabled: boolean;\n /** Provider name, or \"auto\" for routing.order resolution. */\n active: string | null;\n providers: Record<string, WorkerProvider>;\n /** Role \u2192 target; see RoleTarget. */\n roles: Record<string, RoleTarget>;\n /** MCP set name \u2192 server names; `--mcp <set>` and role `mcp` expand these. */\n mcpSets: Record<string, string[]>;\n pane: WorkersPaneConfig;\n logDir: string;\n routing: WorkersRoutingConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_LOG_DIR = \"~/.claude/logs/workers\";\n\nexport const DEFAULT_PANE: WorkersPaneConfig = {\n enabled: true,\n fontSize: 13,\n autoExitSecs: 60,\n};\n\nexport const DEFAULT_ROUTING: WorkersRoutingConfig = {\n order: [],\n cooldownMinutes: 30,\n retryOnQuota: true,\n};\n\nexport function defaultWorkersConfig(): WorkersConfig {\n return {\n enabled: false,\n active: null,\n providers: {},\n roles: {},\n mcpSets: {},\n pane: { ...DEFAULT_PANE },\n logDir: DEFAULT_LOG_DIR,\n routing: { ...DEFAULT_ROUTING, order: [] },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport class WorkersConfigError extends Error {}\n\nfunction bad(path: string, why: string): never {\n throw new WorkersConfigError(`workers${path}: ${why}`);\n}\n\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : \"\";\n}\n\nfunction parseProvider(name: string, raw: unknown): WorkerProvider {\n if (typeof raw !== \"object\" || raw === null) bad(`.providers.${name}`, \"must be an object\");\n const p = raw as Record<string, unknown>;\n\n const protocol = p.protocol === undefined ? \"anthropic\" : str(p.protocol);\n if (protocol !== \"anthropic\" && protocol !== \"openai\") {\n bad(`.providers.${name}.protocol`, `\"${str(p.protocol)}\" is neither \"anthropic\" nor \"openai\"`);\n }\n const engine = p.engine === undefined ? \"claude\" : str(p.engine);\n if (engine !== \"claude\" && engine !== \"codex\") {\n bad(`.providers.${name}.engine`, `\"${str(p.engine)}\" is neither \"claude\" nor \"codex\"`);\n }\n\n const baseUrl = str(p.baseUrl);\n // openai providers reach the model through the PAI proxy; their baseUrl is\n // the proxy URL, filled in by the runner \u2014 only anthropic needs one here.\n if (!baseUrl && protocol !== \"openai\") bad(`.providers.${name}.baseUrl`, \"is required\");\n\n const keyFile =\n p.keyFile === undefined || p.keyFile === null || str(p.keyFile) === \"\"\n ? null\n : str(p.keyFile);\n\n const modelsRaw = p.models === undefined ? {} : p.models;\n if (typeof modelsRaw !== \"object\" || modelsRaw === null) {\n bad(`.providers.${name}.models`, \"must be an object\");\n }\n const m = modelsRaw as Record<string, unknown>;\n const defaultModel = str(m.default);\n if (!defaultModel) bad(`.providers.${name}.models.default`, \"is required\");\n const fast = m.fast === undefined ? undefined : str(m.fast);\n\n const env: Record<string, string> = {};\n if (p.env !== undefined) {\n if (typeof p.env !== \"object\" || p.env === null || Array.isArray(p.env)) {\n bad(`.providers.${name}.env`, \"must be an object of string values\");\n }\n for (const [k, v] of Object.entries(p.env as Record<string, unknown>)) {\n if (typeof v !== \"string\") bad(`.providers.${name}.env.${k}`, \"must be a string\");\n env[k] = v;\n }\n }\n\n const quotaSkipAt = p.quotaSkipAt === undefined ? undefined : p.quotaSkipAt;\n if (quotaSkipAt !== undefined) {\n if (typeof quotaSkipAt !== \"number\" || quotaSkipAt < 0 || quotaSkipAt > 100) {\n bad(`.providers.${name}.quotaSkipAt`, \"must be a number between 0 and 100\");\n }\n }\n\n const contextWindow = p.contextWindow === undefined ? undefined : p.contextWindow;\n if (contextWindow !== undefined) {\n if (typeof contextWindow !== \"number\" || contextWindow <= 0) {\n bad(`.providers.${name}.contextWindow`, \"must be a positive number of tokens\");\n }\n }\n\n const upstreamUrl = str(p.upstreamUrl);\n if (protocol === \"openai\" && !upstreamUrl) {\n bad(`.providers.${name}.upstreamUrl`, `is required for protocol \"openai\" (the Chat Completions base, e.g. \"https://api.openai.com/v1\")`);\n }\n\n return {\n enabled: p.enabled === undefined ? true : p.enabled === true,\n protocol,\n baseUrl,\n keyFile,\n models: fast ? { default: defaultModel, fast } : { default: defaultModel },\n env,\n ...(str(p.note) ? { note: str(p.note) } : {}),\n ...(upstreamUrl ? { upstreamUrl } : {}),\n ...(engine !== \"claude\" ? { engine } : {}),\n ...(str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {}),\n ...(quotaSkipAt !== undefined ? { quotaSkipAt } : {}),\n ...(contextWindow !== undefined ? { contextWindow } : {}),\n };\n}\n\n/**\n * Parse and validate a raw `workers` value. Missing section \u2192 defaults.\n * Unknown-but-typed garbage \u2192 WorkersConfigError naming the offending field.\n */\nexport function parseWorkersConfig(raw: unknown): WorkersConfig {\n const d = defaultWorkersConfig();\n if (raw === undefined || raw === null) return d;\n if (typeof raw !== \"object\" || Array.isArray(raw)) {\n bad(\"\", \"section must be an object\");\n }\n const w = raw as Record<string, unknown>;\n\n const providers: Record<string, WorkerProvider> = {};\n if (w.providers !== undefined) {\n if (typeof w.providers !== \"object\" || w.providers === null || Array.isArray(w.providers)) {\n bad(\".providers\", \"must be an object keyed by provider name\");\n }\n for (const [name, p] of Object.entries(w.providers)) {\n providers[name] = parseProvider(name, p);\n }\n }\n\n const roles: Record<string, RoleTarget> = {};\n if (w.roles !== undefined) {\n if (typeof w.roles !== \"object\" || w.roles === null || Array.isArray(w.roles)) {\n bad(\".roles\", \"must be an object of role \u2192 provider[/alias] or {provider, mcp}\");\n }\n for (const [role, target] of Object.entries(w.roles)) {\n if (typeof target === \"object\" && target !== null && !Array.isArray(target)) {\n const o = target as Record<string, unknown>;\n const provider = str(o.provider);\n if (!provider || provider.includes(\" \")) {\n bad(`.roles.${role}.provider`, `invalid provider \"${provider}\"`);\n }\n let mcp: string[] | undefined;\n if (o.mcp !== undefined) {\n if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== \"string\")) {\n bad(`.roles.${role}.mcp`, \"must be an array of MCP server or set names\");\n }\n mcp = o.mcp as string[];\n }\n roles[role] = mcp ? { provider, mcp } : { provider };\n } else {\n const t = str(target);\n if (!t || t.includes(\" \")) bad(`.roles.${role}`, `invalid target \"${t}\"`);\n roles[role] = t;\n }\n }\n }\n\n const mcpSets: Record<string, string[]> = {};\n if (w.mcpSets !== undefined) {\n if (typeof w.mcpSets !== \"object\" || w.mcpSets === null || Array.isArray(w.mcpSets)) {\n bad(\".mcpSets\", \"must be an object of set name \u2192 [server names]\");\n }\n for (const [setName, servers] of Object.entries(w.mcpSets)) {\n if (!Array.isArray(servers) || servers.some((x) => typeof x !== \"string\")) {\n bad(`.mcpSets.${setName}`, \"must be an array of MCP server names\");\n }\n mcpSets[setName] = servers as string[];\n }\n }\n\n let pane = { ...DEFAULT_PANE };\n if (w.pane !== undefined) {\n if (typeof w.pane !== \"object\" || w.pane === null) bad(\".pane\", \"must be an object\");\n const pc = w.pane as Record<string, unknown>;\n if (pc.enabled !== undefined && typeof pc.enabled !== \"boolean\") bad(\".pane.enabled\", \"must be boolean\");\n if (pc.fontSize !== undefined && (typeof pc.fontSize !== \"number\" || pc.fontSize <= 0)) {\n bad(\".pane.fontSize\", \"must be a positive number of points\");\n }\n if (pc.autoExitSecs !== undefined && typeof pc.autoExitSecs !== \"number\") {\n bad(\".pane.autoExitSecs\", \"must be a number\");\n }\n // legacy fontScale (a relative scale, superseded by fontSize) is tolerated and ignored\n pane = {\n enabled: pc.enabled === undefined ? DEFAULT_PANE.enabled : pc.enabled === true,\n fontSize: pc.fontSize === undefined ? DEFAULT_PANE.fontSize : pc.fontSize,\n autoExitSecs: pc.autoExitSecs === undefined ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs,\n };\n }\n\n let routing = { ...DEFAULT_ROUTING, order: [] as string[] };\n if (w.routing !== undefined) {\n if (typeof w.routing !== \"object\" || w.routing === null) bad(\".routing\", \"must be an object\");\n const r = w.routing as Record<string, unknown>;\n if (r.order !== undefined) {\n if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== \"string\")) {\n bad(\".routing.order\", \"must be an array of provider names\");\n }\n routing.order = r.order as string[];\n }\n if (r.cooldownMinutes !== undefined && typeof r.cooldownMinutes !== \"number\") {\n bad(\".routing.cooldownMinutes\", \"must be a number\");\n }\n if (r.retryOnQuota !== undefined && typeof r.retryOnQuota !== \"boolean\") {\n bad(\".routing.retryOnQuota\", \"must be boolean\");\n }\n routing = {\n order: routing.order,\n cooldownMinutes: r.cooldownMinutes === undefined ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,\n retryOnQuota: r.retryOnQuota === undefined ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true,\n };\n }\n\n const active = w.active === undefined || w.active === null ? null : str(w.active);\n if (active !== null && active !== \"auto\" && !(active in providers)) {\n // Tolerated at parse time (a provider may have been removed while active\n // still names it) but every consumer resolves it to a clear error.\n }\n\n return {\n enabled: w.enabled === undefined ? d.enabled : w.enabled === true,\n active,\n providers,\n roles,\n mcpSets,\n pane,\n logDir: str(w.logDir) || d.logDir,\n routing,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\n/**\n * Read the whole config file and return (raw, workers) \u2014 the raw record so\n * callers can rewrite it preserving every other section, the parsed+validated\n * workers section. Unreadable config throws (readJsonStrict), missing is fine.\n */\nexport function readWorkersSection(): {\n raw: Record<string, unknown>;\n workers: WorkersConfig;\n} {\n const raw = readJsonStrict(CONFIG_FILE, \"~/.config/pai/config.json\");\n return { raw, workers: parseWorkersConfig(raw.workers) };\n}\n\n/** Write the workers section back into the config file, atomically. */\nexport function writeWorkersSection(\n raw: Record<string, unknown>,\n workers: WorkersConfig\n): void {\n raw.workers = workers;\n writeJsonAtomic(CONFIG_FILE, raw, { label: \"~/.config/pai/config.json\" });\n}\n\n/** Expand a leading ~ (config values are written with `~` to stay portable). */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\")) return join(homedir(), p.slice(1));\n return p;\n}\n\n// ---------------------------------------------------------------------------\n// Runnability checks (proxy / codex providers included)\n// ---------------------------------------------------------------------------\n\nexport function assertProviderRunnable(name: string, p: WorkerProvider): void {\n if (p.protocol === \"openai\" && !p.upstreamUrl) {\n throw new WorkersConfigError(\n `provider \"${name}\" uses protocol \"openai\" but has no upstreamUrl \u2014 ` +\n `set its Chat Completions base (e.g. \"https://api.openai.com/v1\") ` +\n `so the PAI proxy knows where to translate to.`\n );\n }\n}\n\n/** Where a provider's key file lives, or null. Shared by run + test + add. */\nexport function providerKeyPath(p: WorkerProvider): string | null {\n return p.keyFile ? expandHome(p.keyFile) : null;\n}\n\nexport const DEFAULT_CONTEXT_WINDOW = 200_000;\n\n/** Context window used by the meter when the init event carries none. */\nexport function providerContextWindow(p: WorkerProvider): number {\n return p.contextWindow ?? DEFAULT_CONTEXT_WINDOW;\n}\n\n/** Directory under which inline keys (MCP `key` field) are stored, 0600. */\nexport function keysDir(): string {\n return join(homedir(), \".config\", \"pai\", \"keys\");\n}\n", "/**\n * json-store.ts \u2014 read/write JSON config files without destroying them\n *\n * The failure this exists to prevent:\n *\n * try { return JSON.parse(read(path)); } catch { return {}; }\n * ... later ...\n * write(path, JSON.stringify(ourData));\n *\n * An unreadable file becomes an empty object, and the next write makes that\n * permanent. Silently, exit code 0. This shape appeared three times in this\n * repo against three different files, and twice in AIBroker.\n *\n * The distinction that matters is between *missing* and *unreadable*:\n *\n * missing \u2014 legitimate first run. Start fresh; writing is safe.\n * unreadable \u2014 the file exists and we could not parse it. Those bytes are\n * the only copy of something. Never overwrite them.\n *\n * Collapsing the second into the first is the bug.\n *\n * NOT everything deserves this guard. For a transient buffer \u2014 an undelivered\n * message queue, a cache \u2014 starting fresh IS the correct recovery, and\n * refusing to write would disable the feature permanently. Use `writeJsonAtomic`\n * alone there: it still prevents a crash from truncating a good file, without\n * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n copyFileSync,\n renameSync,\n unlinkSync,\n mkdirSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n/**\n * Read a JSON file, distinguishing \"absent\" from \"damaged\".\n *\n * @param path file to read\n * @param label how to name it to the user, e.g. \"~/.claude.json\"\n * @throws if the file exists but cannot be read or parsed\n */\nexport function readJsonStrict(path: string, label = path): Record<string, unknown> {\n if (!existsSync(path)) return {};\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (e) {\n throw new Error(\n `Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue \u2014 writing now would replace its contents with ours alone.`\n );\n }\n\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n throw new Error(\n `${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue \u2014 overwriting it would destroy whatever it holds.\\n` +\n `Repair the file, or move it aside and re-run this command.`\n );\n }\n}\n\n/**\n * Write JSON without risking the existing file.\n *\n * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file\n * and renames. Rename is atomic within a filesystem, so a crash mid-write\n * leaves the original intact rather than truncated \u2014 which is how these files\n * become corrupt in the first place.\n */\nexport function writeJsonAtomic(\n path: string,\n data: Record<string, unknown>,\n opts: { backup?: boolean; label?: string } = {}\n): void {\n const { backup = true, label = path } = opts;\n const serialized = JSON.stringify(data, null, 2) + \"\\n\";\n\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n if (backup && existsSync(path)) {\n try {\n copyFileSync(path, `${path}.bak-pai`);\n } catch (e) {\n throw new Error(\n `Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to write without a backup.`\n );\n }\n }\n\n const tmp = `${path}.tmp-pai-${process.pid}`;\n try {\n writeFileSync(tmp, serialized, \"utf8\");\n renameSync(tmp, path);\n } catch (e) {\n try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best effort */ }\n throw new Error(\n `Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `The original is unchanged.`\n );\n }\n}\n", "/**\n * config.ts \u2014 Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\nimport type { TaskConfig } from \"../tasks/types.js\";\nimport { DEFAULT_TASK_CONFIG } from \"../tasks/types.js\";\nimport { paiSocketPath } from \"../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchConfig {\n /** Default search mode: 'keyword', 'semantic', or 'hybrid'. Default: 'keyword'. */\n mode: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Enable cross-encoder reranking by default. Default: true. */\n rerank: boolean;\n /** Recency boost half-life in days. 0 = off. Default: 90. */\n recencyBoostDays: number;\n /** Default max results. Default: 10. */\n defaultLimit: number;\n /** Default snippet length for MCP results. Default: 200. */\n snippetLength: number;\n}\n\nexport interface PostgresConfig {\n /** Connection string \u2014 if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n /** Run an embed pass 60s after daemon start. Off by default: with a large\n * backlog it makes every restart a CPU storm, and it ignores the interval. */\n embedOnStartup: boolean;\n\n /** Local hour (0-23) to anchor the recurring index/embed cycle to. When unset,\n * the cycle is anchored to daemon start, so a daytime restart pins every\n * later pass to daytime too \u2014 a 24h interval does not by itself mean \"at\n * night\". Set this to run maintenance in a fixed window regardless of when\n * the machine last booted. */\n maintenanceHour?: number;\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n vaultPath?: string;\n\n /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n vaultProjectId?: number;\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n\n /** Search defaults \u2014 applied when MCP tool or CLI doesn't specify a value */\n search: SearchConfig;\n\n /** Task bus \u2014 optional external tracker for cross-session work */\n tasks: TaskConfig;\n\n /** Who \"me\" is \u2014 addresses that count as the user's own. */\n identity: IdentityConfig;\n}\n\n/**\n * The user's own identity, for anything that delivers back to them.\n *\n * This exists so \"my own address\" is a fact the system can check rather than\n * something a model infers from context. An assistant deciding on the spot\n * whether an address looks like the user's is exactly the judgement that should\n * not be re-made per message.\n *\n * Empty by default and never guessed at install time: an empty `selfEmails`\n * means nothing is self-addressed, so anything reading this fails closed.\n */\nexport interface IdentityConfig {\n /**\n * Where digests and \"mail me X\" requests are delivered.\n *\n * Must be a mailbox separate from the account doing the sending. Gmail files\n * a message sent from an account to itself \u2014 or to one of its own domain\n * aliases \u2014 under Sent only, and it never reaches the inbox. The send reports\n * success, so this fails silently and looks exactly like delivery. Observed\n * 2026-08-01: owner@example.ch \u2192 owner@example.de, sent fine, invisible.\n *\n * Where a separate mailbox is not available, deliver by writing the message\n * and adding the INBOX label to it rather than relying on the send path.\n */\n deliverTo?: string;\n\n /**\n * Every address that counts as the user's own.\n *\n * Used as an allowlist by anything that may act without review \u2014 outbound\n * mail being the case that motivated it. Membership is the whole test: an\n * address that is not listed is not the user's, however similar it looks.\n * Plus-aliases and domain aliases must be listed explicitly rather than\n * pattern-matched, because the patterns that would match them also match\n * addresses belonging to other people.\n */\n selfEmails: string[];\n\n /** The account used to send on the user's behalf, when one is configured. */\n sendingAccount?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-user Postgres isolation\n// ---------------------------------------------------------------------------\n\n/** Derive a per-user Postgres database name: pai_<username> */\nfunction perUserDbName(): string {\n const username = userInfo().username;\n // Sanitize: only allow alphanumeric and underscore for Postgres identifiers\n const safe = username.replace(/[^a-zA-Z0-9_]/g, \"_\").toLowerCase();\n return `pai_${safe}`;\n}\n\n/** Derive the per-user connection string */\nfunction perUserConnectionString(): string {\n const db = perUserDbName();\n return `postgresql://pai:pai@localhost:5432/${db}`;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: paiSocketPath(),\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n embedOnStartup: false,\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: perUserConnectionString(),\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n tasks: DEFAULT_TASK_CONFIG,\n // Deliberately empty. An install must not guess who the user is: a wrong\n // guess here is an address that can be mailed without review.\n identity: { selfEmails: [] },\n search: {\n mode: \"keyword\",\n rerank: true,\n recencyBoostDays: 90,\n defaultLimit: 10,\n snippetLength: 200,\n },\n};\n\n/** Config template \u2014 generated at runtime so the DB name is per-user */\nfunction configTemplate(): string {\n return `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"${perUserConnectionString()}\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\",\n \"vaultPath\": \"\",\n \"vaultProjectId\": 0,\n \"search\": {\n \"mode\": \"keyword\",\n \"rerank\": true,\n \"recencyBoostDays\": 90,\n \"defaultLimit\": 10,\n \"snippetLength\": 200\n }\n}\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULTS };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n // Compat: config.json may use \"obsidianVaultPath\" (legacy key) instead of \"vaultPath\".\n // Map it across so the daemon picks it up correctly.\n if (parsed.obsidianVaultPath && !parsed.vaultPath) {\n parsed.vaultPath = parsed.obsidianVaultPath;\n process.stderr.write(\n `[pai-daemon] Config: mapped obsidianVaultPath \u2192 vaultPath (${parsed.vaultPath})\\n`\n );\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE)) {\n try {\n writeFileSync(CONFIG_FILE, configTemplate(), \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n", "/**\n * types.ts \u2014 Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" \u2014 Use the per-event routing table (default)\n * - \"voice\" \u2014 All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" \u2014 All events go to WhatsApp text\n * - \"ntfy\" \u2014 All events go to ntfy.sh\n * - \"macos\" \u2014 All events go to macOS notifications\n * - \"cli\" \u2014 All events go to CLI stdout only\n * - \"off\" \u2014 Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event \u2192 channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type \u2014 used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n", "/**\n * types.ts \u2014 Task Bus type definitions\n *\n * Defines the provider registry, ownership resolution, and configuration schema\n * for PAI's cross-session task subsystem.\n *\n * The task bus routes work between PAI sessions through an external tracker.\n * A session files a task; a routine reads it later and dispatches it to the\n * session that owns it \u2014 spawning one if none is running.\n *\n * See Notes/docs/task-bus.md for the architecture and its constraints.\n */\n\n// ---------------------------------------------------------------------------\n// Provider identifiers\n// ---------------------------------------------------------------------------\n\nexport type ProviderId = \"todoist\";\n\n// ---------------------------------------------------------------------------\n// Ownership\n// ---------------------------------------------------------------------------\n\n/**\n * Prefix marking a tracker label as a PAI ownership assertion.\n * A task labelled `pai:acme-api` is owned by the `acme-api` project.\n */\nexport const OWNER_LABEL_PREFIX = \"pai:\";\n\n/**\n * How a task's owner was determined. Recorded so the routine can explain\n * itself, and so a mis-resolution is diagnosable rather than silent.\n *\n * - \"label\" \u2014 an explicit `pai:<project>` label (authoritative)\n * - \"container\" \u2014 the enclosing sub-project name matched a PAI alias (fallback)\n * - \"none\" \u2014 unresolved; the task stays in the findings inbox\n */\nexport type OwnerSource = \"label\" | \"container\" | \"none\";\n\nexport interface TaskOwner {\n /** Resolved PAI project short name, e.g. \"acme-api\". Null when UNROUTED. */\n project: string | null;\n /** Absolute path to the project root. Null when UNROUTED. */\n rootPath: string | null;\n source: OwnerSource;\n /**\n * The raw string that resolution was attempted against, kept for diagnostics\n * when `source` is \"none\" \u2014 e.g. a \"Reading List \uD83D\uDCDA\" container matches no\n * PAI project, which is expected rather than a fault.\n */\n rawHint?: string;\n}\n\n/** An unresolved owner. UNROUTED is a normal state, not an error. */\nexport const UNROUTED: TaskOwner = {\n project: null,\n rootPath: null,\n source: \"none\",\n};\n\n// ---------------------------------------------------------------------------\n// Tasks\n// ---------------------------------------------------------------------------\n\nexport type TaskPriority = \"p1\" | \"p2\" | \"p3\" | \"p4\";\n\nexport interface Task {\n /** Provider-native task ID. Opaque; never parsed. */\n id: string;\n title: string;\n /**\n * Full procedure AND reasoning \u2014 enough that the task is actionable months\n * later, or by the user alone, without re-deriving anything. Enforced at\n * filing time rather than left to discipline.\n */\n body: string;\n owner: TaskOwner;\n /** ISO 8601 date or datetime. Null when the task has no due date. */\n due: string | null;\n /**\n * The tracker's own recurrence text, e.g. \"every day at 08:00\". Null for a\n * one-off.\n *\n * Kept verbatim rather than parsed into a rule because it is also the only\n * way to write a recurrence back: Todoist re-parses this string, and it is\n * what lets a due date be restored without destroying the recurrence.\n */\n recurrence?: string | null;\n priority: TaskPriority;\n labels: string[];\n /**\n * Stable reference to the artifact this task is about. Prefer a `hook://`\n * URL over a filesystem path \u2014 it survives renames and moves, and opens in\n * DEVONthink To Go on iOS.\n */\n sourceUrl?: string;\n /** True for organizational headers that cannot be completed. */\n isHeader?: boolean;\n}\n\n/** A task being filed. `owner` is a project short name, resolved on write. */\nexport interface NewTask {\n title: string;\n body: string;\n owner?: string | null;\n due?: string;\n priority?: TaskPriority;\n labels?: string[];\n sourceUrl?: string;\n /**\n * Sub-project to file into, created if absent.\n *\n * The convention is one sub-project per PAI project under the bus root. It\n * previously existed only in the shape of the data, so every session had to\n * re-derive it \u2014 and a session that inferred cautiously filed flat instead,\n * which is exactly the pile the convention prevents.\n */\n into?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface ListOptions {\n /** Only tasks due on or before this ISO date. Omit for all open tasks. */\n dueBefore?: string;\n /** Restrict to one resolved owner. Omit for every owner. */\n owner?: string;\n /** Include tasks that resolved to UNROUTED. Default: true. */\n includeUnrouted?: boolean;\n limit?: number;\n}\n\nexport interface TaskProvider {\n readonly providerId: ProviderId;\n\n /**\n * False when no credential is configured. The bus degrades to a no-op\n * rather than failing \u2014 a user without a tracker still gets working PAI.\n */\n isConfigured(): boolean;\n\n listOpen(opts: ListOptions): Promise<Task[]>;\n add(task: NewTask): Promise<Task>;\n complete(id: string): Promise<void>;\n\n /**\n * Rewrite a task's due date through the tracker's natural-language field.\n *\n * Optional, and deliberately expressed as a string rather than a date: a\n * recurring task's schedule and its next occurrence are the same field, so\n * moving the date without the rule silently downgrades a routine to a one-off.\n * A provider that cannot express both at once should not offer this.\n */\n setDue?(id: string, dueString: string): Promise<void>;\n\n /**\n * Sub-projects under the bus root \u2014 the set of addresses a task can be filed\n * against, one per session.\n *\n * Optional because it is not universal: a tracker may address work by tag or\n * list rather than by nested project, and forcing a nesting concept onto one\n * that has none would mean faking it. A provider without these simply does\n * not offer session-scoped inboxes, and callers say so rather than failing.\n */\n listSubProjects?(): Promise<Array<{ id: string; name: string }>>;\n findOrCreateSubProject?(name: string): Promise<{ id: string; created: boolean }>;\n\n /**\n * The comment thread on a task, oldest first.\n *\n * Optional because not every tracker has threaded comments. Where it exists,\n * the thread is usually where the reasoning lives \u2014 the question, the answer,\n * the correction \u2014 and completing the task takes it out of view. That is what\n * the archive exists to keep.\n */\n listComments?(taskId: string): Promise<Array<{ id: string; content: string; postedAt?: string }>>;\n\n /**\n * One task by id, whether open or completed.\n *\n * Needed because archiving runs at or after completion, and a completed task\n * is gone from `listOpen` \u2014 which is exactly the moment its discussion stops\n * being visible and most needs keeping.\n */\n getTask?(id: string): Promise<Task | null>;\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface TodoistProviderConfig {\n enabled: boolean;\n /**\n * API token. Resolution order is apiKey \u2192 TODOIST_API_KEY env \u2192 unconfigured.\n *\n * Never read this from another tool's config file. PAI ships as a product;\n * scraping ~/.claude.json for a key belonging to the Todoist MCP is not\n * acceptable even though the key is sitting there.\n */\n apiKey?: string;\n /**\n * Tracker project ID that roots the bus (the \"Claude \uD83E\uDD16\" project).\n *\n * Stored as an ID, never a name. Todoist's project search silently returns\n * zero results for names containing emoji \u2014 resolving by name would report\n * \"no tasks\" instead of failing, which is the exact class of silent failure\n * this subsystem exists to surface.\n */\n rootProjectId?: string;\n /** Section ID for the findings inbox. Tasks land here when UNROUTED. */\n findingsSectionId?: string;\n}\n\nexport interface TaskConfig {\n /** Master switch. When false the bus is inert. */\n enabled: boolean;\n providers: {\n todoist: TodoistProviderConfig;\n };\n /**\n * Dispatch work to the owning session automatically, spawning one if absent.\n * Requires AIBroker. When false \u2014 or when AIBroker is unavailable \u2014 PAI\n * reports which project owns each task and leaves acting to the user.\n */\n autoDispatch: boolean;\n\n /**\n * Seconds AIBroker may spend on a single dispatch, spawn included.\n *\n * Passed down to the transport so both sides share one deadline. Raise it on\n * a loaded machine where sessions are slow to start accepting input.\n */\n dispatchTimeoutSecs?: number;\n\n /**\n * Project a task goes to when it carries the bare `pai` marker and its\n * location says nothing \u2014 an Inbox capture, typically.\n *\n * This is the one thing a task's location cannot express: \"an AI should take\n * this, and I do not know which one yet\". Everything else is answered by the\n * project the task sits in.\n *\n * Unset means such a task stays UNROUTED, which is a legitimate choice: it\n * then surfaces in the findings inbox for triage rather than being guessed at.\n */\n defaultOwner?: string;\n}\n\nexport const DEFAULT_TASK_CONFIG: TaskConfig = {\n enabled: false,\n providers: {\n todoist: {\n enabled: false,\n },\n },\n autoDispatch: false,\n};\n\n// ---------------------------------------------------------------------------\n// Dispatch results\n// ---------------------------------------------------------------------------\n\n/**\n * What happened to one task during a dispatch run.\n *\n * - \"delivered\" \u2014 sent to an already-running session, and confirmed submitted\n * - \"queued\" \u2014 typed into a live session that was mid-turn, so submission\n * could not be confirmed inside the window. This is delivery:\n * Claude Code holds typed input until the current turn ends.\n * Never retried \u2014 the text is already in the input box, so a\n * second attempt is a second copy, not a retry. One trigger\n * arrived three times on 2026-08-01 for exactly that reason.\n * - \"spawned\" \u2014 none running; one was launched, came up, and received it\n * - \"unrouted\" \u2014 no owner resolved; left in the findings inbox\n * - \"unlaunchable\" \u2014 an owner resolved but no PAI alias exists to launch it\n * - \"unreachable\" \u2014 a session was launched but never became ready to accept input\n * - \"skipped\" \u2014 autoDispatch is off, or no transport; reported only\n *\n * `unlaunchable` and `unreachable` are distinct because the fixes differ:\n * the first is a setup gap (register an alias), the second is a runtime\n * failure (find out why the session did not come up). Collapsing them would\n * send users looking in the wrong place.\n *\n * None of these are errors. A task that could not be delivered is a routing\n * result to report, not an exception to throw.\n */\nexport type DispatchOutcome =\n | \"delivered\"\n | \"queued\"\n | \"spawned\"\n | \"unrouted\"\n | \"unlaunchable\"\n | \"unreachable\"\n | \"skipped\";\n\nexport interface DispatchResult {\n task: Task;\n outcome: DispatchOutcome;\n /** Session the task reached, when it reached one. */\n session?: string;\n /** Why the task did not reach a session. Present on failure outcomes. */\n reason?: string;\n}\n", "/**\n * runtime-paths.ts \u2014 the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else \u2014 a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon \u2014 read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? \"/tmp/pai-daemon.pid\";\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n] as const;\n", "/**\n * paths.ts \u2014 where worker run artefacts live.\n *\n * Everything a run writes (event mirror, status file, pane registry, routing\n * state, the empty MCP config) sits under one logDir so the whole tree is\n * disposable and configurable: `workers.logDir`, default ~/.claude/logs/workers.\n */\n\nimport { join } from \"node:path\";\nimport { expandHome, type WorkersConfig } from \"./config.js\";\n\n/** Absolute logDir for the given config. */\nexport function workersLogDir(config: WorkersConfig): string {\n return expandHome(config.logDir);\n}\n\nexport function statusPath(logDir: string, id: string): string {\n return join(logDir, `${id}.status`);\n}\n\nexport function eventsPath(logDir: string, id: string): string {\n return join(logDir, `${id}.jsonl`);\n}\n\nexport function ledgerPath(logDir: string): string {\n return join(logDir, \"ledger.log\");\n}\n\nexport function routingStatePath(logDir: string): string {\n return join(logDir, \"routing-state.json\");\n}\n\nexport function panesDir(logDir: string): string {\n return join(logDir, \"panes\");\n}\n\n/**\n * The strict empty MCP config handed to headless workers. Written into the\n * logDir on demand (never into the user's vendor config directory \u2014 this is\n * PAI state, not vendor state).\n */\nexport function noMcpConfigPath(logDir: string): string {\n return join(logDir, \"no-mcp.json\");\n}\n", "/**\n * ledger.ts \u2014 the routing ledger, one append-only log for every worker event.\n *\n * Line shapes (whitespace-aligned so `tail` reads as a table):\n *\n * 2026-09-17 12:00:00 WORKER-START id=<id> provider=<p> mode=headless model=<m> cwd=<cwd> label=<label>\n * 2026-09-17 12:01:00 WORKER-END id=<id> provider=<p> mode=headless model=<m> rc=0 secs=60 turns=3 tools=8 label=<label>\n * 2026-09-17 12:00:20 WORKER-REROUTE from=<a> to=<b> reason=quota\n * 2026-09-17 12:00:00 DENIED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>\n * 2026-09-17 12:00:00 ALLOWED-ANTHROPIC-AGENT cwd=<cwd> desc=<desc>\n *\n * `pai worker log` (and glm-log before it) counts these; keep the tags stable.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport function ledgerStamp(d: Date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +\n `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n );\n}\n\nexport interface LedgerLine {\n stamp: string;\n event: string;\n fields: Record<string, string>;\n}\n\n/** Parse `2026-09-17 12:00:00 TAG key=value \u2026` into its parts. */\nexport function parseLedgerLine(line: string): LedgerLine | null {\n const m = line.match(/^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) (\\S+)(?: (.*))?$/);\n if (!m) return null;\n const fields: Record<string, string> = {};\n // values are whitespace-collapsed by the writers, so spaces safely delimit\n for (const part of (m[3] ?? \"\").split(\" \")) {\n if (!part) continue;\n const eq = part.indexOf(\"=\");\n if (eq <= 0) continue;\n fields[part.slice(0, eq)] = part.slice(eq + 1);\n }\n return { stamp: m[1], event: m[2], fields };\n}\n\nexport function parseLedger(text: string): LedgerLine[] {\n const out: LedgerLine[] = [];\n for (const line of text.split(\"\\n\")) {\n if (!line.trim()) continue;\n const parsed = parseLedgerLine(line);\n if (parsed) out.push(parsed);\n }\n return out;\n}\n\n/** Append one ledger line. `kv` order is the caller's; values are flattened. */\nexport function appendLedger(\n path: string,\n event: string,\n kv: Record<string, string | number | null | undefined>,\n now: Date = new Date()\n): void {\n const parts: string[] = [];\n for (const [k, v] of Object.entries(kv)) {\n if (v === undefined || v === null) continue;\n parts.push(`${k}=${String(v).replace(/\\s+/g, \" \")}`);\n }\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n const tail = parts.length ? \" \" + parts.join(\" \") : \"\";\n appendFileSync(path, `${ledgerStamp(now)} ${event}${tail}\\n`, \"utf8\");\n}\n\nexport interface LedgerSummary {\n scope: string;\n started: number;\n endedOk: number;\n endedFailed: number;\n denied: number;\n allowed: number;\n reroutes: number;\n lastLines: string[];\n}\n\n/** The counts `pai worker log` prints, over today's lines or the whole file. */\nexport function ledgerSummary(\n path: string,\n scope: \"today\" | \"all\",\n lastN = 15,\n now: Date = new Date()\n): LedgerSummary | null {\n if (!existsSync(path)) return null;\n const text = readFileSync(path, \"utf8\");\n const today = ledgerStamp(now).slice(0, 10);\n const lines = text.split(\"\\n\").filter((l) => l.trim());\n const scoped =\n scope === \"all\" ? lines : lines.filter((l) => l.startsWith(today));\n const parsed = scoped.map(parseLedgerLine).filter((x): x is LedgerLine => x !== null);\n const count = (ev: string) => parsed.filter((p) => p.event === ev).length;\n const ends = parsed.filter((p) => p.event === \"WORKER-END\");\n const ok = ends.filter((p) => p.fields.rc === \"0\").length;\n return {\n scope: scope === \"all\" ? \"all time\" : `today ${today}`,\n started: count(\"WORKER-START\"),\n endedOk: ok,\n endedFailed: ends.length - ok,\n denied: count(\"DENIED-ANTHROPIC-AGENT\"),\n allowed: count(\"ALLOWED-ANTHROPIC-AGENT\"),\n reroutes: count(\"WORKER-REROUTE\"),\n lastLines: lines.slice(-lastN),\n };\n}\n", "#!/usr/bin/env node\n\n/**\n * route-agents-to-worker.ts \u2014 PreToolUse hook on the Agent tool.\n *\n * Denies every in-process subagent when worker providers are configured, so no\n * worker ever runs on Anthropic. The deny reason tells the orchestrator how to\n * delegate through `pai worker run` instead \u2014 same idea as the old\n * route-agents-to-glm.sh, but provider-neutral and ledgered in the workers\n * log dir.\n *\n * Allows (and does not touch) when:\n * - the tool is not the Agent tool (this hook is Agent-matched, but stay safe)\n * - ALLOW_ANTHROPIC_AGENTS=1 is in the environment (one-session bypass)\n * - workers are off or no provider is configured\n *\n * Every decision is appended to the routing ledger read by `pai worker log`.\n */\n\nimport { readWorkersSection } from \"../../../workers/config.js\";\nimport { workersLogDir, ledgerPath } from \"../../../workers/paths.js\";\nimport { appendLedger } from \"../../../workers/ledger.js\";\n\ninterface HookInput {\n session_id?: string;\n cwd?: string;\n tool_name?: string;\n tool_input?: { description?: string; prompt?: string; subagent_type?: string } | string;\n}\n\nconst ALLOW = JSON.stringify({ hookSpecificOutput: { hookEventName: \"PreToolUse\", permissionDecision: \"allow\" } });\n\nfunction deny(reason: string): string {\n return JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n permissionDecisionReason: reason,\n },\n });\n}\n\nfunction shortLabel(input: HookInput): string {\n const desc =\n typeof input.tool_input === \"object\" && input.tool_input !== null\n ? input.tool_input.description ?? \"\"\n : \"\";\n return desc.replace(/\\s+/g, \" \").trim().slice(0, 60);\n}\n\nasync function main(): Promise<void> {\n let text = \"\";\n try {\n for await (const chunk of process.stdin) text += chunk;\n } catch {\n process.stdout.write(ALLOW);\n return;\n }\n if (!text.trim()) {\n process.stdout.write(ALLOW);\n return;\n }\n\n let input: HookInput;\n try {\n input = JSON.parse(text) as HookInput;\n } catch {\n process.stdout.write(ALLOW);\n return;\n }\n\n if (input.tool_name && input.tool_name !== \"Agent\") {\n process.stdout.write(ALLOW);\n return;\n }\n\n const label = shortLabel(input);\n const cwd = input.cwd ?? process.cwd();\n\n const ledger = (event: string): void => {\n try {\n appendLedger(ledgerPath(workersLogDir(readWorkersSection().workers)), event, {\n cwd,\n ...(label ? { label } : {}),\n });\n } catch {\n // no config / no log dir yet \u2014 the decision below still stands\n }\n };\n\n if (process.env.ALLOW_ANTHROPIC_AGENTS === \"1\") {\n ledger(\"ALLOWED-ANTHROPIC-AGENT\");\n process.stdout.write(ALLOW);\n return;\n }\n\n let workers: ReturnType<typeof readWorkersSection>[\"workers\"];\n try {\n workers = readWorkersSection().workers;\n } catch {\n // broken workers config must not break the session\n process.stdout.write(ALLOW);\n return;\n }\n if (!workers.enabled || Object.keys(workers.providers).length === 0) {\n process.stdout.write(ALLOW);\n return;\n }\n\n ledger(\"DENIED-ANTHROPIC-AGENT\");\n process.stdout.write(\n deny(\n \"Agent tool is disabled: subagents run on the configured worker provider, not Anthropic. \" +\n \"Delegate with Bash instead, in the background:\\n\\n\" +\n `pai worker run --label \"${label || \"task\"}\" --role research -p '<full, self-contained task spec>' ` +\n \"--allowedTools 'Read,Edit,Write,Bash,Grep,Glob' --output-format json\\n\\n\" +\n \"- Run it with run_in_background: true and always with a timeout.\\n\" +\n \"- Use --role spotcheck (or implement) as the task demands.\\n\" +\n \"- Web research: add WebSearch,WebFetch to --allowedTools.\\n\" +\n \"- The answer is in the `result` field of the JSON it prints. Review the diff yourself.\\n\" +\n \"- pai worker ps lists running workers; pai worker follow <id> shows one live.\\n\" +\n \"Keep only orchestration, review and synthesis in this session. \" +\n \"To run subagents on Anthropic for one session: start it with ALLOW_ANTHROPIC_AGENTS=1.\"\n )\n );\n}\n\nmain().catch(() => process.stdout.write(ALLOW));\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["homedir", "join", "DEFAULT_ROUTING", "join", "homedir", "join", "join", "existsSync", "mkdirSync", "readFileSync", "existsSync", "mkdirSync"]
|
|
7
|
+
}
|