@ctliz/agent-intercom-claude 0.12.0-connect.3
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/.claude-plugin/plugin.json +26 -0
- package/.mcp.json +10 -0
- package/CHANGELOG.md +62 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +506 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +460 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +125 -0
- package/broker/boss-contracts.ts +267 -0
- package/broker/broker.ts +2028 -0
- package/broker/client.ts +1048 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +463 -0
- package/broker/validation.ts +132 -0
- package/claude/cci.ts +512 -0
- package/claude/ccim.ts +31 -0
- package/claude/cli-runner.ts +220 -0
- package/claude/contact.ts +33 -0
- package/claude/inbox-monitor.ts +49 -0
- package/claude/inbox.ts +77 -0
- package/claude/mcp-protocol.ts +253 -0
- package/claude/native-bridge.ts +219 -0
- package/claude/native-protocol.ts +346 -0
- package/claude/permission-policy.ts +287 -0
- package/claude/runtime.ts +510 -0
- package/claude/server.ts +69 -0
- package/claude/team.ts +348 -0
- package/claude/transport.ts +97 -0
- package/claude/worker-config.ts +198 -0
- package/claude/worker-daemon.ts +418 -0
- package/commands/intercom-id.md +9 -0
- package/commands/intercom.md +8 -0
- package/config.ts +195 -0
- package/dist/broker.mjs +2530 -0
- package/dist/cci.mjs +3607 -0
- package/dist/ccim.mjs +3621 -0
- package/dist/claude-server.mjs +2730 -0
- package/dist/inbox-monitor.mjs +68 -0
- package/dist/worker-daemon.mjs +2797 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-claude-link.txt +21 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/monitors/monitors.json +8 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +93 -0
- package/protocol-v4/contract.ts +45 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build.mjs +71 -0
- package/scripts/verify-core-provenance.mjs +64 -0
- package/scripts/verify-packed-runtime.mjs +140 -0
- package/skills/claude-intercom/SKILL.md +90 -0
- package/types.ts +212 -0
package/dist/cci.mjs
ADDED
|
@@ -0,0 +1,3607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// claude/cci.ts
|
|
4
|
+
import { once as once2 } from "node:events";
|
|
5
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6
|
+
import { spawn as spawn4, spawnSync as spawnSync3 } from "node:child_process";
|
|
7
|
+
import { existsSync as existsSync5, realpathSync as realpathSync3, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8
|
+
import { basename as basename3, dirname as dirname7, join as join10, resolve as resolve4 } from "node:path";
|
|
9
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
|
|
12
|
+
// claude/worker-daemon.ts
|
|
13
|
+
import { once } from "node:events";
|
|
14
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
|
|
17
|
+
// claude/cli-runner.ts
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
// claude/permission-policy.ts
|
|
21
|
+
import {
|
|
22
|
+
closeSync,
|
|
23
|
+
constants,
|
|
24
|
+
fstatSync,
|
|
25
|
+
lstatSync,
|
|
26
|
+
openSync,
|
|
27
|
+
realpathSync
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { dirname, isAbsolute, join, parse, relative, sep } from "node:path";
|
|
30
|
+
var CLAUDE_PERMISSION_MODES = [
|
|
31
|
+
"acceptEdits",
|
|
32
|
+
"auto",
|
|
33
|
+
"bypassPermissions",
|
|
34
|
+
"manual",
|
|
35
|
+
"dontAsk",
|
|
36
|
+
"plan"
|
|
37
|
+
];
|
|
38
|
+
function validateClaudePermissionMode(value, field = "permissionMode") {
|
|
39
|
+
if (typeof value !== "string" || !CLAUDE_PERMISSION_MODES.includes(value)) {
|
|
40
|
+
throw new Error(`${field} must be one of: ${CLAUDE_PERMISSION_MODES.join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function resolveClaudePermissionPolicy(input) {
|
|
45
|
+
const ceiling = input.ceiling ?? "standard";
|
|
46
|
+
if (ceiling !== "standard" && ceiling !== "read-only") {
|
|
47
|
+
throw new Error("permission ceiling must be standard or read-only");
|
|
48
|
+
}
|
|
49
|
+
if (input.dangerouslySkipPermissions !== void 0 && typeof input.dangerouslySkipPermissions !== "boolean") {
|
|
50
|
+
throw new Error("dangerouslySkipPermissions must be a boolean");
|
|
51
|
+
}
|
|
52
|
+
const dangerouslySkipPermissions = input.dangerouslySkipPermissions === true;
|
|
53
|
+
const requestedMode = input.permissionMode === void 0 ? void 0 : validateClaudePermissionMode(input.permissionMode);
|
|
54
|
+
if (ceiling === "read-only") {
|
|
55
|
+
if (dangerouslySkipPermissions) {
|
|
56
|
+
throw new Error("read-only Claude roles cannot skip permission checks");
|
|
57
|
+
}
|
|
58
|
+
if (requestedMode !== void 0 && requestedMode !== "plan") {
|
|
59
|
+
throw new Error("read-only Claude roles require permission mode plan");
|
|
60
|
+
}
|
|
61
|
+
return { permissionMode: "plan", dangerouslySkipPermissions: false, ceiling };
|
|
62
|
+
}
|
|
63
|
+
if (dangerouslySkipPermissions) {
|
|
64
|
+
return { dangerouslySkipPermissions: true, ceiling };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
permissionMode: requestedMode ?? "manual",
|
|
68
|
+
dangerouslySkipPermissions: false,
|
|
69
|
+
ceiling
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var PERMISSION_OVERRIDE_OPTIONS = /* @__PURE__ */ new Set([
|
|
73
|
+
"--allow-dangerously-skip-permissions",
|
|
74
|
+
"--dangerously-skip-permissions",
|
|
75
|
+
"--permission-mode"
|
|
76
|
+
]);
|
|
77
|
+
var CLAUDE_2_1_220_HARDENED_CAPABILITY_OPTIONS = [
|
|
78
|
+
"--add-dir",
|
|
79
|
+
"--agent",
|
|
80
|
+
"--agents",
|
|
81
|
+
"--allowed-tools",
|
|
82
|
+
"--allowedTools",
|
|
83
|
+
"--mcp-config",
|
|
84
|
+
"--plugin-dir",
|
|
85
|
+
"--plugin-dir-no-mcp",
|
|
86
|
+
"--plugin-url",
|
|
87
|
+
"--setting-sources",
|
|
88
|
+
"--settings",
|
|
89
|
+
"--tools",
|
|
90
|
+
"--worktree",
|
|
91
|
+
"-w"
|
|
92
|
+
];
|
|
93
|
+
var HARDENED_PERMISSION_CAPABILITY_OPTIONS = new Set(
|
|
94
|
+
CLAUDE_2_1_220_HARDENED_CAPABILITY_OPTIONS
|
|
95
|
+
);
|
|
96
|
+
function permissionCapabilityOption(arg) {
|
|
97
|
+
const equalsIndex = arg.indexOf("=");
|
|
98
|
+
const option = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex);
|
|
99
|
+
if (HARDENED_PERMISSION_CAPABILITY_OPTIONS.has(option)) return option;
|
|
100
|
+
if (arg.length > 1 && arg.startsWith("-") && !arg.startsWith("--")) return arg;
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
function isContainedPath(root, candidate) {
|
|
104
|
+
const pathFromRoot = relative(root, candidate);
|
|
105
|
+
return pathFromRoot === "" || !isAbsolute(pathFromRoot) && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`);
|
|
106
|
+
}
|
|
107
|
+
function sameFile(left, right) {
|
|
108
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
109
|
+
}
|
|
110
|
+
function pathError(field, detail, cause) {
|
|
111
|
+
return cause === void 0 ? new Error(`${field} ${detail}`) : new Error(`${field} ${detail}`, { cause });
|
|
112
|
+
}
|
|
113
|
+
function inspectExistingRealDirectory(input, base, field) {
|
|
114
|
+
if (input.length === 0) {
|
|
115
|
+
throw pathError(field, "must name an existing real directory");
|
|
116
|
+
}
|
|
117
|
+
const rawAbsolute = isAbsolute(input) ? input : `${base.endsWith(sep) ? base : `${base}${sep}`}${input}`;
|
|
118
|
+
const root = parse(rawAbsolute).root;
|
|
119
|
+
let cursor = root;
|
|
120
|
+
const inspect = (path) => {
|
|
121
|
+
let descriptor2;
|
|
122
|
+
try {
|
|
123
|
+
descriptor2 = lstatSync(path, { bigint: true });
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
throw pathError(field, "must name an existing real directory", cause);
|
|
126
|
+
}
|
|
127
|
+
if (descriptor2.isSymbolicLink()) {
|
|
128
|
+
throw pathError(field, "must not contain a symbolic-link component");
|
|
129
|
+
}
|
|
130
|
+
if (!descriptor2.isDirectory()) {
|
|
131
|
+
throw pathError(field, "must contain only existing directories");
|
|
132
|
+
}
|
|
133
|
+
return descriptor2;
|
|
134
|
+
};
|
|
135
|
+
let descriptor = inspect(cursor);
|
|
136
|
+
for (const component of rawAbsolute.slice(root.length).split(sep)) {
|
|
137
|
+
if (component === "" || component === ".") continue;
|
|
138
|
+
cursor = component === ".." ? dirname(cursor) : join(cursor, component);
|
|
139
|
+
descriptor = inspect(cursor);
|
|
140
|
+
}
|
|
141
|
+
let canonicalPath;
|
|
142
|
+
try {
|
|
143
|
+
canonicalPath = realpathSync(cursor);
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
throw pathError(field, "cannot be bound to a canonical real directory", cause);
|
|
146
|
+
}
|
|
147
|
+
return { canonicalPath, descriptor };
|
|
148
|
+
}
|
|
149
|
+
function openBoundDirectory(input, base, field) {
|
|
150
|
+
if (typeof constants.O_DIRECTORY !== "number" || typeof constants.O_NOFOLLOW !== "number") {
|
|
151
|
+
throw pathError(field, "cannot be securely bound on this platform");
|
|
152
|
+
}
|
|
153
|
+
const first = inspectExistingRealDirectory(input, base, field);
|
|
154
|
+
let fd;
|
|
155
|
+
try {
|
|
156
|
+
fd = openSync(first.canonicalPath, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
throw pathError(field, "cannot be bound to an open real directory", cause);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
162
|
+
if (!opened.isDirectory() || !sameFile(opened, first.descriptor)) {
|
|
163
|
+
throw pathError(field, "changed while its real directory was being bound");
|
|
164
|
+
}
|
|
165
|
+
const second = inspectExistingRealDirectory(input, base, field);
|
|
166
|
+
if (second.canonicalPath !== first.canonicalPath || !sameFile(opened, second.descriptor)) {
|
|
167
|
+
throw pathError(field, "changed while its real directory was being bound");
|
|
168
|
+
}
|
|
169
|
+
return { path: first.canonicalPath, fd };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function bindHardenedClaudePaths(cwd, addDirs, field = "hardened Claude paths") {
|
|
176
|
+
const descriptors = [];
|
|
177
|
+
try {
|
|
178
|
+
const cwdBinding = openBoundDirectory(cwd, process.cwd(), `${field}.cwd`);
|
|
179
|
+
descriptors.push(cwdBinding.fd);
|
|
180
|
+
const canonicalAddDirs = addDirs.map((addDir, index) => {
|
|
181
|
+
const binding = openBoundDirectory(addDir, cwdBinding.path, `${field}.addDirs[${index}]`);
|
|
182
|
+
descriptors.push(binding.fd);
|
|
183
|
+
if (!isContainedPath(cwdBinding.path, binding.path)) {
|
|
184
|
+
throw pathError(`${field}.addDirs[${index}]`, "must stay within the assigned workspace root");
|
|
185
|
+
}
|
|
186
|
+
return binding.path;
|
|
187
|
+
});
|
|
188
|
+
let released = false;
|
|
189
|
+
return {
|
|
190
|
+
cwd: cwdBinding.path,
|
|
191
|
+
addDirs: canonicalAddDirs,
|
|
192
|
+
release() {
|
|
193
|
+
if (released) return;
|
|
194
|
+
released = true;
|
|
195
|
+
for (const fd of descriptors.splice(0)) closeSync(fd);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
for (const fd of descriptors) closeSync(fd);
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function assertNoPermissionOverrides(args, field = "claudeArgs", ceiling = "standard") {
|
|
204
|
+
if (ceiling !== "read-only") return;
|
|
205
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
206
|
+
const arg = args[index];
|
|
207
|
+
const option = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg;
|
|
208
|
+
if (PERMISSION_OVERRIDE_OPTIONS.has(option)) {
|
|
209
|
+
throw new Error(`${field}[${index}] cannot override the validated Claude permission policy`);
|
|
210
|
+
}
|
|
211
|
+
if (ceiling === "read-only" && permissionCapabilityOption(arg) !== void 0) {
|
|
212
|
+
throw new Error(`${field}[${index}] cannot add a permission capability to a hardened Claude role`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// claude/cli-runner.ts
|
|
218
|
+
function prepareClaudeLaunch(options) {
|
|
219
|
+
const args = ["-p", "--output-format", "json"];
|
|
220
|
+
const permission = resolveClaudePermissionPolicy({
|
|
221
|
+
permissionMode: options.permissionMode,
|
|
222
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
223
|
+
ceiling: options.permissionCeiling
|
|
224
|
+
});
|
|
225
|
+
assertNoPermissionOverrides(options.extraArgs ?? [], "extraArgs", permission.ceiling);
|
|
226
|
+
let cwd = options.cwd;
|
|
227
|
+
let addDirs = options.addDirs ?? [];
|
|
228
|
+
let release = () => {
|
|
229
|
+
};
|
|
230
|
+
if (permission.ceiling === "read-only") {
|
|
231
|
+
if (options.mcpConfig !== void 0) {
|
|
232
|
+
throw new Error("mcpConfig cannot add an arbitrary MCP capability to a hardened Claude role");
|
|
233
|
+
}
|
|
234
|
+
const binding = bindHardenedClaudePaths(options.cwd, addDirs);
|
|
235
|
+
cwd = binding.cwd;
|
|
236
|
+
addDirs = binding.addDirs;
|
|
237
|
+
release = binding.release;
|
|
238
|
+
args.push("--bare");
|
|
239
|
+
}
|
|
240
|
+
if (options.sessionId) {
|
|
241
|
+
args.push("--resume", options.sessionId);
|
|
242
|
+
}
|
|
243
|
+
if (options.model) {
|
|
244
|
+
args.push("--model", options.model);
|
|
245
|
+
}
|
|
246
|
+
if (options.appendSystemPrompt) {
|
|
247
|
+
args.push("--append-system-prompt", options.appendSystemPrompt);
|
|
248
|
+
}
|
|
249
|
+
if (permission.dangerouslySkipPermissions) {
|
|
250
|
+
args.push("--dangerously-skip-permissions");
|
|
251
|
+
} else if (permission.permissionMode && (permission.ceiling === "read-only" || options.permissionMode !== void 0)) {
|
|
252
|
+
args.push("--permission-mode", permission.permissionMode);
|
|
253
|
+
}
|
|
254
|
+
for (const dir of addDirs) {
|
|
255
|
+
args.push("--add-dir", dir);
|
|
256
|
+
}
|
|
257
|
+
if (options.mcpConfig) {
|
|
258
|
+
args.push("--mcp-config", options.mcpConfig);
|
|
259
|
+
}
|
|
260
|
+
if (options.extraArgs?.length) {
|
|
261
|
+
args.push(...options.extraArgs);
|
|
262
|
+
}
|
|
263
|
+
return { args, cwd, release };
|
|
264
|
+
}
|
|
265
|
+
function isRecord(value) {
|
|
266
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
267
|
+
}
|
|
268
|
+
function runClaudeTurn(options) {
|
|
269
|
+
return new Promise((resolve5, reject) => {
|
|
270
|
+
const command = options.claudeCommand ?? "claude";
|
|
271
|
+
const launch = prepareClaudeLaunch(options);
|
|
272
|
+
let child;
|
|
273
|
+
try {
|
|
274
|
+
child = spawn(command, launch.args, {
|
|
275
|
+
cwd: launch.cwd,
|
|
276
|
+
env: { ...process.env, ...options.env },
|
|
277
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
launch.release();
|
|
281
|
+
}
|
|
282
|
+
let stdout = "";
|
|
283
|
+
let stderr = "";
|
|
284
|
+
let settled = false;
|
|
285
|
+
let timeoutHandle;
|
|
286
|
+
const cleanup = () => {
|
|
287
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
288
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
289
|
+
};
|
|
290
|
+
const finishReject = (error) => {
|
|
291
|
+
if (settled) return;
|
|
292
|
+
settled = true;
|
|
293
|
+
cleanup();
|
|
294
|
+
reject(error);
|
|
295
|
+
};
|
|
296
|
+
const finishResolve = (result) => {
|
|
297
|
+
if (settled) return;
|
|
298
|
+
settled = true;
|
|
299
|
+
cleanup();
|
|
300
|
+
resolve5(result);
|
|
301
|
+
};
|
|
302
|
+
const onAbort = () => {
|
|
303
|
+
child.kill("SIGTERM");
|
|
304
|
+
finishReject(new Error("Claude turn aborted"));
|
|
305
|
+
};
|
|
306
|
+
if (options.signal) {
|
|
307
|
+
if (options.signal.aborted) {
|
|
308
|
+
child.kill("SIGTERM");
|
|
309
|
+
finishReject(new Error("Claude turn aborted"));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
313
|
+
}
|
|
314
|
+
if (options.timeoutMs) {
|
|
315
|
+
timeoutHandle = setTimeout(() => {
|
|
316
|
+
child.kill("SIGTERM");
|
|
317
|
+
finishReject(new Error(`Claude turn timed out after ${options.timeoutMs}ms`));
|
|
318
|
+
}, options.timeoutMs);
|
|
319
|
+
}
|
|
320
|
+
child.on("error", (error) => {
|
|
321
|
+
finishReject(new Error(`Failed to spawn "${command}": ${error.message}`, { cause: error }));
|
|
322
|
+
});
|
|
323
|
+
child.stdout?.on("data", (chunk) => {
|
|
324
|
+
stdout += chunk.toString("utf8");
|
|
325
|
+
});
|
|
326
|
+
child.stderr?.on("data", (chunk) => {
|
|
327
|
+
stderr += chunk.toString("utf8");
|
|
328
|
+
});
|
|
329
|
+
child.on("close", (exitCode) => {
|
|
330
|
+
if (settled) return;
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(stdout.trim());
|
|
334
|
+
} catch {
|
|
335
|
+
const fallback = stdout.trim() || stderr.trim();
|
|
336
|
+
finishResolve({
|
|
337
|
+
sessionId: options.sessionId ?? null,
|
|
338
|
+
result: fallback,
|
|
339
|
+
isError: exitCode !== 0,
|
|
340
|
+
raw: stdout
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (isRecord(parsed) && typeof parsed.result === "string") {
|
|
345
|
+
finishResolve({
|
|
346
|
+
sessionId: typeof parsed.session_id === "string" ? parsed.session_id : null,
|
|
347
|
+
result: parsed.result,
|
|
348
|
+
isError: Boolean(parsed.is_error),
|
|
349
|
+
raw: parsed
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
finishResolve({
|
|
354
|
+
sessionId: options.sessionId ?? null,
|
|
355
|
+
result: stdout.trim(),
|
|
356
|
+
isError: exitCode !== 0,
|
|
357
|
+
raw: parsed
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
child.stdin?.write(options.prompt);
|
|
361
|
+
child.stdin?.end();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// claude/worker-config.ts
|
|
366
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
367
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
368
|
+
import { cwd as processCwd } from "node:process";
|
|
369
|
+
|
|
370
|
+
// broker/paths.ts
|
|
371
|
+
import { chmodSync, mkdirSync, readFileSync } from "fs";
|
|
372
|
+
import { isAbsolute as isAbsolute2, join as join2, resolve } from "path";
|
|
373
|
+
import { homedir } from "os";
|
|
374
|
+
var INTERCOM_DIR_MODE = 448;
|
|
375
|
+
var INTERCOM_RUNTIME_FILE_MODE = 384;
|
|
376
|
+
var INTERCOM_TCP_HOST = "127.0.0.1";
|
|
377
|
+
var INTERCOM_PROTOCOL_NAME = "pi-intercom";
|
|
378
|
+
var INTERCOM_PROTOCOL_VERSION = 4;
|
|
379
|
+
function sanitizePipeSegment(value) {
|
|
380
|
+
return value.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "default";
|
|
381
|
+
}
|
|
382
|
+
function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
|
|
383
|
+
const configured = env.PI_CODING_AGENT_DIR?.trim();
|
|
384
|
+
if (!configured) {
|
|
385
|
+
return join2(homeDir, ".pi/agent");
|
|
386
|
+
}
|
|
387
|
+
return isAbsolute2(configured) ? configured : resolve(cwd, configured);
|
|
388
|
+
}
|
|
389
|
+
function getIntercomDirPath(agentDir = getAgentDirPath()) {
|
|
390
|
+
return join2(agentDir, "intercom");
|
|
391
|
+
}
|
|
392
|
+
function shouldUseWindowsTcpTransport(platform = process.platform, env = process.env) {
|
|
393
|
+
if (platform !== "win32") {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const transport = env.PI_INTERCOM_TRANSPORT?.trim().toLowerCase();
|
|
397
|
+
if (transport === "tcp") {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
const legacyOptIn = env.PI_INTERCOM_TCP?.trim().toLowerCase();
|
|
401
|
+
return legacyOptIn === "1" || legacyOptIn === "true";
|
|
402
|
+
}
|
|
403
|
+
function getBrokerPortFilePath(intercomDir = getIntercomDirPath()) {
|
|
404
|
+
return join2(intercomDir, "broker.port.json");
|
|
405
|
+
}
|
|
406
|
+
function getBrokerSocketPath(platform = process.platform, agentDir = getAgentDirPath()) {
|
|
407
|
+
if (platform === "win32") {
|
|
408
|
+
return `\\\\.\\pipe\\pi-intercom-${sanitizePipeSegment(agentDir)}`;
|
|
409
|
+
}
|
|
410
|
+
return join2(getIntercomDirPath(agentDir), "broker.sock");
|
|
411
|
+
}
|
|
412
|
+
function getBrokerConnectTarget(platform = process.platform, env = process.env, intercomDir = getIntercomDirPath(getAgentDirPath(env))) {
|
|
413
|
+
if (shouldUseWindowsTcpTransport(platform, env)) {
|
|
414
|
+
const endpointFile = getBrokerPortFilePath(intercomDir);
|
|
415
|
+
const raw = readFileSync(endpointFile, "utf-8");
|
|
416
|
+
const parsed = JSON.parse(raw);
|
|
417
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
418
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}: expected a JSON object`);
|
|
419
|
+
}
|
|
420
|
+
const endpoint = parsed;
|
|
421
|
+
if (endpoint.transport !== "tcp" || endpoint.host !== INTERCOM_TCP_HOST || typeof endpoint.port !== "number" || !Number.isSafeInteger(endpoint.port) || endpoint.port <= 0 || endpoint.port > 65535 || typeof endpoint.stateId !== "string" || endpoint.stateId.length === 0) {
|
|
422
|
+
throw new Error(`Invalid intercom TCP endpoint at ${endpointFile}`);
|
|
423
|
+
}
|
|
424
|
+
return { transport: "tcp", host: endpoint.host, port: endpoint.port, stateId: endpoint.stateId };
|
|
425
|
+
}
|
|
426
|
+
return getBrokerSocketPath(platform, getAgentDirPath(env));
|
|
427
|
+
}
|
|
428
|
+
function ensureIntercomRuntimeDir(intercomDir = getIntercomDirPath(), platform = process.platform) {
|
|
429
|
+
mkdirSync(intercomDir, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
430
|
+
if (platform !== "win32") {
|
|
431
|
+
chmodSync(intercomDir, INTERCOM_DIR_MODE);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function restrictIntercomRuntimeFile(filePath, platform = process.platform) {
|
|
435
|
+
if (platform !== "win32") {
|
|
436
|
+
chmodSync(filePath, INTERCOM_RUNTIME_FILE_MODE);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// broker/validation.ts
|
|
441
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
442
|
+
function isPlainDataRecord(value) {
|
|
443
|
+
if (typeof value !== "object" || value === null || nodeUtilTypes.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
448
|
+
if (typeof key !== "string") return false;
|
|
449
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
450
|
+
return descriptor !== void 0 && descriptor.enumerable && Object.hasOwn(descriptor, "value");
|
|
451
|
+
});
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function hasExactDataKeys(value, required, optional = []) {
|
|
457
|
+
if (!isPlainDataRecord(value)) return false;
|
|
458
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
459
|
+
const keys = Reflect.ownKeys(value);
|
|
460
|
+
return keys.length >= required.length && keys.every((key) => typeof key === "string" && allowed.has(key)) && required.every((key) => Object.hasOwn(value, key));
|
|
461
|
+
}
|
|
462
|
+
function assertPlainDataGraph(value, path = "$", seen = /* @__PURE__ */ new WeakSet()) {
|
|
463
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) return;
|
|
464
|
+
if (nodeUtilTypes.isProxy(value)) {
|
|
465
|
+
throw new Error(`${path} must not contain a Proxy`);
|
|
466
|
+
}
|
|
467
|
+
if (seen.has(value)) {
|
|
468
|
+
throw new Error(`${path} must not contain a cycle or repeated object reference`);
|
|
469
|
+
}
|
|
470
|
+
seen.add(value);
|
|
471
|
+
if (Array.isArray(value)) {
|
|
472
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
473
|
+
throw new Error(`${path} must use the exact Array prototype`);
|
|
474
|
+
}
|
|
475
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
476
|
+
if (lengthDescriptor === void 0 || !Object.hasOwn(lengthDescriptor, "value")) {
|
|
477
|
+
throw new Error(`${path}.length must be an own data property`);
|
|
478
|
+
}
|
|
479
|
+
const length = lengthDescriptor.value;
|
|
480
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
482
|
+
if (key === "length") continue;
|
|
483
|
+
if (typeof key !== "string") throw new Error(`${path} must not contain symbol properties`);
|
|
484
|
+
const index = Number(key);
|
|
485
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) {
|
|
486
|
+
throw new Error(`${path} must not contain non-index properties`);
|
|
487
|
+
}
|
|
488
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
489
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
490
|
+
throw new Error(`${path}[${index}] must be an enumerable own data property`);
|
|
491
|
+
}
|
|
492
|
+
descriptors.set(index, descriptor);
|
|
493
|
+
}
|
|
494
|
+
if (descriptors.size !== length) throw new Error(`${path} must be dense`);
|
|
495
|
+
for (let index = 0; index < length; index += 1) {
|
|
496
|
+
const descriptor = descriptors.get(index);
|
|
497
|
+
if (descriptor === void 0) throw new Error(`${path} must be dense`);
|
|
498
|
+
assertPlainDataGraph(descriptor.value, `${path}[${index}]`, seen);
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
503
|
+
throw new Error(`${path} must be a plain data object`);
|
|
504
|
+
}
|
|
505
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
506
|
+
if (typeof key !== "string") throw new Error(`${path} must not contain symbol properties`);
|
|
507
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
508
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) {
|
|
509
|
+
throw new Error(`${path}.${key} must be an enumerable own data property`);
|
|
510
|
+
}
|
|
511
|
+
assertPlainDataGraph(descriptor.value, `${path}.${key}`, seen);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function isDenseArrayOf(value, predicate) {
|
|
515
|
+
if (typeof value !== "object" || value === null || nodeUtilTypes.isProxy(value) || !Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) return false;
|
|
516
|
+
try {
|
|
517
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
518
|
+
if (lengthDescriptor === void 0 || !Object.hasOwn(lengthDescriptor, "value")) return false;
|
|
519
|
+
const length = lengthDescriptor.value;
|
|
520
|
+
const entries = /* @__PURE__ */ new Map();
|
|
521
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
522
|
+
if (key === "length") continue;
|
|
523
|
+
if (typeof key !== "string") return false;
|
|
524
|
+
const index = Number(key);
|
|
525
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== key) return false;
|
|
526
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
527
|
+
if (descriptor === void 0 || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return false;
|
|
528
|
+
entries.set(index, descriptor.value);
|
|
529
|
+
}
|
|
530
|
+
if (entries.size !== length) return false;
|
|
531
|
+
for (let index = 0; index < length; index += 1) {
|
|
532
|
+
if (!predicate(entries.get(index), index)) return false;
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// claude/transport.ts
|
|
541
|
+
import { spawnSync } from "node:child_process";
|
|
542
|
+
var CLAUDE_INTERCOM_TRANSPORT_ENV = "CLAUDE_INTERCOM_TRANSPORT";
|
|
543
|
+
var MIN_NATIVE_CLAUDE_VERSION = "2.1.220";
|
|
544
|
+
var MAX_NATIVE_CLAUDE_VERSION = "2.1.226";
|
|
545
|
+
function parseClaudeIntercomTransport(value, source = "transport") {
|
|
546
|
+
if (value === void 0 || value === null || value === "") return "auto";
|
|
547
|
+
if (value === "auto" || value === "native" || value === "mcp") return value;
|
|
548
|
+
throw new Error(`${source} must be one of auto, native, or mcp`);
|
|
549
|
+
}
|
|
550
|
+
function parseClaudeVersion(output) {
|
|
551
|
+
const match = output.match(/(?:^|\s)(\d+\.\d+\.\d+)(?:\s|$)/);
|
|
552
|
+
return match?.[1] ?? null;
|
|
553
|
+
}
|
|
554
|
+
function compareVersions(left, right) {
|
|
555
|
+
const a = left.split(".").map(Number);
|
|
556
|
+
const b = right.split(".").map(Number);
|
|
557
|
+
for (let index = 0; index < 3; index += 1) {
|
|
558
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
559
|
+
if (difference !== 0) return difference;
|
|
560
|
+
}
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
function isNativeClaudeVersionCompatible(version) {
|
|
564
|
+
return compareVersions(version, MIN_NATIVE_CLAUDE_VERSION) >= 0 && compareVersions(version, MAX_NATIVE_CLAUDE_VERSION) <= 0;
|
|
565
|
+
}
|
|
566
|
+
function probeClaudeVersion(command = "claude") {
|
|
567
|
+
const result = spawnSync(command, ["--version"], {
|
|
568
|
+
encoding: "utf8",
|
|
569
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
570
|
+
});
|
|
571
|
+
if (result.error || result.status !== 0) {
|
|
572
|
+
return {
|
|
573
|
+
command,
|
|
574
|
+
version: null,
|
|
575
|
+
compatible: false,
|
|
576
|
+
reason: `could not run ${command} --version`
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const version = parseClaudeVersion(`${result.stdout}
|
|
580
|
+
${result.stderr}`);
|
|
581
|
+
if (!version) {
|
|
582
|
+
return { command, version: null, compatible: false, reason: "Claude version output was not recognized" };
|
|
583
|
+
}
|
|
584
|
+
const compatible = isNativeClaudeVersionCompatible(version);
|
|
585
|
+
return {
|
|
586
|
+
command,
|
|
587
|
+
version,
|
|
588
|
+
compatible,
|
|
589
|
+
reason: compatible ? `Claude ${version} is within the native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}` : `Claude ${version} is outside the verified native transport compatibility window ${MIN_NATIVE_CLAUDE_VERSION}-${MAX_NATIVE_CLAUDE_VERSION}`
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function resolveClaudeIntercomTransport(options = {}) {
|
|
593
|
+
const requested = parseClaudeIntercomTransport(
|
|
594
|
+
options.requested ?? options.env?.[CLAUDE_INTERCOM_TRANSPORT_ENV] ?? "auto",
|
|
595
|
+
options.requested === void 0 ? CLAUDE_INTERCOM_TRANSPORT_ENV : "transport"
|
|
596
|
+
);
|
|
597
|
+
const command = options.claudeCommand || "claude";
|
|
598
|
+
const versionProbe = (options.probe ?? probeClaudeVersion)(command);
|
|
599
|
+
if (requested === "mcp") return { requested, selected: "mcp", ...versionProbe };
|
|
600
|
+
if (requested === "native" && !versionProbe.compatible) {
|
|
601
|
+
throw new Error(`Native Claude intercom transport was requested, but ${versionProbe.reason}. Use --transport mcp or install a verified-compatible Claude version.`);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
requested,
|
|
605
|
+
selected: requested === "native" || versionProbe.compatible ? "native" : "mcp",
|
|
606
|
+
...versionProbe
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// claude/worker-config.ts
|
|
611
|
+
var DEFAULT_WORKER_CONFIG_PATH = join3(getIntercomDirPath(), "claude-worker.json");
|
|
612
|
+
var DEFAULT_WORKER_STATE_PATH = join3(getIntercomDirPath(), "claude-worker-state.json");
|
|
613
|
+
function isRecord2(value) {
|
|
614
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
615
|
+
}
|
|
616
|
+
function optionalString(value, field) {
|
|
617
|
+
if (value === void 0 || value === null) return void 0;
|
|
618
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
619
|
+
const trimmed = value.trim();
|
|
620
|
+
return trimmed || void 0;
|
|
621
|
+
}
|
|
622
|
+
function requireString(value, field) {
|
|
623
|
+
const result = optionalString(value, field);
|
|
624
|
+
if (!result) throw new Error(`${field} must be a non-empty string`);
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
function optionalBoolean(value, field) {
|
|
628
|
+
if (value === void 0 || value === null) return void 0;
|
|
629
|
+
if (typeof value !== "boolean") throw new Error(`${field} must be a boolean`);
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
function optionalStringArray(value, field) {
|
|
633
|
+
if (value === void 0 || value === null) return void 0;
|
|
634
|
+
if (!isDenseArrayOf(value, (_entry) => true)) {
|
|
635
|
+
throw new Error(`${field} must be an exact dense array`);
|
|
636
|
+
}
|
|
637
|
+
return value.map((item, index) => requireString(item, `${field}[${index}]`));
|
|
638
|
+
}
|
|
639
|
+
function normalizeAgent(raw, index) {
|
|
640
|
+
if (!isRecord2(raw)) throw new Error(`agents[${index}] must be an object`);
|
|
641
|
+
const id = requireString(raw.id, `agents[${index}].id`);
|
|
642
|
+
const name = optionalString(raw.name, `agents[${index}].name`) ?? id;
|
|
643
|
+
const cwdValue = optionalString(raw.cwd, `agents[${index}].cwd`) ?? processCwd();
|
|
644
|
+
let cwd = resolve2(cwdValue);
|
|
645
|
+
const permissionModeValue = optionalString(raw.permissionMode, `agents[${index}].permissionMode`);
|
|
646
|
+
const dangerouslySkipPermissionsValue = optionalBoolean(
|
|
647
|
+
raw.dangerouslySkipPermissions,
|
|
648
|
+
`agents[${index}].dangerouslySkipPermissions`
|
|
649
|
+
);
|
|
650
|
+
const permissionCeilingValue = optionalString(raw.permissionCeiling, `agents[${index}].permissionCeiling`);
|
|
651
|
+
if (permissionCeilingValue !== void 0 && permissionCeilingValue !== "standard" && permissionCeilingValue !== "read-only") {
|
|
652
|
+
throw new Error(`agents[${index}].permissionCeiling must be standard or read-only`);
|
|
653
|
+
}
|
|
654
|
+
const bossRoleValue = optionalString(raw.bossRole, `agents[${index}].bossRole`);
|
|
655
|
+
if (bossRoleValue !== void 0 && bossRoleValue !== "adversary" && bossRoleValue !== "council") {
|
|
656
|
+
throw new Error(`agents[${index}].bossRole must be adversary or council`);
|
|
657
|
+
}
|
|
658
|
+
if (bossRoleValue !== void 0 && permissionCeilingValue === "standard") {
|
|
659
|
+
throw new Error(`agents[${index}].permissionCeiling cannot widen an Adversary/Council role`);
|
|
660
|
+
}
|
|
661
|
+
const permission = resolveClaudePermissionPolicy({
|
|
662
|
+
permissionMode: permissionModeValue,
|
|
663
|
+
dangerouslySkipPermissions: dangerouslySkipPermissionsValue,
|
|
664
|
+
ceiling: bossRoleValue === void 0 ? permissionCeilingValue : "read-only"
|
|
665
|
+
});
|
|
666
|
+
const claudeArgs = optionalStringArray(raw.claudeArgs, `agents[${index}].claudeArgs`);
|
|
667
|
+
let addDirs = optionalStringArray(raw.addDirs, `agents[${index}].addDirs`);
|
|
668
|
+
const mcpConfig = optionalString(raw.mcpConfig, `agents[${index}].mcpConfig`);
|
|
669
|
+
assertNoPermissionOverrides(claudeArgs ?? [], `agents[${index}].claudeArgs`, permission.ceiling);
|
|
670
|
+
if (permission.ceiling === "read-only") {
|
|
671
|
+
if (mcpConfig !== void 0) {
|
|
672
|
+
throw new Error(`agents[${index}].mcpConfig cannot add an arbitrary MCP capability to a hardened Claude role`);
|
|
673
|
+
}
|
|
674
|
+
const binding = bindHardenedClaudePaths(cwdValue, addDirs ?? [], `agents[${index}]`);
|
|
675
|
+
try {
|
|
676
|
+
cwd = binding.cwd;
|
|
677
|
+
addDirs = binding.addDirs.length ? binding.addDirs : void 0;
|
|
678
|
+
} finally {
|
|
679
|
+
binding.release();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
id,
|
|
684
|
+
name,
|
|
685
|
+
cwd,
|
|
686
|
+
model: optionalString(raw.model, `agents[${index}].model`),
|
|
687
|
+
sessionId: optionalString(raw.sessionId, `agents[${index}].sessionId`),
|
|
688
|
+
instructions: optionalString(raw.instructions, `agents[${index}].instructions`),
|
|
689
|
+
...permission.ceiling === "standard" && permissionModeValue === void 0 ? {} : { permissionMode: permission.permissionMode },
|
|
690
|
+
...dangerouslySkipPermissionsValue === void 0 && permission.ceiling !== "read-only" ? {} : { dangerouslySkipPermissions: permission.dangerouslySkipPermissions },
|
|
691
|
+
...permissionCeilingValue === void 0 && bossRoleValue === void 0 ? {} : { permissionCeiling: permission.ceiling },
|
|
692
|
+
...bossRoleValue === void 0 ? {} : { bossRole: bossRoleValue },
|
|
693
|
+
addDirs,
|
|
694
|
+
mcpConfig,
|
|
695
|
+
claudeArgs,
|
|
696
|
+
transport: raw.transport === void 0 ? void 0 : parseClaudeIntercomTransport(raw.transport, `agents[${index}].transport`)
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function defaultWorkerConfig(env = process.env) {
|
|
700
|
+
const id = env.CLAUDE_INTERCOM_WORKER_ID?.trim() || "claude-worker";
|
|
701
|
+
return {
|
|
702
|
+
statePath: env.CLAUDE_INTERCOM_WORKER_STATE?.trim() || DEFAULT_WORKER_STATE_PATH,
|
|
703
|
+
agents: [{
|
|
704
|
+
id,
|
|
705
|
+
name: env.CLAUDE_INTERCOM_WORKER_NAME?.trim() || id,
|
|
706
|
+
cwd: resolve2(env.CLAUDE_INTERCOM_WORKER_CWD?.trim() || processCwd()),
|
|
707
|
+
model: env.CLAUDE_INTERCOM_WORKER_MODEL?.trim() || void 0,
|
|
708
|
+
instructions: env.CLAUDE_INTERCOM_WORKER_INSTRUCTIONS?.trim() || void 0,
|
|
709
|
+
transport: parseClaudeIntercomTransport(env.CLAUDE_INTERCOM_TRANSPORT, "CLAUDE_INTERCOM_TRANSPORT")
|
|
710
|
+
}]
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function loadWorkerConfig(path = process.env.CLAUDE_INTERCOM_WORKER_CONFIG || DEFAULT_WORKER_CONFIG_PATH) {
|
|
714
|
+
if (!existsSync(path)) return defaultWorkerConfig();
|
|
715
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
716
|
+
if (!isRecord2(parsed)) throw new Error("Worker config must be a JSON object");
|
|
717
|
+
if (!isDenseArrayOf(parsed.agents, (_entry) => true)) {
|
|
718
|
+
throw new Error("Worker config requires an exact dense agents array");
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
statePath: resolve2(optionalString(parsed.statePath, "statePath") ?? DEFAULT_WORKER_STATE_PATH),
|
|
722
|
+
claudeCommand: optionalString(parsed.claudeCommand, "claudeCommand"),
|
|
723
|
+
agents: parsed.agents.map(normalizeAgent)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function loadWorkerState(path) {
|
|
727
|
+
if (!existsSync(path)) return { agents: {} };
|
|
728
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
729
|
+
if (!isRecord2(parsed) || !isRecord2(parsed.agents)) return { agents: {} };
|
|
730
|
+
const agents = {};
|
|
731
|
+
for (const [id, value] of Object.entries(parsed.agents)) {
|
|
732
|
+
if (!isRecord2(value) || typeof value.sessionId !== "string") continue;
|
|
733
|
+
agents[id] = {
|
|
734
|
+
sessionId: value.sessionId,
|
|
735
|
+
updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return { agents };
|
|
739
|
+
}
|
|
740
|
+
function saveWorkerState(path, state) {
|
|
741
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
742
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}
|
|
743
|
+
`, { mode: 384 });
|
|
744
|
+
restrictIntercomRuntimeFile(path);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// broker/client.ts
|
|
748
|
+
import { EventEmitter } from "events";
|
|
749
|
+
import net from "net";
|
|
750
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
751
|
+
import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
|
|
752
|
+
|
|
753
|
+
// broker/framing.ts
|
|
754
|
+
var MAX_FRAME_BYTES = 1024 * 1024;
|
|
755
|
+
function writeMessage(socket, msg) {
|
|
756
|
+
const json = JSON.stringify(msg);
|
|
757
|
+
const payload = Buffer.from(json, "utf-8");
|
|
758
|
+
const header = Buffer.alloc(4);
|
|
759
|
+
header.writeUInt32BE(payload.length, 0);
|
|
760
|
+
socket.write(Buffer.concat([header, payload]));
|
|
761
|
+
}
|
|
762
|
+
function createMessageReader(onMessage, onError, maxFrameBytes = MAX_FRAME_BYTES) {
|
|
763
|
+
let buffer = Buffer.alloc(0);
|
|
764
|
+
function reportMessage(payload) {
|
|
765
|
+
let msg;
|
|
766
|
+
try {
|
|
767
|
+
msg = JSON.parse(payload.toString("utf-8"));
|
|
768
|
+
} catch (error) {
|
|
769
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
770
|
+
onError(new Error(`Failed to parse intercom message: ${message}`, { cause: error }));
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
onMessage(msg);
|
|
775
|
+
return true;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
778
|
+
onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return (data) => {
|
|
783
|
+
let remaining = data;
|
|
784
|
+
while (remaining.length > 0) {
|
|
785
|
+
if (buffer.length < 4) {
|
|
786
|
+
const headerBytes = Math.min(4 - buffer.length, remaining.length);
|
|
787
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, headerBytes)]);
|
|
788
|
+
remaining = remaining.subarray(headerBytes);
|
|
789
|
+
if (buffer.length < 4) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const length = buffer.readUInt32BE(0);
|
|
794
|
+
if (length > maxFrameBytes) {
|
|
795
|
+
buffer = Buffer.alloc(0);
|
|
796
|
+
onError(new Error(`Intercom frame length ${length} exceeds maximum ${maxFrameBytes} bytes`));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const missingPayloadBytes = length - Math.max(0, buffer.length - 4);
|
|
800
|
+
const payloadBytes = Math.min(missingPayloadBytes, remaining.length);
|
|
801
|
+
if (payloadBytes > 0) {
|
|
802
|
+
buffer = Buffer.concat([buffer, remaining.subarray(0, payloadBytes)]);
|
|
803
|
+
remaining = remaining.subarray(payloadBytes);
|
|
804
|
+
}
|
|
805
|
+
if (buffer.length < 4 + length) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const payload = buffer.subarray(4, 4 + length);
|
|
809
|
+
buffer = Buffer.alloc(0);
|
|
810
|
+
if (!reportMessage(payload)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// outbound-outbox.ts
|
|
818
|
+
import { createHash } from "crypto";
|
|
819
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2 } from "fs";
|
|
820
|
+
import { join as join4 } from "path";
|
|
821
|
+
|
|
822
|
+
// durable-json.ts
|
|
823
|
+
import { randomUUID } from "crypto";
|
|
824
|
+
import { closeSync as closeSync2, fsyncSync, openSync as openSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
825
|
+
import { dirname as dirname3 } from "path";
|
|
826
|
+
function writeDurableJson(filePath, value) {
|
|
827
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
828
|
+
writeFileSync2(temporaryPath, JSON.stringify(value), { encoding: "utf-8", mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
829
|
+
const fileDescriptor = openSync2(temporaryPath, "r");
|
|
830
|
+
try {
|
|
831
|
+
fsyncSync(fileDescriptor);
|
|
832
|
+
} finally {
|
|
833
|
+
closeSync2(fileDescriptor);
|
|
834
|
+
}
|
|
835
|
+
renameSync(temporaryPath, filePath);
|
|
836
|
+
restrictIntercomRuntimeFile(filePath);
|
|
837
|
+
if (process.platform !== "win32") {
|
|
838
|
+
const directoryDescriptor = openSync2(dirname3(filePath), "r");
|
|
839
|
+
try {
|
|
840
|
+
fsyncSync(directoryDescriptor);
|
|
841
|
+
} finally {
|
|
842
|
+
closeSync2(directoryDescriptor);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// outbound-outbox.ts
|
|
848
|
+
var OUTBOX_STATE_VERSION = 1;
|
|
849
|
+
var MAX_OUTBOX_MESSAGES = 256;
|
|
850
|
+
function fingerprint(entry) {
|
|
851
|
+
return JSON.stringify({
|
|
852
|
+
to: entry.to,
|
|
853
|
+
replyTo: entry.message.replyTo,
|
|
854
|
+
expectsReply: entry.message.expectsReply,
|
|
855
|
+
content: entry.message.content
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function isStoredOutboundMessage(value) {
|
|
859
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
860
|
+
const entry = value;
|
|
861
|
+
if (typeof entry.to !== "string" || typeof entry.queuedAt !== "number") return false;
|
|
862
|
+
if (typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) return false;
|
|
863
|
+
const message = entry.message;
|
|
864
|
+
return typeof message.id === "string" && typeof message.timestamp === "number" && typeof message.content === "object" && message.content !== null && typeof message.content.text === "string";
|
|
865
|
+
}
|
|
866
|
+
function fileName(sessionId) {
|
|
867
|
+
return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
|
|
868
|
+
}
|
|
869
|
+
var PersistentOutboundOutbox = class {
|
|
870
|
+
directory;
|
|
871
|
+
filePath;
|
|
872
|
+
state;
|
|
873
|
+
constructor(sessionId, intercomDir = getIntercomDirPath()) {
|
|
874
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
875
|
+
this.directory = join4(intercomDir, "outbox");
|
|
876
|
+
mkdirSync3(this.directory, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
877
|
+
if (process.platform !== "win32") chmodSync2(this.directory, INTERCOM_DIR_MODE);
|
|
878
|
+
this.filePath = join4(this.directory, fileName(sessionId));
|
|
879
|
+
this.state = this.load();
|
|
880
|
+
}
|
|
881
|
+
list() {
|
|
882
|
+
return this.state.entries.map((entry) => ({ ...entry, message: { ...entry.message, content: { ...entry.message.content } } }));
|
|
883
|
+
}
|
|
884
|
+
enqueue(to, message) {
|
|
885
|
+
const existing = this.state.entries.find((entry) => entry.message.id === message.id);
|
|
886
|
+
if (existing) {
|
|
887
|
+
if (fingerprint(existing) !== fingerprint({ to, message })) {
|
|
888
|
+
throw new Error(`Message ID ${message.id} is already queued with a different payload`);
|
|
889
|
+
}
|
|
890
|
+
return "existing";
|
|
891
|
+
}
|
|
892
|
+
if (this.state.entries.length >= MAX_OUTBOX_MESSAGES) {
|
|
893
|
+
throw new Error(`Durable outbox is full (${MAX_OUTBOX_MESSAGES} messages)`);
|
|
894
|
+
}
|
|
895
|
+
this.state.entries.push({ to, message, queuedAt: Date.now() });
|
|
896
|
+
this.persist();
|
|
897
|
+
return "added";
|
|
898
|
+
}
|
|
899
|
+
remove(messageId) {
|
|
900
|
+
const remaining = this.state.entries.filter((entry) => entry.message.id !== messageId);
|
|
901
|
+
if (remaining.length === this.state.entries.length) return;
|
|
902
|
+
this.state.entries = remaining;
|
|
903
|
+
this.persist();
|
|
904
|
+
}
|
|
905
|
+
clear() {
|
|
906
|
+
if (this.state.entries.length === 0) return;
|
|
907
|
+
this.state.entries = [];
|
|
908
|
+
this.persist();
|
|
909
|
+
}
|
|
910
|
+
load() {
|
|
911
|
+
if (!existsSync2(this.filePath)) return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
912
|
+
try {
|
|
913
|
+
const parsed = JSON.parse(readFileSync3(this.filePath, "utf-8"));
|
|
914
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
915
|
+
const state = parsed;
|
|
916
|
+
if (state.version !== OUTBOX_STATE_VERSION || !Array.isArray(state.entries) || !state.entries.every(isStoredOutboundMessage)) {
|
|
917
|
+
throw new Error("invalid outbox state");
|
|
918
|
+
}
|
|
919
|
+
return { version: OUTBOX_STATE_VERSION, entries: state.entries };
|
|
920
|
+
} catch {
|
|
921
|
+
const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
|
|
922
|
+
renameSync2(this.filePath, corruptPath);
|
|
923
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
924
|
+
return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
persist() {
|
|
928
|
+
writeDurableJson(this.filePath, this.state);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// broker/access-credential.ts
|
|
933
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
934
|
+
var ACCESS_CREDENTIAL_ENV = "AGENT_INTERCOM_ACCESS_CREDENTIAL_PATH";
|
|
935
|
+
var ACCESS_CREDENTIAL_VERSION = 1;
|
|
936
|
+
function nonEmptyString(value) {
|
|
937
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
938
|
+
}
|
|
939
|
+
function loadRemoteAccessCredential(env = process.env) {
|
|
940
|
+
const path = env[ACCESS_CREDENTIAL_ENV]?.trim();
|
|
941
|
+
if (!path) return void 0;
|
|
942
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
943
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
944
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
945
|
+
}
|
|
946
|
+
const credential = parsed;
|
|
947
|
+
if (nonEmptyString(credential.enrollmentToken)) {
|
|
948
|
+
return { path, access: { enrollmentToken: credential.enrollmentToken }, enrollment: true };
|
|
949
|
+
}
|
|
950
|
+
if (credential.version === ACCESS_CREDENTIAL_VERSION && nonEmptyString(credential.sessionCredential) && nonEmptyString(credential.sessionId) && typeof credential.generation === "number" && Number.isSafeInteger(credential.generation) && credential.generation > 0) {
|
|
951
|
+
return {
|
|
952
|
+
path,
|
|
953
|
+
access: {
|
|
954
|
+
sessionCredential: credential.sessionCredential,
|
|
955
|
+
sessionId: credential.sessionId,
|
|
956
|
+
generation: credential.generation
|
|
957
|
+
},
|
|
958
|
+
enrollment: false
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
throw new Error(`Invalid Agent Intercom access credential at ${path}`);
|
|
962
|
+
}
|
|
963
|
+
function writeRemoteSessionCredential(path, sessionId, metadata) {
|
|
964
|
+
if (!metadata.sessionCredential) {
|
|
965
|
+
throw new Error("Remote enrollment response omitted the session credential");
|
|
966
|
+
}
|
|
967
|
+
writeDurableJson(path, {
|
|
968
|
+
version: ACCESS_CREDENTIAL_VERSION,
|
|
969
|
+
sessionCredential: metadata.sessionCredential,
|
|
970
|
+
sessionId,
|
|
971
|
+
generation: metadata.generation
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// protocol-v4/contract.ts
|
|
976
|
+
import {
|
|
977
|
+
INTERCOM_PROTOCOL_NAME as INTERCOM_PROTOCOL_NAME2,
|
|
978
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
979
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
980
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
981
|
+
INTERCOM_PROTOCOL_VERSION as INTERCOM_PROTOCOL_VERSION2,
|
|
982
|
+
INTERCOM_SCOPE_ENV,
|
|
983
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
984
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
985
|
+
intercomScopeIdFromEnv,
|
|
986
|
+
parseIntercomScopeId,
|
|
987
|
+
sameIntercomScope
|
|
988
|
+
} from "@ctliz/agent-intercom-core/protocol-v4";
|
|
989
|
+
var INTERCOM_REGISTRATION_METADATA_ERROR = "Registration metadata is invalid";
|
|
990
|
+
function parseIntercomScopeIdForRegistration(value) {
|
|
991
|
+
try {
|
|
992
|
+
return parseIntercomScopeId(value, "registrationMetadata");
|
|
993
|
+
} catch {
|
|
994
|
+
throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function intercomScopeIdFromEnvForRegistration(env = process.env) {
|
|
998
|
+
try {
|
|
999
|
+
return intercomScopeIdFromEnv(env);
|
|
1000
|
+
} catch {
|
|
1001
|
+
throw new Error(INTERCOM_REGISTRATION_METADATA_ERROR);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// types.ts
|
|
1006
|
+
var DELIVERY_FAILURE_CODES = [
|
|
1007
|
+
"INVALID_MESSAGE",
|
|
1008
|
+
"SESSION_NOT_FOUND",
|
|
1009
|
+
"AMBIGUOUS_TARGET",
|
|
1010
|
+
"SENDER_NOT_FOUND",
|
|
1011
|
+
"INVALID_REPLY_TARGET",
|
|
1012
|
+
"MUTUAL_ASK",
|
|
1013
|
+
"ASK_ALREADY_PENDING",
|
|
1014
|
+
"DUPLICATE_MESSAGE_ID",
|
|
1015
|
+
"CONFLICTING_MESSAGE_ID",
|
|
1016
|
+
"TOO_MANY_PENDING_DELIVERIES",
|
|
1017
|
+
"TOO_MANY_PENDING_ASKS",
|
|
1018
|
+
"RECIPIENT_DISCONNECTED",
|
|
1019
|
+
"SENDER_DISCONNECTED",
|
|
1020
|
+
"DELIVERY_TIMEOUT",
|
|
1021
|
+
"CONTROL_DISPATCH_UNAVAILABLE"
|
|
1022
|
+
];
|
|
1023
|
+
|
|
1024
|
+
// broker/boss-contracts.ts
|
|
1025
|
+
import {
|
|
1026
|
+
BOSS_CAPABILITY_FEATURE_DIGEST,
|
|
1027
|
+
BOSS_POLICY_SEMANTICS_HASH,
|
|
1028
|
+
BOSS_RUN_FEATURE,
|
|
1029
|
+
BOSS_RUN_FEATURE_CONTRACT,
|
|
1030
|
+
INTERCOM_BASE_PROTOCOL_VERSION,
|
|
1031
|
+
evaluateBrokerCompatibility,
|
|
1032
|
+
parseBossControlEnvelope,
|
|
1033
|
+
parseBossParticipantBinding,
|
|
1034
|
+
parseBossParticipantCredentialEnvelope,
|
|
1035
|
+
parseBossPolicyPrincipal,
|
|
1036
|
+
parseBossRunFeatureContract,
|
|
1037
|
+
parseBrokerCapabilityAdvertisement,
|
|
1038
|
+
isTerminalParticipantState,
|
|
1039
|
+
parseParticipantState,
|
|
1040
|
+
parseWorkerIdentityV2
|
|
1041
|
+
} from "@ctliz/agent-intercom-core/boss";
|
|
1042
|
+
import {
|
|
1043
|
+
ContractValidationError,
|
|
1044
|
+
assertExactKeys,
|
|
1045
|
+
assertRecord,
|
|
1046
|
+
readHexDigest
|
|
1047
|
+
} from "@ctliz/agent-intercom-core/canonical";
|
|
1048
|
+
function assertCompatibleOrdinaryAdvertisement(value) {
|
|
1049
|
+
assertPlainDataGraph(value, "$.capabilities");
|
|
1050
|
+
const advertisement = parseBrokerCapabilityAdvertisement(value);
|
|
1051
|
+
const decision = evaluateBrokerCompatibility({
|
|
1052
|
+
clientKind: "ordinary",
|
|
1053
|
+
supportedBaseProtocolVersions: [INTERCOM_BASE_PROTOCOL_VERSION]
|
|
1054
|
+
}, advertisement);
|
|
1055
|
+
if (!decision.compatible || decision.mode !== "ordinary") {
|
|
1056
|
+
throw new ContractValidationError("$.capabilities", "broker does not support exact base protocol v3");
|
|
1057
|
+
}
|
|
1058
|
+
return advertisement;
|
|
1059
|
+
}
|
|
1060
|
+
function assertCompatibleBossAdvertisement(value) {
|
|
1061
|
+
assertPlainDataGraph(value, "$.capabilities");
|
|
1062
|
+
const advertisement = parseBrokerCapabilityAdvertisement(value);
|
|
1063
|
+
if (advertisement.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || !advertisement.features.some((feature) => feature.feature === BOSS_RUN_FEATURE)) {
|
|
1064
|
+
throw new ContractValidationError("$.capabilities", "broker did not negotiate the exact required Boss feature");
|
|
1065
|
+
}
|
|
1066
|
+
return advertisement;
|
|
1067
|
+
}
|
|
1068
|
+
function parseBossRegistrationRequest(value) {
|
|
1069
|
+
assertPlainDataGraph(value, "$.boss");
|
|
1070
|
+
assertRecord(value, "$.boss");
|
|
1071
|
+
assertExactKeys(value, ["featureContract", "capabilityDigest", "credential"], [], "$.boss");
|
|
1072
|
+
const featureContract = parseBossRunFeatureContract(value.featureContract);
|
|
1073
|
+
const capabilityDigest = readHexDigest(value.capabilityDigest, "$.boss.capabilityDigest");
|
|
1074
|
+
if (featureContract.feature !== BOSS_RUN_FEATURE_CONTRACT.feature || featureContract.version !== BOSS_RUN_FEATURE_CONTRACT.version || featureContract.baseProtocolVersion !== INTERCOM_BASE_PROTOCOL_VERSION || featureContract.semanticsHash !== BOSS_RUN_FEATURE_CONTRACT.semanticsHash || featureContract.controlEnvelopeVersion !== BOSS_RUN_FEATURE_CONTRACT.controlEnvelopeVersion || capabilityDigest !== BOSS_CAPABILITY_FEATURE_DIGEST) {
|
|
1075
|
+
throw new ContractValidationError("$.boss", "Boss registration diverges from the canonical feature contract");
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
featureContract,
|
|
1079
|
+
capabilityDigest,
|
|
1080
|
+
credential: parseBossParticipantCredentialEnvelope(value.credential)
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function parseBossSessionMetadata(value, sessionId) {
|
|
1084
|
+
assertPlainDataGraph(value, "$.boss");
|
|
1085
|
+
assertRecord(value, "$.boss");
|
|
1086
|
+
assertExactKeys(value, [
|
|
1087
|
+
"binding",
|
|
1088
|
+
"principal",
|
|
1089
|
+
"liveWorker",
|
|
1090
|
+
"featureContract",
|
|
1091
|
+
"policySemanticsHash",
|
|
1092
|
+
"capabilityDigest",
|
|
1093
|
+
"brokerIdentityVerified"
|
|
1094
|
+
], [], "$.boss");
|
|
1095
|
+
const binding = parseBossParticipantBinding(value.binding);
|
|
1096
|
+
const parsedPrincipal = parseBossPolicyPrincipal(value.principal);
|
|
1097
|
+
if (parsedPrincipal.principalClass !== "boss-private") {
|
|
1098
|
+
throw new ContractValidationError("$.boss.principal", "must be a Boss-private principal");
|
|
1099
|
+
}
|
|
1100
|
+
const principal = parsedPrincipal;
|
|
1101
|
+
assertRecord(value.liveWorker, "$.boss.liveWorker");
|
|
1102
|
+
assertExactKeys(value.liveWorker, ["identity", "state"], [], "$.boss.liveWorker");
|
|
1103
|
+
const liveWorkerIdentity = parseWorkerIdentityV2(value.liveWorker.identity);
|
|
1104
|
+
const liveWorkerState = parseParticipantState(value.liveWorker.state, "$.boss.liveWorker.state");
|
|
1105
|
+
const featureContract = parseBossRunFeatureContract(value.featureContract);
|
|
1106
|
+
const policySemanticsHash = readHexDigest(value.policySemanticsHash, "$.boss.policySemanticsHash");
|
|
1107
|
+
const capabilityDigest = readHexDigest(value.capabilityDigest, "$.boss.capabilityDigest");
|
|
1108
|
+
if (value.brokerIdentityVerified !== true) {
|
|
1109
|
+
throw new ContractValidationError("$.boss.brokerIdentityVerified", "must be true for a live Boss session");
|
|
1110
|
+
}
|
|
1111
|
+
if (sessionId !== void 0 && binding.sessionId !== sessionId) {
|
|
1112
|
+
throw new ContractValidationError("$.boss.binding.sessionId", "must match the registered session ID");
|
|
1113
|
+
}
|
|
1114
|
+
if (sessionId !== void 0 && principal.principalId !== sessionId) {
|
|
1115
|
+
throw new ContractValidationError("$.boss.principal.principalId", "must match the registered session ID");
|
|
1116
|
+
}
|
|
1117
|
+
if (!("bossRunId" in liveWorkerIdentity) || sessionId !== void 0 && liveWorkerIdentity.workerId !== sessionId || liveWorkerIdentity.bossRunId !== binding.bossRunId || liveWorkerIdentity.participantId !== binding.participantId || liveWorkerIdentity.bindingEpoch !== binding.bindingEpoch || isTerminalParticipantState(liveWorkerState)) {
|
|
1118
|
+
throw new ContractValidationError(
|
|
1119
|
+
"$.boss.liveWorker",
|
|
1120
|
+
"must attest the same live Boss worker identity and binding as the registered session"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
if (binding.state !== "active" || principal.state !== "active" || binding.bossRunId !== principal.bossRunId || binding.participantId !== principal.participantId || binding.role !== principal.role || binding.bindingEpoch !== principal.bindingEpoch || binding.assignedManagerParticipantId !== principal.assignedManagerParticipantId) {
|
|
1124
|
+
throw new ContractValidationError("$.boss", "binding and policy principal are not the same active participant");
|
|
1125
|
+
}
|
|
1126
|
+
if (featureContract.semanticsHash !== BOSS_RUN_FEATURE_CONTRACT.semanticsHash || policySemanticsHash !== BOSS_POLICY_SEMANTICS_HASH || capabilityDigest !== BOSS_CAPABILITY_FEATURE_DIGEST) {
|
|
1127
|
+
throw new ContractValidationError("$.boss", "Boss session attestation diverges from Core");
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
binding,
|
|
1131
|
+
principal,
|
|
1132
|
+
liveWorker: { identity: liveWorkerIdentity, state: liveWorkerState },
|
|
1133
|
+
featureContract,
|
|
1134
|
+
policySemanticsHash,
|
|
1135
|
+
capabilityDigest,
|
|
1136
|
+
brokerIdentityVerified: true
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function assertBossRegistrationEcho(requestValue, metadataValue, sessionId) {
|
|
1140
|
+
const request = parseBossRegistrationRequest(requestValue);
|
|
1141
|
+
const metadata = parseBossSessionMetadata(metadataValue, sessionId);
|
|
1142
|
+
const credential = request.credential;
|
|
1143
|
+
const binding = metadata.binding;
|
|
1144
|
+
if (credential.bossRunId !== binding.bossRunId || credential.participantId !== binding.participantId || credential.role !== binding.role || credential.communicationProfile !== binding.communicationProfile || credential.bindingEpoch !== binding.bindingEpoch || request.featureContract.feature !== metadata.featureContract.feature || request.featureContract.version !== metadata.featureContract.version || request.featureContract.baseProtocolVersion !== metadata.featureContract.baseProtocolVersion || request.featureContract.semanticsHash !== metadata.featureContract.semanticsHash || request.featureContract.controlEnvelopeVersion !== metadata.featureContract.controlEnvelopeVersion || request.capabilityDigest !== metadata.capabilityDigest) {
|
|
1145
|
+
throw new ContractValidationError("$.boss", "broker-owned Boss registration echo does not match the requested participant binding");
|
|
1146
|
+
}
|
|
1147
|
+
return metadata;
|
|
1148
|
+
}
|
|
1149
|
+
function parseCorrelatedBossControl(value, messageId) {
|
|
1150
|
+
assertPlainDataGraph(value, "$.control");
|
|
1151
|
+
const envelope = parseBossControlEnvelope(value);
|
|
1152
|
+
if (messageId !== void 0 && envelope.messageId !== messageId) {
|
|
1153
|
+
throw new ContractValidationError("$.control.messageId", "must match the transport message ID");
|
|
1154
|
+
}
|
|
1155
|
+
return envelope;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// broker/client.ts
|
|
1159
|
+
function toError(error) {
|
|
1160
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1161
|
+
}
|
|
1162
|
+
function connectToBrokerTarget(target) {
|
|
1163
|
+
return typeof target === "string" ? net.connect(target) : net.connect({ host: target.host, port: target.port });
|
|
1164
|
+
}
|
|
1165
|
+
function isAttachment(value) {
|
|
1166
|
+
if (!hasExactDataKeys(value, ["type", "name", "content"], ["language"])) {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
const attachment = value;
|
|
1170
|
+
if (attachment.type !== "file" && attachment.type !== "snippet" && attachment.type !== "context") {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof attachment.name !== "string" || typeof attachment.content !== "string") {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
return attachment.language === void 0 || typeof attachment.language === "string";
|
|
1177
|
+
}
|
|
1178
|
+
function isMessage(value) {
|
|
1179
|
+
if (!hasExactDataKeys(value, ["id", "timestamp", "content"], ["replyTo", "expectsReply", "control"])) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
const message = value;
|
|
1183
|
+
if (typeof message.id !== "string" || typeof message.timestamp !== "number") {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
if (message.replyTo !== void 0 && typeof message.replyTo !== "string") {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
if (message.expectsReply !== void 0 && typeof message.expectsReply !== "boolean") {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
if (message.control !== void 0) {
|
|
1193
|
+
try {
|
|
1194
|
+
parseCorrelatedBossControl(message.control, message.id);
|
|
1195
|
+
} catch {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
|
|
1199
|
+
}
|
|
1200
|
+
if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
const content = message.content;
|
|
1204
|
+
if (typeof content.text !== "string") {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) return false;
|
|
1208
|
+
return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment);
|
|
1209
|
+
}
|
|
1210
|
+
function isSessionInfo(value) {
|
|
1211
|
+
if (!hasExactDataKeys(
|
|
1212
|
+
value,
|
|
1213
|
+
["id", "cwd", "model", "pid", "startedAt", "lastActivity"],
|
|
1214
|
+
[
|
|
1215
|
+
"name",
|
|
1216
|
+
"status",
|
|
1217
|
+
"peerUid",
|
|
1218
|
+
"trustedLocal",
|
|
1219
|
+
"origin",
|
|
1220
|
+
"remoteHostId",
|
|
1221
|
+
"parentSessionId",
|
|
1222
|
+
"rootSessionId",
|
|
1223
|
+
"generation",
|
|
1224
|
+
"canDelegate",
|
|
1225
|
+
"depth",
|
|
1226
|
+
"maxDepth",
|
|
1227
|
+
"maxChildren",
|
|
1228
|
+
"boss"
|
|
1229
|
+
]
|
|
1230
|
+
)) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
const session = value;
|
|
1234
|
+
if (typeof session.id !== "string" || typeof session.cwd !== "string" || typeof session.model !== "string" || typeof session.pid !== "number" || typeof session.startedAt !== "number" || typeof session.lastActivity !== "number") {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
if (session.name !== void 0 && typeof session.name !== "string") {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
if (session.status !== void 0 && typeof session.status !== "string") {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
if (session.peerUid !== void 0 && typeof session.peerUid !== "number") {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
if (session.trustedLocal !== void 0 && typeof session.trustedLocal !== "boolean") return false;
|
|
1247
|
+
if (session.origin !== void 0 && session.origin !== "local" && session.origin !== "remote") return false;
|
|
1248
|
+
if (session.remoteHostId !== void 0 && typeof session.remoteHostId !== "string") return false;
|
|
1249
|
+
if (session.parentSessionId !== void 0 && typeof session.parentSessionId !== "string") return false;
|
|
1250
|
+
if (session.rootSessionId !== void 0 && typeof session.rootSessionId !== "string") return false;
|
|
1251
|
+
if (session.generation !== void 0 && (typeof session.generation !== "number" || !Number.isSafeInteger(session.generation))) return false;
|
|
1252
|
+
if (session.canDelegate !== void 0 && typeof session.canDelegate !== "boolean") return false;
|
|
1253
|
+
for (const field of ["depth", "maxDepth", "maxChildren"]) {
|
|
1254
|
+
if (session[field] !== void 0 && (typeof session[field] !== "number" || !Number.isSafeInteger(session[field]))) return false;
|
|
1255
|
+
}
|
|
1256
|
+
if (session.boss !== void 0) {
|
|
1257
|
+
try {
|
|
1258
|
+
parseBossSessionMetadata(session.boss, session.id);
|
|
1259
|
+
} catch {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
function isRemoteAccessMetadata(value) {
|
|
1266
|
+
if (!hasExactDataKeys(
|
|
1267
|
+
value,
|
|
1268
|
+
["origin", "remoteHostId", "parentSessionId", "rootSessionId", "generation", "canDelegate", "depth", "maxDepth", "maxChildren"],
|
|
1269
|
+
["sessionCredential"]
|
|
1270
|
+
)) return false;
|
|
1271
|
+
const access = value;
|
|
1272
|
+
return access.origin === "remote" && typeof access.remoteHostId === "string" && typeof access.parentSessionId === "string" && typeof access.rootSessionId === "string" && typeof access.generation === "number" && Number.isSafeInteger(access.generation) && access.generation > 0 && typeof access.canDelegate === "boolean" && typeof access.depth === "number" && Number.isSafeInteger(access.depth) && typeof access.maxDepth === "number" && Number.isSafeInteger(access.maxDepth) && typeof access.maxChildren === "number" && Number.isSafeInteger(access.maxChildren) && (access.sessionCredential === void 0 || typeof access.sessionCredential === "string");
|
|
1273
|
+
}
|
|
1274
|
+
function isDeliveryFailureCode(value) {
|
|
1275
|
+
return typeof value === "string" && DELIVERY_FAILURE_CODES.includes(value);
|
|
1276
|
+
}
|
|
1277
|
+
var IntercomClient = class extends EventEmitter {
|
|
1278
|
+
constructor(options = {}) {
|
|
1279
|
+
super();
|
|
1280
|
+
this.options = options;
|
|
1281
|
+
this.scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
1282
|
+
}
|
|
1283
|
+
options;
|
|
1284
|
+
socket = null;
|
|
1285
|
+
scopeId;
|
|
1286
|
+
_sessionId = null;
|
|
1287
|
+
pendingSends = /* @__PURE__ */ new Map();
|
|
1288
|
+
lateDeliveryAcceptances = /* @__PURE__ */ new Map();
|
|
1289
|
+
pendingLists = /* @__PURE__ */ new Map();
|
|
1290
|
+
pendingAskControls = /* @__PURE__ */ new Map();
|
|
1291
|
+
outbox = null;
|
|
1292
|
+
remoteAccessCredential;
|
|
1293
|
+
requestedBossRegistration;
|
|
1294
|
+
bossSessionMetadata;
|
|
1295
|
+
disconnecting = false;
|
|
1296
|
+
disconnectError = null;
|
|
1297
|
+
failPending(error) {
|
|
1298
|
+
for (const pending of this.pendingSends.values()) {
|
|
1299
|
+
pending.reject(error);
|
|
1300
|
+
}
|
|
1301
|
+
this.pendingSends.clear();
|
|
1302
|
+
for (const pending of this.pendingLists.values()) {
|
|
1303
|
+
pending.reject(error);
|
|
1304
|
+
}
|
|
1305
|
+
this.pendingLists.clear();
|
|
1306
|
+
for (const pending of this.pendingAskControls.values()) {
|
|
1307
|
+
clearTimeout(pending.timeout);
|
|
1308
|
+
pending.resolve(false);
|
|
1309
|
+
}
|
|
1310
|
+
this.pendingAskControls.clear();
|
|
1311
|
+
this.lateDeliveryAcceptances.clear();
|
|
1312
|
+
}
|
|
1313
|
+
hasQueuedOutboundMessage(messageId) {
|
|
1314
|
+
return this.outbox?.list().some((entry) => entry.message.id === messageId) ?? false;
|
|
1315
|
+
}
|
|
1316
|
+
get sessionId() {
|
|
1317
|
+
return this._sessionId;
|
|
1318
|
+
}
|
|
1319
|
+
get outboxSize() {
|
|
1320
|
+
return this.outbox?.list().length ?? 0;
|
|
1321
|
+
}
|
|
1322
|
+
isConnected() {
|
|
1323
|
+
const socket = this.socket;
|
|
1324
|
+
return Boolean(socket && this._sessionId && !this.disconnecting && !socket.destroyed && !socket.writableEnded && socket.writable);
|
|
1325
|
+
}
|
|
1326
|
+
requireActiveSocket() {
|
|
1327
|
+
if (this.disconnecting) {
|
|
1328
|
+
throw new Error("Client disconnecting");
|
|
1329
|
+
}
|
|
1330
|
+
const socket = this.socket;
|
|
1331
|
+
if (!socket || !this._sessionId) {
|
|
1332
|
+
throw new Error("Not connected");
|
|
1333
|
+
}
|
|
1334
|
+
if (socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1335
|
+
throw new Error("Client disconnected");
|
|
1336
|
+
}
|
|
1337
|
+
return socket;
|
|
1338
|
+
}
|
|
1339
|
+
connect(session, sessionId) {
|
|
1340
|
+
if (this.socket) {
|
|
1341
|
+
return Promise.reject(new Error("Already connected"));
|
|
1342
|
+
}
|
|
1343
|
+
return new Promise((resolve5, reject) => {
|
|
1344
|
+
let socket;
|
|
1345
|
+
let target;
|
|
1346
|
+
try {
|
|
1347
|
+
this.requestedBossRegistration = session.boss === void 0 ? void 0 : parseBossRegistrationRequest(session.boss);
|
|
1348
|
+
target = getBrokerConnectTarget();
|
|
1349
|
+
this.remoteAccessCredential = loadRemoteAccessCredential();
|
|
1350
|
+
socket = connectToBrokerTarget(target);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
this.requestedBossRegistration = void 0;
|
|
1353
|
+
reject(toError(error));
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
this.socket = socket;
|
|
1357
|
+
this.disconnectError = null;
|
|
1358
|
+
let settled = false;
|
|
1359
|
+
const timeout = setTimeout(() => {
|
|
1360
|
+
if (!this._sessionId) {
|
|
1361
|
+
cleanupConnectionAttempt();
|
|
1362
|
+
cleanupSocketListeners();
|
|
1363
|
+
if (this.socket === socket) {
|
|
1364
|
+
this.socket = null;
|
|
1365
|
+
}
|
|
1366
|
+
this.requestedBossRegistration = void 0;
|
|
1367
|
+
socket.destroy();
|
|
1368
|
+
reject(new Error("Connection timeout"));
|
|
1369
|
+
}
|
|
1370
|
+
}, 1e4);
|
|
1371
|
+
let connectionEstablished = false;
|
|
1372
|
+
const onRegistered = () => {
|
|
1373
|
+
settled = true;
|
|
1374
|
+
connectionEstablished = true;
|
|
1375
|
+
cleanupConnectionAttempt();
|
|
1376
|
+
resolve5();
|
|
1377
|
+
};
|
|
1378
|
+
const onError = (err) => {
|
|
1379
|
+
settled = true;
|
|
1380
|
+
cleanupConnectionAttempt();
|
|
1381
|
+
cleanupSocketListeners();
|
|
1382
|
+
if (this.socket === socket) {
|
|
1383
|
+
this.socket = null;
|
|
1384
|
+
}
|
|
1385
|
+
this.requestedBossRegistration = void 0;
|
|
1386
|
+
socket.destroy();
|
|
1387
|
+
reject(err);
|
|
1388
|
+
};
|
|
1389
|
+
const onClose = () => {
|
|
1390
|
+
const wasConnecting = !settled && !this._sessionId;
|
|
1391
|
+
const wasDisconnecting = this.disconnecting;
|
|
1392
|
+
const disconnectError = this.disconnectError ?? new Error("Client disconnected");
|
|
1393
|
+
this.disconnecting = false;
|
|
1394
|
+
cleanupConnectionAttempt();
|
|
1395
|
+
cleanupSocketListeners();
|
|
1396
|
+
this.failPending(disconnectError);
|
|
1397
|
+
if (this.socket === socket) {
|
|
1398
|
+
this.socket = null;
|
|
1399
|
+
}
|
|
1400
|
+
this._sessionId = null;
|
|
1401
|
+
this.requestedBossRegistration = void 0;
|
|
1402
|
+
this.bossSessionMetadata = void 0;
|
|
1403
|
+
this.disconnectError = null;
|
|
1404
|
+
if (connectionEstablished && !wasDisconnecting) {
|
|
1405
|
+
this.emit("disconnected", disconnectError);
|
|
1406
|
+
}
|
|
1407
|
+
if (wasConnecting) {
|
|
1408
|
+
reject(new Error("Connection closed before registration"));
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
const onSocketError = (err) => {
|
|
1412
|
+
if (connectionEstablished) {
|
|
1413
|
+
this.disconnectError = err;
|
|
1414
|
+
this.emit("error", err);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
const onReaderError = (error) => {
|
|
1418
|
+
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
|
|
1419
|
+
if (!connectionEstablished) {
|
|
1420
|
+
onError(protocolError);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
this.disconnectError = protocolError;
|
|
1424
|
+
this.emit("error", protocolError);
|
|
1425
|
+
socket.destroy();
|
|
1426
|
+
};
|
|
1427
|
+
const reader = createMessageReader((msg) => {
|
|
1428
|
+
this.handleBrokerMessage(msg);
|
|
1429
|
+
}, onReaderError);
|
|
1430
|
+
const cleanupConnectionAttempt = () => {
|
|
1431
|
+
this.off("_registered", onRegistered);
|
|
1432
|
+
socket.off("error", onError);
|
|
1433
|
+
clearTimeout(timeout);
|
|
1434
|
+
};
|
|
1435
|
+
const cleanupSocketListeners = () => {
|
|
1436
|
+
socket.off("data", reader);
|
|
1437
|
+
socket.off("error", onSocketError);
|
|
1438
|
+
socket.off("close", onClose);
|
|
1439
|
+
};
|
|
1440
|
+
socket.on("data", reader);
|
|
1441
|
+
socket.on("error", onError);
|
|
1442
|
+
socket.on("close", onClose);
|
|
1443
|
+
socket.on("error", onSocketError);
|
|
1444
|
+
this.once("_registered", onRegistered);
|
|
1445
|
+
try {
|
|
1446
|
+
writeMessage(socket, {
|
|
1447
|
+
type: "register",
|
|
1448
|
+
protocol: INTERCOM_PROTOCOL_NAME,
|
|
1449
|
+
version: INTERCOM_PROTOCOL_VERSION,
|
|
1450
|
+
session,
|
|
1451
|
+
...!this.remoteAccessCredential && sessionId ? { sessionId } : {},
|
|
1452
|
+
...this.remoteAccessCredential ? { access: this.remoteAccessCredential.access } : {},
|
|
1453
|
+
...this.scopeId ? { scopeId: this.scopeId } : {},
|
|
1454
|
+
...typeof target === "string" ? {} : { stateId: target.stateId }
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
cleanupConnectionAttempt();
|
|
1458
|
+
cleanupSocketListeners();
|
|
1459
|
+
if (this.socket === socket) {
|
|
1460
|
+
this.socket = null;
|
|
1461
|
+
}
|
|
1462
|
+
this.requestedBossRegistration = void 0;
|
|
1463
|
+
socket.destroy();
|
|
1464
|
+
reject(toError(error));
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
handleBrokerMessage(msg) {
|
|
1469
|
+
if (!isPlainDataRecord(msg) || typeof msg.type !== "string") {
|
|
1470
|
+
throw new Error("Invalid broker message");
|
|
1471
|
+
}
|
|
1472
|
+
const brokerMessage = msg;
|
|
1473
|
+
if (this._sessionId === null && brokerMessage.type !== "registered" && brokerMessage.type !== "error") {
|
|
1474
|
+
throw new Error(`Received ${brokerMessage.type} before registered`);
|
|
1475
|
+
}
|
|
1476
|
+
switch (brokerMessage.type) {
|
|
1477
|
+
case "registered": {
|
|
1478
|
+
const baseKeys = ["type", "sessionId", "protocol", "version"];
|
|
1479
|
+
if (this.requestedBossRegistration !== void 0 && (brokerMessage.capabilities === void 0 || brokerMessage.boss === void 0)) {
|
|
1480
|
+
throw new Error("Boss registration was downgraded to an ordinary session");
|
|
1481
|
+
}
|
|
1482
|
+
const exactKeys = this.requestedBossRegistration !== void 0 ? [...baseKeys, "capabilities", "boss"] : this.remoteAccessCredential ? [...baseKeys, "remoteAccess", "access"] : baseKeys;
|
|
1483
|
+
const optionalKeys = this.requestedBossRegistration === void 0 ? ["capabilities"] : [];
|
|
1484
|
+
if (!hasExactDataKeys(brokerMessage, exactKeys, optionalKeys) || typeof brokerMessage.sessionId !== "string" || brokerMessage.protocol !== INTERCOM_PROTOCOL_NAME || brokerMessage.version !== INTERCOM_PROTOCOL_VERSION) {
|
|
1485
|
+
throw new Error("Invalid registered message");
|
|
1486
|
+
}
|
|
1487
|
+
if (this._sessionId !== null) {
|
|
1488
|
+
throw new Error("Received duplicate registered message");
|
|
1489
|
+
}
|
|
1490
|
+
if (this.requestedBossRegistration !== void 0) {
|
|
1491
|
+
assertCompatibleBossAdvertisement(brokerMessage.capabilities);
|
|
1492
|
+
this.bossSessionMetadata = assertBossRegistrationEcho(
|
|
1493
|
+
this.requestedBossRegistration,
|
|
1494
|
+
brokerMessage.boss,
|
|
1495
|
+
brokerMessage.sessionId
|
|
1496
|
+
);
|
|
1497
|
+
this.requestedBossRegistration = void 0;
|
|
1498
|
+
} else {
|
|
1499
|
+
if (brokerMessage.boss !== void 0) {
|
|
1500
|
+
throw new Error("Ordinary registration received unsolicited Boss authority metadata");
|
|
1501
|
+
}
|
|
1502
|
+
if (brokerMessage.capabilities !== void 0) {
|
|
1503
|
+
assertCompatibleOrdinaryAdvertisement(brokerMessage.capabilities);
|
|
1504
|
+
}
|
|
1505
|
+
this.bossSessionMetadata = void 0;
|
|
1506
|
+
}
|
|
1507
|
+
if (this.remoteAccessCredential) {
|
|
1508
|
+
const contract = brokerMessage.remoteAccess;
|
|
1509
|
+
const contractFields = hasExactDataKeys(
|
|
1510
|
+
contract,
|
|
1511
|
+
["feature", "policySemanticsVersion", "policySemanticsHash"]
|
|
1512
|
+
) ? contract : void 0;
|
|
1513
|
+
if (!contractFields || contractFields.feature !== "remote-access-v1" || contractFields.policySemanticsVersion !== POLICY_SEMANTICS_VERSION || contractFields.policySemanticsHash !== POLICY_SEMANTICS_HASH) {
|
|
1514
|
+
throw new Error("Remote Intercom policy contract is absent or incompatible");
|
|
1515
|
+
}
|
|
1516
|
+
if (!isRemoteAccessMetadata(brokerMessage.access)) {
|
|
1517
|
+
throw new Error("Remote Intercom registration omitted broker-owned provenance");
|
|
1518
|
+
}
|
|
1519
|
+
if (this.remoteAccessCredential.enrollment) {
|
|
1520
|
+
writeRemoteSessionCredential(this.remoteAccessCredential.path, brokerMessage.sessionId, brokerMessage.access);
|
|
1521
|
+
} else {
|
|
1522
|
+
const reconnect = this.remoteAccessCredential.access;
|
|
1523
|
+
if (!isPlainDataRecord(reconnect) || reconnect.sessionId !== brokerMessage.sessionId || reconnect.generation !== brokerMessage.access.generation) {
|
|
1524
|
+
throw new Error("Remote Intercom reconnect identity or generation changed unexpectedly");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
this._sessionId = brokerMessage.sessionId;
|
|
1529
|
+
this.outbox = new PersistentOutboundOutbox(brokerMessage.sessionId);
|
|
1530
|
+
this.lateDeliveryAcceptances.clear();
|
|
1531
|
+
this.replayOutbox();
|
|
1532
|
+
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
case "sessions": {
|
|
1536
|
+
const { requestId, sessions } = brokerMessage;
|
|
1537
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "requestId", "sessions"]) || typeof requestId !== "string" || !isDenseArrayOf(sessions, isSessionInfo)) {
|
|
1538
|
+
throw new Error("Invalid sessions message");
|
|
1539
|
+
}
|
|
1540
|
+
const pending = this.pendingLists.get(requestId);
|
|
1541
|
+
if (!pending) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
this.pendingLists.delete(requestId);
|
|
1545
|
+
pending.resolve(sessions);
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
case "message": {
|
|
1549
|
+
const { deliveryId, from, message } = brokerMessage;
|
|
1550
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "deliveryId", "from", "message"]) || typeof deliveryId !== "string" || !isSessionInfo(from) || !isMessage(message)) {
|
|
1551
|
+
throw new Error("Invalid message event");
|
|
1552
|
+
}
|
|
1553
|
+
if (message.control !== void 0) {
|
|
1554
|
+
if (this.bossSessionMetadata === void 0) {
|
|
1555
|
+
throw new Error("Ordinary session received unavailable Boss control traffic");
|
|
1556
|
+
}
|
|
1557
|
+
this.emit("control", from, message.control, deliveryId);
|
|
1558
|
+
} else {
|
|
1559
|
+
this.emit("message", from, message, deliveryId);
|
|
1560
|
+
}
|
|
1561
|
+
break;
|
|
1562
|
+
}
|
|
1563
|
+
case "delivery_accepted": {
|
|
1564
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1565
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "deliveryId"]) || typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1566
|
+
throw new Error("Invalid delivery_accepted message");
|
|
1567
|
+
}
|
|
1568
|
+
const pending = this.pendingSends.get(messageId);
|
|
1569
|
+
if (pending) {
|
|
1570
|
+
if (pending.accepted) {
|
|
1571
|
+
throw new Error("Duplicate delivery_accepted message");
|
|
1572
|
+
}
|
|
1573
|
+
pending.accepted = true;
|
|
1574
|
+
pending.deliveryId = deliveryId;
|
|
1575
|
+
this.emit("delivery_accepted", messageId, deliveryId);
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1579
|
+
throw new Error("Unexpected delivery_accepted message without a pending send or queued outbox message");
|
|
1580
|
+
}
|
|
1581
|
+
if (this.lateDeliveryAcceptances.has(messageId)) {
|
|
1582
|
+
throw new Error("Duplicate delivery_accepted message");
|
|
1583
|
+
}
|
|
1584
|
+
this.lateDeliveryAcceptances.set(messageId, deliveryId);
|
|
1585
|
+
this.emit("delivery_accepted", messageId, deliveryId);
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1588
|
+
case "delivered": {
|
|
1589
|
+
const { deliveryId, messageId } = brokerMessage;
|
|
1590
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "deliveryId"]) || typeof deliveryId !== "string" || typeof messageId !== "string") {
|
|
1591
|
+
throw new Error("Invalid delivered message");
|
|
1592
|
+
}
|
|
1593
|
+
const pending = this.pendingSends.get(messageId);
|
|
1594
|
+
if (pending) {
|
|
1595
|
+
if (!pending.accepted) {
|
|
1596
|
+
throw new Error("Delivered message arrived before delivery acceptance");
|
|
1597
|
+
}
|
|
1598
|
+
if (pending.deliveryId !== deliveryId) {
|
|
1599
|
+
throw new Error("Delivered message used a mismatched delivery ID");
|
|
1600
|
+
}
|
|
1601
|
+
this.outbox?.remove(messageId);
|
|
1602
|
+
this.pendingSends.delete(messageId);
|
|
1603
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1604
|
+
pending.resolve({ id: messageId, accepted: true, delivered: true, deliveryId });
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1608
|
+
throw new Error("Unexpected delivered message without a pending send or queued outbox message");
|
|
1609
|
+
}
|
|
1610
|
+
const acceptedDeliveryId = this.lateDeliveryAcceptances.get(messageId);
|
|
1611
|
+
if (acceptedDeliveryId === void 0) {
|
|
1612
|
+
throw new Error("Delivered message arrived before delivery acceptance");
|
|
1613
|
+
}
|
|
1614
|
+
if (acceptedDeliveryId !== deliveryId) {
|
|
1615
|
+
throw new Error("Delivered message used a mismatched delivery ID");
|
|
1616
|
+
}
|
|
1617
|
+
this.outbox?.remove(messageId);
|
|
1618
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1619
|
+
this.emit("outbox_delivered", messageId, deliveryId);
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
case "delivery_failed": {
|
|
1623
|
+
const { accepted, code, messageId, reason } = brokerMessage;
|
|
1624
|
+
if (!hasExactDataKeys(brokerMessage, ["type", "messageId", "accepted", "code", "reason"]) || typeof accepted !== "boolean" || !isDeliveryFailureCode(code) || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1625
|
+
throw new Error("Invalid delivery_failed message");
|
|
1626
|
+
}
|
|
1627
|
+
const pending = this.pendingSends.get(messageId);
|
|
1628
|
+
if (pending) {
|
|
1629
|
+
if (accepted !== pending.accepted) {
|
|
1630
|
+
throw new Error("Delivery failure contradicted the accepted delivery state");
|
|
1631
|
+
}
|
|
1632
|
+
this.outbox?.remove(messageId);
|
|
1633
|
+
this.pendingSends.delete(messageId);
|
|
1634
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1635
|
+
pending.resolve({
|
|
1636
|
+
id: messageId,
|
|
1637
|
+
accepted,
|
|
1638
|
+
delivered: false,
|
|
1639
|
+
code,
|
|
1640
|
+
reason,
|
|
1641
|
+
...pending.deliveryId !== void 0 ? { deliveryId: pending.deliveryId } : {}
|
|
1642
|
+
});
|
|
1643
|
+
break;
|
|
1644
|
+
}
|
|
1645
|
+
if (!this.hasQueuedOutboundMessage(messageId)) {
|
|
1646
|
+
throw new Error("Unexpected delivery_failed message without a pending send or queued outbox message");
|
|
1647
|
+
}
|
|
1648
|
+
if (accepted !== this.lateDeliveryAcceptances.has(messageId)) {
|
|
1649
|
+
throw new Error("Delivery failure contradicted the accepted delivery state");
|
|
1650
|
+
}
|
|
1651
|
+
this.outbox?.remove(messageId);
|
|
1652
|
+
this.lateDeliveryAcceptances.delete(messageId);
|
|
1653
|
+
this.emit("outbox_failed", messageId, code, reason);
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
case "ask_deferred": {
|
|
1657
|
+
const { fromSessionId, messageId } = brokerMessage;
|
|
1658
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string") {
|
|
1659
|
+
throw new Error("Invalid ask_deferred message");
|
|
1660
|
+
}
|
|
1661
|
+
this.emit("ask_deferred", messageId, fromSessionId);
|
|
1662
|
+
break;
|
|
1663
|
+
}
|
|
1664
|
+
case "ask_cancelled": {
|
|
1665
|
+
const { fromSessionId, messageId, reason } = brokerMessage;
|
|
1666
|
+
if (typeof fromSessionId !== "string" || typeof messageId !== "string" || typeof reason !== "string") {
|
|
1667
|
+
throw new Error("Invalid ask_cancelled message");
|
|
1668
|
+
}
|
|
1669
|
+
this.emit("ask_cancelled", messageId, fromSessionId, reason);
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
case "ask_control_result": {
|
|
1673
|
+
const { action, applied, messageId, requestId } = brokerMessage;
|
|
1674
|
+
if (action !== "defer" && action !== "cancel" || typeof applied !== "boolean" || typeof messageId !== "string" || typeof requestId !== "string") {
|
|
1675
|
+
throw new Error("Invalid ask_control_result message");
|
|
1676
|
+
}
|
|
1677
|
+
const pending = this.pendingAskControls.get(requestId);
|
|
1678
|
+
if (!pending) return;
|
|
1679
|
+
clearTimeout(pending.timeout);
|
|
1680
|
+
this.pendingAskControls.delete(requestId);
|
|
1681
|
+
pending.resolve(applied);
|
|
1682
|
+
break;
|
|
1683
|
+
}
|
|
1684
|
+
case "session_joined": {
|
|
1685
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1686
|
+
throw new Error("Invalid session_joined message");
|
|
1687
|
+
}
|
|
1688
|
+
this.emit("session_joined", brokerMessage.session);
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
case "session_left": {
|
|
1692
|
+
if (typeof brokerMessage.sessionId !== "string") {
|
|
1693
|
+
throw new Error("Invalid session_left message");
|
|
1694
|
+
}
|
|
1695
|
+
this.emit("session_left", brokerMessage.sessionId);
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
case "presence_update": {
|
|
1699
|
+
if (!isSessionInfo(brokerMessage.session)) {
|
|
1700
|
+
throw new Error("Invalid presence_update message");
|
|
1701
|
+
}
|
|
1702
|
+
this.emit("presence_update", brokerMessage.session);
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
case "error": {
|
|
1706
|
+
if (typeof brokerMessage.code !== "string" || typeof brokerMessage.error !== "string") {
|
|
1707
|
+
throw new Error("Invalid error message");
|
|
1708
|
+
}
|
|
1709
|
+
if (this._sessionId === null) {
|
|
1710
|
+
const error2 = new Error(brokerMessage.error);
|
|
1711
|
+
error2.code = brokerMessage.code;
|
|
1712
|
+
throw error2;
|
|
1713
|
+
}
|
|
1714
|
+
const error = new Error(brokerMessage.error);
|
|
1715
|
+
error.code = brokerMessage.code;
|
|
1716
|
+
this.emit("error", error);
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
default:
|
|
1720
|
+
throw new Error(`Unknown broker message type: ${brokerMessage.type}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
async disconnect(preserveAsks = false) {
|
|
1724
|
+
const socket = this.socket;
|
|
1725
|
+
if (!socket) {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
this.disconnecting = true;
|
|
1729
|
+
this.disconnectError = null;
|
|
1730
|
+
this.failPending(new Error("Client disconnected"));
|
|
1731
|
+
if (!preserveAsks) this.outbox?.clear();
|
|
1732
|
+
await new Promise((resolve5) => {
|
|
1733
|
+
let settled = false;
|
|
1734
|
+
const finish = () => {
|
|
1735
|
+
if (settled) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
settled = true;
|
|
1739
|
+
clearTimeout(timeout);
|
|
1740
|
+
socket.off("close", onClose);
|
|
1741
|
+
socket.off("error", onError);
|
|
1742
|
+
resolve5();
|
|
1743
|
+
};
|
|
1744
|
+
const onClose = () => finish();
|
|
1745
|
+
const onError = () => {
|
|
1746
|
+
socket.destroy();
|
|
1747
|
+
};
|
|
1748
|
+
const timeout = setTimeout(() => {
|
|
1749
|
+
socket.destroy();
|
|
1750
|
+
}, 2e3);
|
|
1751
|
+
socket.once("close", onClose);
|
|
1752
|
+
socket.once("error", onError);
|
|
1753
|
+
try {
|
|
1754
|
+
writeMessage(socket, { type: "unregister", ...preserveAsks ? { preserveAsks: true } : {} });
|
|
1755
|
+
socket.end();
|
|
1756
|
+
} catch {
|
|
1757
|
+
socket.destroy();
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
listSessions() {
|
|
1762
|
+
let socket;
|
|
1763
|
+
try {
|
|
1764
|
+
socket = this.requireActiveSocket();
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
return Promise.reject(toError(error));
|
|
1767
|
+
}
|
|
1768
|
+
return new Promise((resolve5, reject) => {
|
|
1769
|
+
const requestId = randomUUID2();
|
|
1770
|
+
const wrappedResolve = (sessions) => {
|
|
1771
|
+
clearTimeout(timeout);
|
|
1772
|
+
resolve5(sessions);
|
|
1773
|
+
};
|
|
1774
|
+
const wrappedReject = (error) => {
|
|
1775
|
+
clearTimeout(timeout);
|
|
1776
|
+
reject(error);
|
|
1777
|
+
};
|
|
1778
|
+
const timeout = setTimeout(() => {
|
|
1779
|
+
if (this.pendingLists.has(requestId)) {
|
|
1780
|
+
this.pendingLists.delete(requestId);
|
|
1781
|
+
wrappedReject(new Error("List sessions timeout"));
|
|
1782
|
+
}
|
|
1783
|
+
}, 5e3);
|
|
1784
|
+
this.pendingLists.set(requestId, { resolve: wrappedResolve, reject: wrappedReject });
|
|
1785
|
+
try {
|
|
1786
|
+
writeMessage(socket, { type: "list", requestId });
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
clearTimeout(timeout);
|
|
1789
|
+
this.pendingLists.delete(requestId);
|
|
1790
|
+
reject(toError(error));
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
send(to, options) {
|
|
1795
|
+
return this.sendInternal(to, options);
|
|
1796
|
+
}
|
|
1797
|
+
sendControl(to, controlValue) {
|
|
1798
|
+
const control = parseCorrelatedBossControl(controlValue);
|
|
1799
|
+
return this.sendInternal(to, {
|
|
1800
|
+
text: "",
|
|
1801
|
+
messageId: control.messageId,
|
|
1802
|
+
control
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
sendInternal(to, options) {
|
|
1806
|
+
let socket;
|
|
1807
|
+
try {
|
|
1808
|
+
socket = this.requireActiveSocket();
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
return Promise.reject(toError(error));
|
|
1811
|
+
}
|
|
1812
|
+
const messageId = options.messageId ?? randomUUID2();
|
|
1813
|
+
if (this.pendingSends.has(messageId)) {
|
|
1814
|
+
return Promise.resolve({
|
|
1815
|
+
id: messageId,
|
|
1816
|
+
accepted: false,
|
|
1817
|
+
delivered: false,
|
|
1818
|
+
code: "DUPLICATE_MESSAGE_ID",
|
|
1819
|
+
reason: `Message ID ${messageId} is already pending`
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
const message = {
|
|
1823
|
+
id: messageId,
|
|
1824
|
+
timestamp: Date.now(),
|
|
1825
|
+
replyTo: options.replyTo,
|
|
1826
|
+
expectsReply: options.expectsReply,
|
|
1827
|
+
...options.control === void 0 ? {} : { control: options.control },
|
|
1828
|
+
content: {
|
|
1829
|
+
text: options.text,
|
|
1830
|
+
...options.attachments === void 0 ? {} : { attachments: options.attachments }
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
try {
|
|
1834
|
+
this.outbox?.enqueue(to, message);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
return Promise.reject(toError(error));
|
|
1837
|
+
}
|
|
1838
|
+
return new Promise((resolve5, reject) => {
|
|
1839
|
+
const wrappedResolve = (result) => {
|
|
1840
|
+
clearTimeout(timeout);
|
|
1841
|
+
resolve5(result);
|
|
1842
|
+
};
|
|
1843
|
+
const wrappedReject = (error) => {
|
|
1844
|
+
clearTimeout(timeout);
|
|
1845
|
+
reject(error);
|
|
1846
|
+
};
|
|
1847
|
+
const timeout = setTimeout(() => {
|
|
1848
|
+
const pending = this.pendingSends.get(messageId);
|
|
1849
|
+
if (pending) {
|
|
1850
|
+
if (pending.accepted && pending.deliveryId !== void 0 && this.hasQueuedOutboundMessage(messageId)) {
|
|
1851
|
+
this.lateDeliveryAcceptances.set(messageId, pending.deliveryId);
|
|
1852
|
+
}
|
|
1853
|
+
this.pendingSends.delete(messageId);
|
|
1854
|
+
wrappedReject(new Error("Send timeout"));
|
|
1855
|
+
}
|
|
1856
|
+
}, 1e4);
|
|
1857
|
+
this.pendingSends.set(messageId, {
|
|
1858
|
+
accepted: false,
|
|
1859
|
+
resolve: wrappedResolve,
|
|
1860
|
+
reject: wrappedReject
|
|
1861
|
+
});
|
|
1862
|
+
try {
|
|
1863
|
+
writeMessage(socket, { type: "send", to, message });
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
clearTimeout(timeout);
|
|
1866
|
+
this.pendingSends.delete(messageId);
|
|
1867
|
+
reject(toError(error));
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
acknowledgeMessage(deliveryId) {
|
|
1872
|
+
return this.writeControlMessage({ type: "message_received", deliveryId });
|
|
1873
|
+
}
|
|
1874
|
+
rejectMessage(deliveryId, reason) {
|
|
1875
|
+
return this.writeControlMessage({ type: "message_rejected", deliveryId, code: "CONFLICTING_MESSAGE_ID", reason });
|
|
1876
|
+
}
|
|
1877
|
+
deferAsk(messageId) {
|
|
1878
|
+
return this.sendAskControl("defer", messageId);
|
|
1879
|
+
}
|
|
1880
|
+
cancelAsk(messageId) {
|
|
1881
|
+
return this.sendAskControl("cancel", messageId);
|
|
1882
|
+
}
|
|
1883
|
+
sendAskControl(action, messageId) {
|
|
1884
|
+
const requestId = randomUUID2();
|
|
1885
|
+
return new Promise((resolve5) => {
|
|
1886
|
+
const timeout = setTimeout(() => {
|
|
1887
|
+
this.pendingAskControls.delete(requestId);
|
|
1888
|
+
resolve5(false);
|
|
1889
|
+
}, 2e3);
|
|
1890
|
+
timeout.unref?.();
|
|
1891
|
+
this.pendingAskControls.set(requestId, { resolve: resolve5, timeout });
|
|
1892
|
+
if (!this.writeControlMessage({ type: action === "defer" ? "defer_ask" : "cancel_ask", requestId, messageId })) {
|
|
1893
|
+
clearTimeout(timeout);
|
|
1894
|
+
this.pendingAskControls.delete(requestId);
|
|
1895
|
+
resolve5(false);
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
writeControlMessage(message) {
|
|
1900
|
+
if (this.disconnecting) {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
const socket = this.socket;
|
|
1904
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
writeMessage(socket, message);
|
|
1909
|
+
return true;
|
|
1910
|
+
} catch {
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
replayOutbox() {
|
|
1915
|
+
const socket = this.socket;
|
|
1916
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) return;
|
|
1917
|
+
for (const entry of this.outbox?.list() ?? []) {
|
|
1918
|
+
if (this.pendingSends.has(entry.message.id)) continue;
|
|
1919
|
+
this.lateDeliveryAcceptances.delete(entry.message.id);
|
|
1920
|
+
this.pendingSends.set(entry.message.id, {
|
|
1921
|
+
accepted: false,
|
|
1922
|
+
resolve: (result) => {
|
|
1923
|
+
if (result.delivered && result.deliveryId) {
|
|
1924
|
+
this.emit("outbox_delivered", result.id, result.deliveryId);
|
|
1925
|
+
} else {
|
|
1926
|
+
this.emit("outbox_failed", result.id, result.code, result.reason);
|
|
1927
|
+
}
|
|
1928
|
+
},
|
|
1929
|
+
reject: () => {
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
try {
|
|
1933
|
+
writeMessage(socket, { type: "send", to: entry.to, message: entry.message });
|
|
1934
|
+
} catch {
|
|
1935
|
+
this.pendingSends.delete(entry.message.id);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
updatePresence(updates) {
|
|
1941
|
+
if (this.disconnecting) {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const socket = this.socket;
|
|
1945
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
writeMessage(socket, { type: "presence", ...updates });
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
// broker/spawn.ts
|
|
1953
|
+
import { spawn as spawn2 } from "child_process";
|
|
1954
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1955
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1956
|
+
import { fileURLToPath } from "url";
|
|
1957
|
+
import { createRequire } from "module";
|
|
1958
|
+
import net2 from "net";
|
|
1959
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1960
|
+
import { POLICY_SEMANTICS_HASH as POLICY_SEMANTICS_HASH2, POLICY_SEMANTICS_VERSION as POLICY_SEMANTICS_VERSION2 } from "@ctliz/agent-intercom-core";
|
|
1961
|
+
var INTERCOM_DIR = getIntercomDirPath();
|
|
1962
|
+
var EXTENSION_DIR = join5(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
1963
|
+
var BROKER_PID = join5(INTERCOM_DIR, "broker.pid");
|
|
1964
|
+
var BROKER_SPAWN_LOCK = join5(INTERCOM_DIR, "broker.spawn.lock");
|
|
1965
|
+
function sleep(ms) {
|
|
1966
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1967
|
+
}
|
|
1968
|
+
function getBrokerEntryPath(moduleUrl = import.meta.url) {
|
|
1969
|
+
const moduleDir = dirname4(fileURLToPath(moduleUrl));
|
|
1970
|
+
const bundledBroker = join5(moduleDir, "broker.mjs");
|
|
1971
|
+
return existsSync3(bundledBroker) ? bundledBroker : join5(moduleDir, "broker.ts");
|
|
1972
|
+
}
|
|
1973
|
+
function getTsxCliPath(extensionDir = EXTENSION_DIR) {
|
|
1974
|
+
try {
|
|
1975
|
+
const requireFromExtension = createRequire(import.meta.url);
|
|
1976
|
+
const tsxMain = requireFromExtension.resolve("tsx");
|
|
1977
|
+
return join5(dirname4(tsxMain), "cli.mjs");
|
|
1978
|
+
} catch {
|
|
1979
|
+
return join5(extensionDir, "node_modules", "tsx", "dist", "cli.mjs");
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
function quoteWindowsArg(value) {
|
|
1983
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1984
|
+
}
|
|
1985
|
+
function getWindowsHiddenLauncherPath(intercomDir = INTERCOM_DIR) {
|
|
1986
|
+
return join5(intercomDir, "broker-launch.vbs");
|
|
1987
|
+
}
|
|
1988
|
+
function usesDefaultBrokerCommand(brokerCommand, brokerArgs) {
|
|
1989
|
+
return brokerCommand === "npx" && brokerArgs.length === 2 && brokerArgs[0] === "--no-install" && brokerArgs[1] === "tsx";
|
|
1990
|
+
}
|
|
1991
|
+
function getWindowsBrokerCommandLine(brokerPath, extensionDir = EXTENSION_DIR, nodePath = process.execPath, brokerCommand = "npx", brokerArgs = ["--no-install", "tsx"]) {
|
|
1992
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
1993
|
+
return [quoteWindowsArg(nodePath), quoteWindowsArg(getTsxCliPath(extensionDir)), quoteWindowsArg(brokerPath)].join(" ");
|
|
1994
|
+
}
|
|
1995
|
+
return [quoteWindowsArg(brokerCommand), ...brokerArgs.map(quoteWindowsArg), quoteWindowsArg(brokerPath)].join(" ");
|
|
1996
|
+
}
|
|
1997
|
+
function getWindowsHiddenLauncherScript(commandLine) {
|
|
1998
|
+
return [
|
|
1999
|
+
'Set WshShell = CreateObject("WScript.Shell")',
|
|
2000
|
+
`WshShell.Run "${commandLine.replace(/"/g, '""')}", 0, False`,
|
|
2001
|
+
"Set WshShell = Nothing",
|
|
2002
|
+
""
|
|
2003
|
+
].join("\r\n");
|
|
2004
|
+
}
|
|
2005
|
+
function isBrokerHealthOkMessage(message, requestId) {
|
|
2006
|
+
if (!hasExactDataKeys(
|
|
2007
|
+
message,
|
|
2008
|
+
["type", "requestId", "protocol", "version", "endpoint", "remoteAccess"],
|
|
2009
|
+
["capabilities"]
|
|
2010
|
+
)) return false;
|
|
2011
|
+
const response = message;
|
|
2012
|
+
if (response.type !== "health_ok" || response.requestId !== requestId || response.protocol !== INTERCOM_PROTOCOL_NAME || response.version !== INTERCOM_PROTOCOL_VERSION || response.endpoint !== "local") return false;
|
|
2013
|
+
const remoteAccess = response.remoteAccess;
|
|
2014
|
+
if (!hasExactDataKeys(remoteAccess, ["feature", "policySemanticsVersion", "policySemanticsHash"])) return false;
|
|
2015
|
+
const contract = remoteAccess;
|
|
2016
|
+
const legacyCompatible = contract.feature === "remote-access-v1" && contract.policySemanticsVersion === POLICY_SEMANTICS_VERSION2 && contract.policySemanticsHash === POLICY_SEMANTICS_HASH2;
|
|
2017
|
+
if (!legacyCompatible) return false;
|
|
2018
|
+
if (response.capabilities !== void 0) {
|
|
2019
|
+
try {
|
|
2020
|
+
assertCompatibleOrdinaryAdvertisement(response.capabilities);
|
|
2021
|
+
} catch {
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
2027
|
+
function writeWindowsHiddenLauncher(commandLine, launcherPath = getWindowsHiddenLauncherPath()) {
|
|
2028
|
+
ensureIntercomRuntimeDir(dirname4(launcherPath));
|
|
2029
|
+
writeFileSync3(launcherPath, getWindowsHiddenLauncherScript(commandLine), {
|
|
2030
|
+
encoding: "utf-8",
|
|
2031
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
2032
|
+
});
|
|
2033
|
+
restrictIntercomRuntimeFile(launcherPath);
|
|
2034
|
+
return launcherPath;
|
|
2035
|
+
}
|
|
2036
|
+
function getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs, extensionDir = EXTENSION_DIR, platform = process.platform, intercomDir = INTERCOM_DIR, nodePath = process.execPath) {
|
|
2037
|
+
if (platform === "win32") {
|
|
2038
|
+
const launcherPath = getWindowsHiddenLauncherPath(intercomDir);
|
|
2039
|
+
return {
|
|
2040
|
+
kind: "windows-launcher",
|
|
2041
|
+
command: "wscript.exe",
|
|
2042
|
+
args: [launcherPath],
|
|
2043
|
+
launcherPath,
|
|
2044
|
+
launcherCommandLine: getWindowsBrokerCommandLine(brokerPath, extensionDir, nodePath, brokerCommand, brokerArgs)
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
|
|
2048
|
+
return {
|
|
2049
|
+
kind: "direct",
|
|
2050
|
+
command: nodePath,
|
|
2051
|
+
args: [getTsxCliPath(extensionDir), brokerPath]
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
return {
|
|
2055
|
+
kind: "direct",
|
|
2056
|
+
command: brokerCommand,
|
|
2057
|
+
args: [...brokerArgs, brokerPath]
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env) {
|
|
2061
|
+
return {
|
|
2062
|
+
detached: true,
|
|
2063
|
+
stdio: "ignore",
|
|
2064
|
+
cwd: extensionDir,
|
|
2065
|
+
env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
|
|
2066
|
+
windowsHide: true
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
function toError2(error) {
|
|
2070
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2071
|
+
}
|
|
2072
|
+
async function spawnBrokerIfNeeded(brokerCommand, brokerArgs) {
|
|
2073
|
+
ensureIntercomRuntimeDir(INTERCOM_DIR);
|
|
2074
|
+
if (await isBrokerRunning()) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const ownsLock = acquireSpawnLock();
|
|
2078
|
+
if (!ownsLock) {
|
|
2079
|
+
await waitForBroker();
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
if (await isBrokerRunning()) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (await checkBrokerHealth() === "incompatible") {
|
|
2087
|
+
throw new Error(`Incompatible live intercom broker; expected ${INTERCOM_PROTOCOL_NAME} v${INTERCOM_PROTOCOL_VERSION}. Stop it explicitly during a coordinated migration.`);
|
|
2088
|
+
}
|
|
2089
|
+
const brokerPath = getBrokerEntryPath();
|
|
2090
|
+
const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
|
|
2091
|
+
if (launch.kind === "windows-launcher") {
|
|
2092
|
+
writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
|
|
2093
|
+
}
|
|
2094
|
+
const child = spawn2(launch.command, launch.args, getBrokerSpawnOptions());
|
|
2095
|
+
child.unref();
|
|
2096
|
+
await new Promise((resolve5, reject) => {
|
|
2097
|
+
const cleanup = () => {
|
|
2098
|
+
child.off("error", onError);
|
|
2099
|
+
child.off("exit", onExit);
|
|
2100
|
+
};
|
|
2101
|
+
const onError = (error) => {
|
|
2102
|
+
cleanup();
|
|
2103
|
+
reject(new Error(`Failed to spawn intercom broker: ${error.message}`, { cause: error }));
|
|
2104
|
+
};
|
|
2105
|
+
const onExit = (code, signal) => {
|
|
2106
|
+
if (launch.kind === "windows-launcher" && code === 0 && signal === null) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
cleanup();
|
|
2110
|
+
if (signal) {
|
|
2111
|
+
reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
|
|
2115
|
+
};
|
|
2116
|
+
child.once("error", onError);
|
|
2117
|
+
child.once("exit", onExit);
|
|
2118
|
+
waitForBroker().then(() => {
|
|
2119
|
+
cleanup();
|
|
2120
|
+
resolve5();
|
|
2121
|
+
}, (error) => {
|
|
2122
|
+
cleanup();
|
|
2123
|
+
reject(toError2(error));
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
} finally {
|
|
2127
|
+
releaseSpawnLock();
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async function isBrokerRunning() {
|
|
2131
|
+
if (await checkSocketConnectable()) {
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
if (!existsSync3(BROKER_PID)) return false;
|
|
2135
|
+
try {
|
|
2136
|
+
const pid = parseInt(readFileSync5(BROKER_PID, "utf-8").trim(), 10);
|
|
2137
|
+
if (!Number.isFinite(pid)) return false;
|
|
2138
|
+
process.kill(pid, 0);
|
|
2139
|
+
return checkSocketConnectable();
|
|
2140
|
+
} catch {
|
|
2141
|
+
return false;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function connectToBrokerTarget2(target) {
|
|
2145
|
+
return typeof target === "string" ? net2.connect(target) : net2.connect({ host: target.host, port: target.port });
|
|
2146
|
+
}
|
|
2147
|
+
async function checkSocketConnectable() {
|
|
2148
|
+
return await checkBrokerHealth() === "compatible";
|
|
2149
|
+
}
|
|
2150
|
+
function checkBrokerHealth() {
|
|
2151
|
+
return new Promise((resolve5) => {
|
|
2152
|
+
let target;
|
|
2153
|
+
try {
|
|
2154
|
+
target = getBrokerConnectTarget();
|
|
2155
|
+
} catch {
|
|
2156
|
+
resolve5("unreachable");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const socket = connectToBrokerTarget2(target);
|
|
2160
|
+
const requestId = randomUUID3();
|
|
2161
|
+
const expectedStateId = typeof target === "string" ? void 0 : target.stateId;
|
|
2162
|
+
let settled = false;
|
|
2163
|
+
const finish = (health) => {
|
|
2164
|
+
if (settled) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
settled = true;
|
|
2168
|
+
clearTimeout(timeout);
|
|
2169
|
+
socket.off("connect", onConnect);
|
|
2170
|
+
socket.off("error", onError);
|
|
2171
|
+
socket.off("data", reader);
|
|
2172
|
+
socket.destroy();
|
|
2173
|
+
resolve5(health);
|
|
2174
|
+
};
|
|
2175
|
+
const onConnect = () => {
|
|
2176
|
+
try {
|
|
2177
|
+
writeMessage(socket, {
|
|
2178
|
+
type: "health",
|
|
2179
|
+
requestId,
|
|
2180
|
+
...expectedStateId ? { stateId: expectedStateId } : {}
|
|
2181
|
+
});
|
|
2182
|
+
} catch {
|
|
2183
|
+
finish("unreachable");
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
const onError = () => finish("unreachable");
|
|
2187
|
+
const reader = createMessageReader((message) => {
|
|
2188
|
+
if (isBrokerHealthOkMessage(message, requestId)) {
|
|
2189
|
+
finish("compatible");
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (isPlainDataRecord(message) && message.type === "health_ok" && message.requestId === requestId) {
|
|
2193
|
+
finish("incompatible");
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
finish("unreachable");
|
|
2197
|
+
}, () => finish("unreachable"));
|
|
2198
|
+
socket.on("connect", onConnect);
|
|
2199
|
+
socket.on("error", onError);
|
|
2200
|
+
socket.on("data", reader);
|
|
2201
|
+
const timeout = setTimeout(() => finish("unreachable"), 1e3);
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
function acquireSpawnLock() {
|
|
2205
|
+
const maxRetries = 5;
|
|
2206
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
2207
|
+
try {
|
|
2208
|
+
writeFileSync3(BROKER_SPAWN_LOCK, `${process.pid}
|
|
2209
|
+
${Date.now()}
|
|
2210
|
+
`, {
|
|
2211
|
+
flag: "wx",
|
|
2212
|
+
mode: INTERCOM_RUNTIME_FILE_MODE
|
|
2213
|
+
});
|
|
2214
|
+
restrictIntercomRuntimeFile(BROKER_SPAWN_LOCK);
|
|
2215
|
+
return true;
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
if (!(error instanceof Error) || error.code !== "EEXIST") {
|
|
2218
|
+
throw error;
|
|
2219
|
+
}
|
|
2220
|
+
if (isSpawnLockStale()) {
|
|
2221
|
+
try {
|
|
2222
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
2223
|
+
} catch {
|
|
2224
|
+
}
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
return false;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
function isSpawnLockStale() {
|
|
2233
|
+
if (!existsSync3(BROKER_SPAWN_LOCK)) {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
try {
|
|
2237
|
+
const [pidLine = "", createdAtLine = "0"] = readFileSync5(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
|
|
2238
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
2239
|
+
const createdAt = Number.parseInt(createdAtLine, 10);
|
|
2240
|
+
const ageMs = Date.now() - createdAt;
|
|
2241
|
+
if (Number.isFinite(pid)) {
|
|
2242
|
+
try {
|
|
2243
|
+
process.kill(pid, 0);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return true;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return !Number.isFinite(createdAt) || ageMs > 1e4;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function releaseSpawnLock() {
|
|
2254
|
+
try {
|
|
2255
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
async function waitForBroker(timeoutMs = 5e3) {
|
|
2260
|
+
const start = Date.now();
|
|
2261
|
+
while (Date.now() - start < timeoutMs) {
|
|
2262
|
+
if (await checkSocketConnectable()) {
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
await sleep(100);
|
|
2266
|
+
}
|
|
2267
|
+
throw new Error("Broker failed to start within timeout");
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// config.ts
|
|
2271
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
2272
|
+
import { join as join6, resolve as resolve3 } from "path";
|
|
2273
|
+
import { homedir as homedir2 } from "os";
|
|
2274
|
+
var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
|
|
2275
|
+
var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
|
|
2276
|
+
function getConfigPath() {
|
|
2277
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ? resolve3(process.env.PI_CODING_AGENT_DIR) : join6(homedir2(), ".pi", "agent");
|
|
2278
|
+
return join6(agentDir, "intercom", "config.json");
|
|
2279
|
+
}
|
|
2280
|
+
var defaults = {
|
|
2281
|
+
brokerCommand: "npx",
|
|
2282
|
+
brokerArgs: ["--no-install", "tsx"],
|
|
2283
|
+
confirmSend: false,
|
|
2284
|
+
enabled: true,
|
|
2285
|
+
replyHint: true,
|
|
2286
|
+
inboundForkHandlers: {
|
|
2287
|
+
enabled: true,
|
|
2288
|
+
when: "auto",
|
|
2289
|
+
notify: "summary",
|
|
2290
|
+
triggerParentOnSummary: "auto"
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
function loadConfig() {
|
|
2294
|
+
const configPath = getConfigPath();
|
|
2295
|
+
if (!existsSync4(configPath)) {
|
|
2296
|
+
return { ...defaults };
|
|
2297
|
+
}
|
|
2298
|
+
try {
|
|
2299
|
+
const raw = readFileSync6(configPath, "utf-8");
|
|
2300
|
+
const parsed = JSON.parse(raw);
|
|
2301
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2302
|
+
throw new Error("Config must be a JSON object");
|
|
2303
|
+
}
|
|
2304
|
+
const parsedConfig = parsed;
|
|
2305
|
+
const config = { ...defaults };
|
|
2306
|
+
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
|
|
2307
|
+
if (typeof parsedConfig.brokerCommand !== "string") {
|
|
2308
|
+
throw new Error(`"brokerCommand" must be a string`);
|
|
2309
|
+
}
|
|
2310
|
+
const brokerCommand = parsedConfig.brokerCommand.trim();
|
|
2311
|
+
if (!brokerCommand) {
|
|
2312
|
+
throw new Error(`"brokerCommand" must not be empty`);
|
|
2313
|
+
}
|
|
2314
|
+
config.brokerCommand = brokerCommand;
|
|
2315
|
+
}
|
|
2316
|
+
if (Object.hasOwn(parsedConfig, "brokerArgs")) {
|
|
2317
|
+
if (!Array.isArray(parsedConfig.brokerArgs)) {
|
|
2318
|
+
throw new Error(`"brokerArgs" must be an array`);
|
|
2319
|
+
}
|
|
2320
|
+
const brokerArgs = [];
|
|
2321
|
+
for (const arg of parsedConfig.brokerArgs) {
|
|
2322
|
+
if (typeof arg !== "string") {
|
|
2323
|
+
throw new Error(`"brokerArgs" items must be strings`);
|
|
2324
|
+
}
|
|
2325
|
+
brokerArgs.push(arg);
|
|
2326
|
+
}
|
|
2327
|
+
config.brokerArgs = brokerArgs;
|
|
2328
|
+
}
|
|
2329
|
+
if (Object.hasOwn(parsedConfig, "confirmSend")) {
|
|
2330
|
+
if (typeof parsedConfig.confirmSend !== "boolean") {
|
|
2331
|
+
throw new Error(`"confirmSend" must be a boolean`);
|
|
2332
|
+
}
|
|
2333
|
+
config.confirmSend = parsedConfig.confirmSend;
|
|
2334
|
+
}
|
|
2335
|
+
if (Object.hasOwn(parsedConfig, "enabled")) {
|
|
2336
|
+
if (typeof parsedConfig.enabled !== "boolean") {
|
|
2337
|
+
throw new Error(`"enabled" must be a boolean`);
|
|
2338
|
+
}
|
|
2339
|
+
config.enabled = parsedConfig.enabled;
|
|
2340
|
+
}
|
|
2341
|
+
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
2342
|
+
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
2343
|
+
throw new Error(`"replyHint" must be a boolean`);
|
|
2344
|
+
}
|
|
2345
|
+
config.replyHint = parsedConfig.replyHint;
|
|
2346
|
+
}
|
|
2347
|
+
if (Object.hasOwn(parsedConfig, "status")) {
|
|
2348
|
+
if (typeof parsedConfig.status !== "string") {
|
|
2349
|
+
throw new Error(`"status" must be a string`);
|
|
2350
|
+
}
|
|
2351
|
+
config.status = parsedConfig.status;
|
|
2352
|
+
}
|
|
2353
|
+
if (Object.hasOwn(parsedConfig, "inboundForkHandlers")) {
|
|
2354
|
+
if (typeof parsedConfig.inboundForkHandlers !== "object" || parsedConfig.inboundForkHandlers === null || Array.isArray(parsedConfig.inboundForkHandlers)) {
|
|
2355
|
+
throw new Error(`"inboundForkHandlers" must be an object`);
|
|
2356
|
+
}
|
|
2357
|
+
const forkConfig = parsedConfig.inboundForkHandlers;
|
|
2358
|
+
config.inboundForkHandlers = { ...defaults.inboundForkHandlers };
|
|
2359
|
+
if (Object.hasOwn(forkConfig, "enabled")) {
|
|
2360
|
+
if (typeof forkConfig.enabled !== "boolean") throw new Error(`"inboundForkHandlers.enabled" must be a boolean`);
|
|
2361
|
+
config.inboundForkHandlers.enabled = forkConfig.enabled;
|
|
2362
|
+
}
|
|
2363
|
+
if (Object.hasOwn(forkConfig, "when")) {
|
|
2364
|
+
if (forkConfig.when !== "auto" && forkConfig.when !== "busy" && forkConfig.when !== "always") throw new Error(`"inboundForkHandlers.when" must be "auto", "busy", or "always"`);
|
|
2365
|
+
config.inboundForkHandlers.when = forkConfig.when;
|
|
2366
|
+
}
|
|
2367
|
+
if (Object.hasOwn(forkConfig, "notify")) {
|
|
2368
|
+
if (forkConfig.notify !== "ack-and-summary" && forkConfig.notify !== "summary" && forkConfig.notify !== "none") throw new Error(`"inboundForkHandlers.notify" must be "ack-and-summary", "summary", or "none"`);
|
|
2369
|
+
config.inboundForkHandlers.notify = forkConfig.notify;
|
|
2370
|
+
}
|
|
2371
|
+
if (Object.hasOwn(forkConfig, "piCommand")) {
|
|
2372
|
+
if (typeof forkConfig.piCommand !== "string") throw new Error(`"inboundForkHandlers.piCommand" must be a string`);
|
|
2373
|
+
const piCommand = forkConfig.piCommand.trim();
|
|
2374
|
+
if (piCommand) config.inboundForkHandlers.piCommand = piCommand;
|
|
2375
|
+
}
|
|
2376
|
+
if (Object.hasOwn(forkConfig, "triggerParentOnSummary")) {
|
|
2377
|
+
const triggerParentOnSummary = forkConfig.triggerParentOnSummary;
|
|
2378
|
+
if (typeof triggerParentOnSummary !== "boolean" && triggerParentOnSummary !== "auto") {
|
|
2379
|
+
throw new Error(`"inboundForkHandlers.triggerParentOnSummary" must be a boolean or "auto"`);
|
|
2380
|
+
}
|
|
2381
|
+
config.inboundForkHandlers.triggerParentOnSummary = triggerParentOnSummary;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return config;
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
console.error(`Failed to load intercom config at ${configPath}:`, error);
|
|
2387
|
+
return { ...defaults };
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// claude/inbox.ts
|
|
2392
|
+
import { dirname as dirname5, join as join7 } from "node:path";
|
|
2393
|
+
function sanitizeInboxSegment(value) {
|
|
2394
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "session";
|
|
2395
|
+
}
|
|
2396
|
+
function defaultInboxPath(sessionId, intercomDir = getIntercomDirPath()) {
|
|
2397
|
+
return join7(intercomDir, `inbox-${sanitizeInboxSegment(sessionId)}.jsonl`);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// claude/team.ts
|
|
2401
|
+
import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
|
|
2402
|
+
var BOSS_NONTERMINAL_STATES = new Set(
|
|
2403
|
+
PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
// claude/runtime.ts
|
|
2407
|
+
function formatAttachments(attachments) {
|
|
2408
|
+
if (!attachments?.length) return "";
|
|
2409
|
+
return attachments.map((attachment) => {
|
|
2410
|
+
if (attachment.language) {
|
|
2411
|
+
return `
|
|
2412
|
+
|
|
2413
|
+
---
|
|
2414
|
+
Attachment: ${attachment.name}
|
|
2415
|
+
~~~${attachment.language}
|
|
2416
|
+
${attachment.content}
|
|
2417
|
+
~~~`;
|
|
2418
|
+
}
|
|
2419
|
+
return `
|
|
2420
|
+
|
|
2421
|
+
---
|
|
2422
|
+
Attachment: ${attachment.name}
|
|
2423
|
+
${attachment.content}`;
|
|
2424
|
+
}).join("");
|
|
2425
|
+
}
|
|
2426
|
+
function formatSessionDisplay(session) {
|
|
2427
|
+
const name = session.name || session.id;
|
|
2428
|
+
return session.origin === "remote" ? `${name} [remote:${session.remoteHostId || "unknown-host"}]` : name;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// claude/contact.ts
|
|
2432
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
|
|
2433
|
+
function formatContactInstruction(current, sessions) {
|
|
2434
|
+
const name = current.name?.trim();
|
|
2435
|
+
const duplicate = Boolean(name && sessions.some((session) => session.id !== current.id && session.name?.trim().toLowerCase() === name.toLowerCase()));
|
|
2436
|
+
return name && !duplicate ? `Intercom target: ${name}
|
|
2437
|
+
Stable session ID: ${current.id}` : `Intercom target: ${current.id}`;
|
|
2438
|
+
}
|
|
2439
|
+
function copyTextToClipboard(text) {
|
|
2440
|
+
const candidates = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip.exe", []]] : [
|
|
2441
|
+
...process.env.WAYLAND_DISPLAY ? [["wl-copy", []]] : [],
|
|
2442
|
+
...process.env.DISPLAY ? [["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]] : [],
|
|
2443
|
+
["clip.exe", []]
|
|
2444
|
+
];
|
|
2445
|
+
for (const [command, args] of candidates) {
|
|
2446
|
+
if (spawnSync2("which", [command], { stdio: "ignore" }).status !== 0) continue;
|
|
2447
|
+
if (command === "wl-copy") {
|
|
2448
|
+
const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
2449
|
+
child.stdin.end(text);
|
|
2450
|
+
child.unref();
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
|
|
2454
|
+
}
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// claude/worker-daemon.ts
|
|
2459
|
+
var MAX_WAKES_PER_MINUTE = 20;
|
|
2460
|
+
function formatWorkerActivity(activity) {
|
|
2461
|
+
const sender = formatSessionDisplay(activity.from);
|
|
2462
|
+
if (activity.type === "started") {
|
|
2463
|
+
return `
|
|
2464
|
+
[intercom] Wake from ${sender}: ${activity.message.content.text}
|
|
2465
|
+
[intercom] Claude is working\u2026
|
|
2466
|
+
`;
|
|
2467
|
+
}
|
|
2468
|
+
if (activity.type === "completed") {
|
|
2469
|
+
const result = activity.result || "(Claude finished without a final message)";
|
|
2470
|
+
return `[intercom] Completed wake from ${sender} (session ${activity.sessionId ?? "none"})
|
|
2471
|
+
${result}
|
|
2472
|
+
`;
|
|
2473
|
+
}
|
|
2474
|
+
return `[intercom] Wake from ${sender} failed: ${activity.error}
|
|
2475
|
+
`;
|
|
2476
|
+
}
|
|
2477
|
+
function reportToTerminal(activity) {
|
|
2478
|
+
process.stderr.write(formatWorkerActivity(activity));
|
|
2479
|
+
}
|
|
2480
|
+
function formatMessage(from, message, agent) {
|
|
2481
|
+
const replyInstruction = message.expectsReply ? [
|
|
2482
|
+
"",
|
|
2483
|
+
"",
|
|
2484
|
+
"The sender is blocking on your reply.",
|
|
2485
|
+
"Your FINAL assistant message will be sent back to them automatically as the reply.",
|
|
2486
|
+
"Do not try to use intercom tools to answer; just put your answer in your final message."
|
|
2487
|
+
].join("\n") : "";
|
|
2488
|
+
const attachments = formatAttachments(message.content.attachments);
|
|
2489
|
+
const custom = agent.instructions ? `
|
|
2490
|
+
|
|
2491
|
+
Agent instructions:
|
|
2492
|
+
${agent.instructions}` : "";
|
|
2493
|
+
return [
|
|
2494
|
+
`Intercom message for ${agent.name}.`,
|
|
2495
|
+
`From: ${formatSessionDisplay(from)} (${from.id})`,
|
|
2496
|
+
`Message id: ${message.id}`,
|
|
2497
|
+
"",
|
|
2498
|
+
message.content.text,
|
|
2499
|
+
attachments,
|
|
2500
|
+
custom,
|
|
2501
|
+
replyInstruction
|
|
2502
|
+
].join("\n");
|
|
2503
|
+
}
|
|
2504
|
+
var VirtualClaudeAgent = class {
|
|
2505
|
+
constructor(agent, state, statePath, claudeCommand, reportActivity = reportToTerminal, runTurn = runClaudeTurn, options = {}) {
|
|
2506
|
+
this.agent = agent;
|
|
2507
|
+
this.state = state;
|
|
2508
|
+
this.statePath = statePath;
|
|
2509
|
+
this.claudeCommand = claudeCommand;
|
|
2510
|
+
this.reportActivity = reportActivity;
|
|
2511
|
+
this.runTurn = runTurn;
|
|
2512
|
+
const scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
2513
|
+
const scopeEnv = scopeId === void 0 ? {} : { [INTERCOM_SCOPE_ENV]: scopeId };
|
|
2514
|
+
this.client = options.client ?? new IntercomClient({ env: scopeEnv });
|
|
2515
|
+
this.sessionId = agent.sessionId ?? state.agents[agent.id]?.sessionId ?? null;
|
|
2516
|
+
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
2517
|
+
const config = loadConfig();
|
|
2518
|
+
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
2519
|
+
});
|
|
2520
|
+
this.reconnectDelays = options.reconnectDelays?.length ? options.reconnectDelays : [250, 500, 1e3, 2e3, 5e3];
|
|
2521
|
+
}
|
|
2522
|
+
agent;
|
|
2523
|
+
state;
|
|
2524
|
+
statePath;
|
|
2525
|
+
claudeCommand;
|
|
2526
|
+
reportActivity;
|
|
2527
|
+
runTurn;
|
|
2528
|
+
client;
|
|
2529
|
+
sessionId;
|
|
2530
|
+
messageQueue = Promise.resolve();
|
|
2531
|
+
wakeTimestamps = [];
|
|
2532
|
+
connectPromise = null;
|
|
2533
|
+
reconnectTimer = null;
|
|
2534
|
+
reconnectAttempt = 0;
|
|
2535
|
+
reconnectEnabled = true;
|
|
2536
|
+
intercomStartedAt = Date.now();
|
|
2537
|
+
prepareConnection;
|
|
2538
|
+
reconnectDelays;
|
|
2539
|
+
get id() {
|
|
2540
|
+
return this.agent.id;
|
|
2541
|
+
}
|
|
2542
|
+
async copyContact() {
|
|
2543
|
+
const sessions = await this.client.listSessions();
|
|
2544
|
+
const current = sessions.find((session) => session.id === this.client.sessionId);
|
|
2545
|
+
const text = current ? formatContactInstruction(current, sessions) : `Intercom target: ${this.client.sessionId ?? this.agent.id}`;
|
|
2546
|
+
return copyTextToClipboard(text) ? `Copied ${text.replace(/\n/g, "; ")}` : `Clipboard unavailable. ${text.replace(/\n/g, "; ")}`;
|
|
2547
|
+
}
|
|
2548
|
+
async listPeers() {
|
|
2549
|
+
const sessions = await this.client.listSessions();
|
|
2550
|
+
return sessions.filter((session) => session.id !== this.client.sessionId);
|
|
2551
|
+
}
|
|
2552
|
+
async sendMessage(to, text) {
|
|
2553
|
+
const sessions = await this.client.listSessions();
|
|
2554
|
+
const normalized = to.toLowerCase();
|
|
2555
|
+
const matches = sessions.filter(
|
|
2556
|
+
(session) => session.id === to || session.id.startsWith(to) || session.name?.toLowerCase() === normalized
|
|
2557
|
+
);
|
|
2558
|
+
if (matches.length > 1) {
|
|
2559
|
+
throw new Error(`Multiple intercom sessions match "${to}"; choose a full ID.`);
|
|
2560
|
+
}
|
|
2561
|
+
const target = matches[0]?.id ?? to;
|
|
2562
|
+
const result = await this.client.send(target, { text });
|
|
2563
|
+
if (!result.delivered) {
|
|
2564
|
+
throw new Error(result.reason ?? `Intercom session "${to}" is unavailable.`);
|
|
2565
|
+
}
|
|
2566
|
+
return `Message sent to ${matches[0]?.name || to}.`;
|
|
2567
|
+
}
|
|
2568
|
+
async start() {
|
|
2569
|
+
this.reconnectEnabled = true;
|
|
2570
|
+
this.client.on("message", (from, message, deliveryId) => {
|
|
2571
|
+
this.routeMessage(from, message);
|
|
2572
|
+
this.client.acknowledgeMessage(deliveryId);
|
|
2573
|
+
});
|
|
2574
|
+
this.client.on("error", (error) => {
|
|
2575
|
+
process.stderr.write(`worker ${this.agent.id}: ${error.message}
|
|
2576
|
+
`);
|
|
2577
|
+
});
|
|
2578
|
+
this.client.on("disconnected", () => {
|
|
2579
|
+
this.scheduleReconnect();
|
|
2580
|
+
});
|
|
2581
|
+
await this.connectIntercom();
|
|
2582
|
+
}
|
|
2583
|
+
async connectIntercom() {
|
|
2584
|
+
this.clearReconnectTimer();
|
|
2585
|
+
if (this.client.isConnected()) return;
|
|
2586
|
+
if (this.connectPromise) return this.connectPromise;
|
|
2587
|
+
this.connectPromise = (async () => {
|
|
2588
|
+
await this.prepareConnection();
|
|
2589
|
+
await this.client.connect({
|
|
2590
|
+
name: this.agent.name,
|
|
2591
|
+
cwd: this.agent.cwd,
|
|
2592
|
+
model: this.agent.model ?? "claude",
|
|
2593
|
+
pid: process.pid,
|
|
2594
|
+
startedAt: this.intercomStartedAt,
|
|
2595
|
+
lastActivity: Date.now(),
|
|
2596
|
+
status: "idle"
|
|
2597
|
+
}, this.agent.id);
|
|
2598
|
+
this.reconnectAttempt = 0;
|
|
2599
|
+
})();
|
|
2600
|
+
try {
|
|
2601
|
+
await this.connectPromise;
|
|
2602
|
+
} finally {
|
|
2603
|
+
this.connectPromise = null;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
scheduleReconnect() {
|
|
2607
|
+
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
2608
|
+
const delay = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
2609
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2610
|
+
this.reconnectTimer = null;
|
|
2611
|
+
void this.connectIntercom().then(() => {
|
|
2612
|
+
if (!this.client.isConnected()) {
|
|
2613
|
+
this.reconnectAttempt += 1;
|
|
2614
|
+
this.scheduleReconnect();
|
|
2615
|
+
}
|
|
2616
|
+
}).catch((error) => {
|
|
2617
|
+
this.reconnectAttempt += 1;
|
|
2618
|
+
process.stderr.write(`worker ${this.agent.id}: reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2619
|
+
`);
|
|
2620
|
+
this.scheduleReconnect();
|
|
2621
|
+
});
|
|
2622
|
+
}, delay);
|
|
2623
|
+
this.reconnectTimer.unref?.();
|
|
2624
|
+
}
|
|
2625
|
+
clearReconnectTimer() {
|
|
2626
|
+
if (!this.reconnectTimer) return;
|
|
2627
|
+
clearTimeout(this.reconnectTimer);
|
|
2628
|
+
this.reconnectTimer = null;
|
|
2629
|
+
}
|
|
2630
|
+
async stop() {
|
|
2631
|
+
this.reconnectEnabled = false;
|
|
2632
|
+
this.clearReconnectTimer();
|
|
2633
|
+
if (this.connectPromise) {
|
|
2634
|
+
try {
|
|
2635
|
+
await this.connectPromise;
|
|
2636
|
+
} catch {
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
await this.client.disconnect();
|
|
2640
|
+
}
|
|
2641
|
+
routeMessage(from, message) {
|
|
2642
|
+
if (this.isRateLimited()) {
|
|
2643
|
+
process.stderr.write(`worker ${this.agent.id}: rate limited, dropping message from ${from.id}
|
|
2644
|
+
`);
|
|
2645
|
+
if (message.expectsReply) {
|
|
2646
|
+
void this.client.send(from.id, {
|
|
2647
|
+
text: "This worker is rate limited (too many wakes this minute). Please try again shortly.",
|
|
2648
|
+
replyTo: message.id
|
|
2649
|
+
}).catch((error) => {
|
|
2650
|
+
process.stderr.write(`worker ${this.agent.id}: rate-limit reply failed: ${error instanceof Error ? error.message : String(error)}
|
|
2651
|
+
`);
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
this.messageQueue = this.messageQueue.catch(() => void 0).then(() => this.handleMessage(from, message));
|
|
2657
|
+
}
|
|
2658
|
+
isRateLimited() {
|
|
2659
|
+
const now = Date.now();
|
|
2660
|
+
this.wakeTimestamps = this.wakeTimestamps.filter((timestamp) => now - timestamp < 6e4);
|
|
2661
|
+
if (this.wakeTimestamps.length >= MAX_WAKES_PER_MINUTE) return true;
|
|
2662
|
+
this.wakeTimestamps.push(now);
|
|
2663
|
+
return false;
|
|
2664
|
+
}
|
|
2665
|
+
async handleMessage(from, message) {
|
|
2666
|
+
try {
|
|
2667
|
+
const prompt = formatMessage(from, message, this.agent);
|
|
2668
|
+
this.client.updatePresence({ status: "active" });
|
|
2669
|
+
this.reportActivity({ type: "started", agent: this.agent, from, message });
|
|
2670
|
+
const result = await this.runTurn({
|
|
2671
|
+
prompt,
|
|
2672
|
+
cwd: this.agent.cwd,
|
|
2673
|
+
sessionId: this.sessionId ?? void 0,
|
|
2674
|
+
model: this.agent.model,
|
|
2675
|
+
appendSystemPrompt: this.agent.instructions,
|
|
2676
|
+
// Historical worker configs omitted both permission fields. Preserve
|
|
2677
|
+
// their noninteractive behavior; hardened roles are normalized to an
|
|
2678
|
+
// explicit plan mode before a VirtualClaudeAgent can be constructed.
|
|
2679
|
+
permissionMode: this.agent.permissionMode ?? "bypassPermissions",
|
|
2680
|
+
dangerouslySkipPermissions: this.agent.dangerouslySkipPermissions,
|
|
2681
|
+
permissionCeiling: this.agent.permissionCeiling,
|
|
2682
|
+
addDirs: this.agent.addDirs,
|
|
2683
|
+
mcpConfig: this.agent.mcpConfig,
|
|
2684
|
+
claudeCommand: this.claudeCommand,
|
|
2685
|
+
extraArgs: this.agent.claudeArgs
|
|
2686
|
+
});
|
|
2687
|
+
if (result.sessionId && result.sessionId !== this.sessionId) {
|
|
2688
|
+
this.sessionId = result.sessionId;
|
|
2689
|
+
this.state.agents[this.agent.id] = { sessionId: result.sessionId, updatedAt: Date.now() };
|
|
2690
|
+
saveWorkerState(this.statePath, this.state);
|
|
2691
|
+
}
|
|
2692
|
+
if (result.isError) {
|
|
2693
|
+
const text = result.result || "Claude exited with an error and no message";
|
|
2694
|
+
this.client.updatePresence({ status: `error: ${text}` });
|
|
2695
|
+
this.reportActivity({ type: "error", agent: this.agent, from, message, error: text });
|
|
2696
|
+
} else {
|
|
2697
|
+
this.client.updatePresence({ status: "idle" });
|
|
2698
|
+
this.reportActivity({
|
|
2699
|
+
type: "completed",
|
|
2700
|
+
agent: this.agent,
|
|
2701
|
+
from,
|
|
2702
|
+
message,
|
|
2703
|
+
result: result.result,
|
|
2704
|
+
sessionId: this.sessionId
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
if (message.expectsReply) {
|
|
2708
|
+
await this.client.send(from.id, {
|
|
2709
|
+
text: result.result || "(the worker finished without a final message)",
|
|
2710
|
+
replyTo: message.id
|
|
2711
|
+
});
|
|
2712
|
+
process.stderr.write(`worker ${this.agent.id}: replied to ${from.name || from.id} (session ${this.sessionId ?? "none"})
|
|
2713
|
+
`);
|
|
2714
|
+
}
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
2717
|
+
this.client.updatePresence({ status: `error: ${text}` });
|
|
2718
|
+
this.reportActivity({ type: "error", agent: this.agent, from, message, error: text });
|
|
2719
|
+
if (message.expectsReply) {
|
|
2720
|
+
await this.client.send(from.id, {
|
|
2721
|
+
text: `Worker error: ${text}`,
|
|
2722
|
+
replyTo: message.id
|
|
2723
|
+
}).catch((sendError) => {
|
|
2724
|
+
process.stderr.write(`worker ${this.agent.id}: error reply failed: ${sendError instanceof Error ? sendError.message : String(sendError)}
|
|
2725
|
+
`);
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
var ClaudeWorkerDaemon = class {
|
|
2732
|
+
constructor(config, reportActivity = reportToTerminal, options = {}) {
|
|
2733
|
+
this.config = config;
|
|
2734
|
+
this.reportActivity = reportActivity;
|
|
2735
|
+
this.scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
2736
|
+
}
|
|
2737
|
+
config;
|
|
2738
|
+
reportActivity;
|
|
2739
|
+
agents = [];
|
|
2740
|
+
scopeId;
|
|
2741
|
+
async start() {
|
|
2742
|
+
const intercomConfig = loadConfig();
|
|
2743
|
+
await spawnBrokerIfNeeded(intercomConfig.brokerCommand, intercomConfig.brokerArgs);
|
|
2744
|
+
const state = loadWorkerState(this.config.statePath);
|
|
2745
|
+
const claudeCommand = this.config.claudeCommand ?? "claude";
|
|
2746
|
+
this.agents = this.config.agents.map((agent) => {
|
|
2747
|
+
const transport = resolveClaudeIntercomTransport({
|
|
2748
|
+
requested: agent.transport,
|
|
2749
|
+
claudeCommand
|
|
2750
|
+
});
|
|
2751
|
+
process.stderr.write(`worker ${agent.id}: ${transport.selected} transport (${transport.reason})
|
|
2752
|
+
`);
|
|
2753
|
+
return new VirtualClaudeAgent(
|
|
2754
|
+
{ ...agent, transport: transport.selected },
|
|
2755
|
+
state,
|
|
2756
|
+
this.config.statePath,
|
|
2757
|
+
claudeCommand,
|
|
2758
|
+
this.reportActivity,
|
|
2759
|
+
runClaudeTurn,
|
|
2760
|
+
{ scopeId: this.scopeId }
|
|
2761
|
+
);
|
|
2762
|
+
});
|
|
2763
|
+
for (const agent of this.agents) await agent.start();
|
|
2764
|
+
process.stderr.write(`claude-intercom worker running ${this.agents.length} agent(s)
|
|
2765
|
+
`);
|
|
2766
|
+
}
|
|
2767
|
+
async stop() {
|
|
2768
|
+
for (const agent of this.agents) await agent.stop().catch(() => void 0);
|
|
2769
|
+
}
|
|
2770
|
+
async copyPrimaryContact() {
|
|
2771
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2772
|
+
return this.agents[0].copyContact();
|
|
2773
|
+
}
|
|
2774
|
+
async listPrimaryPeers() {
|
|
2775
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2776
|
+
return this.agents[0].listPeers();
|
|
2777
|
+
}
|
|
2778
|
+
async sendFromPrimary(to, text) {
|
|
2779
|
+
if (!this.agents[0]) throw new Error("No Claude intercom agent is running");
|
|
2780
|
+
return this.agents[0].sendMessage(to, text);
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
async function main() {
|
|
2784
|
+
intercomScopeIdFromEnvForRegistration(process.env);
|
|
2785
|
+
const configPath = process.argv.includes("--config") ? process.argv[process.argv.indexOf("--config") + 1] : void 0;
|
|
2786
|
+
const config = loadWorkerConfig(configPath);
|
|
2787
|
+
if (!config.agents.length) throw new Error("Worker config must include at least one agent");
|
|
2788
|
+
const daemon = new ClaudeWorkerDaemon(config);
|
|
2789
|
+
const stop = () => {
|
|
2790
|
+
void daemon.stop().finally(() => process.exit(0));
|
|
2791
|
+
};
|
|
2792
|
+
process.on("SIGINT", stop);
|
|
2793
|
+
process.on("SIGTERM", stop);
|
|
2794
|
+
await daemon.start();
|
|
2795
|
+
await once(process, "SIGTERM");
|
|
2796
|
+
}
|
|
2797
|
+
function invokedFileBasename() {
|
|
2798
|
+
try {
|
|
2799
|
+
return process.argv[1] ? basename(realpathSync2(process.argv[1])) : "";
|
|
2800
|
+
} catch {
|
|
2801
|
+
return process.argv[1] ? basename(process.argv[1]) : "";
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (invokedFileBasename() === "worker-daemon.ts" || invokedFileBasename() === "worker-daemon.mjs") {
|
|
2805
|
+
void main().catch((error) => {
|
|
2806
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
2807
|
+
`);
|
|
2808
|
+
process.exit(1);
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// claude/native-bridge.ts
|
|
2813
|
+
import { join as join9 } from "node:path";
|
|
2814
|
+
|
|
2815
|
+
// claude/native-protocol.ts
|
|
2816
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2817
|
+
import { execFile } from "node:child_process";
|
|
2818
|
+
import { chmod, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2819
|
+
import { readFileSync as readFileSync7, readdirSync } from "node:fs";
|
|
2820
|
+
import { connect, createServer } from "node:net";
|
|
2821
|
+
import { homedir as homedir3 } from "node:os";
|
|
2822
|
+
import { basename as basename2, dirname as dirname6, join as join8 } from "node:path";
|
|
2823
|
+
function claudeNativeRegistryDir(env = process.env, home = homedir3()) {
|
|
2824
|
+
return join8(env.CLAUDE_CONFIG_DIR || join8(home, ".claude"), "sessions");
|
|
2825
|
+
}
|
|
2826
|
+
var DEFAULT_CLAUDE_SESSION_REGISTRY = claudeNativeRegistryDir();
|
|
2827
|
+
var MAX_NATIVE_FRAME_LINE = 1024 * 1024;
|
|
2828
|
+
function claudeNativeSocketDir(paths = {}) {
|
|
2829
|
+
if (paths.socketDir) return paths.socketDir;
|
|
2830
|
+
const registryDir = paths.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
|
|
2831
|
+
try {
|
|
2832
|
+
for (const file of readdirSync(registryDir)) {
|
|
2833
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
2834
|
+
let entry;
|
|
2835
|
+
try {
|
|
2836
|
+
entry = JSON.parse(readFileSync7(join8(registryDir, file), "utf8"));
|
|
2837
|
+
} catch {
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
if (typeof entry.messagingSocketPath === "string" && entry.messagingSocketPath.endsWith(".sock")) {
|
|
2841
|
+
return dirname6(entry.messagingSocketPath);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
return join8(process.env.XDG_RUNTIME_DIR || "/tmp", "cc-socks");
|
|
2847
|
+
}
|
|
2848
|
+
function nativeSocketLive(socketPath, timeoutMs = 250) {
|
|
2849
|
+
return new Promise((resolve5) => {
|
|
2850
|
+
if (!socketPath) return resolve5(false);
|
|
2851
|
+
const socket = connect({ path: socketPath });
|
|
2852
|
+
let settled = false;
|
|
2853
|
+
const done = (live) => {
|
|
2854
|
+
if (settled) return;
|
|
2855
|
+
settled = true;
|
|
2856
|
+
socket.destroy();
|
|
2857
|
+
resolve5(live);
|
|
2858
|
+
};
|
|
2859
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
2860
|
+
socket.on("connect", () => done(true));
|
|
2861
|
+
socket.on("error", () => done(false));
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
async function listNativeClaudeSessions(options = {}) {
|
|
2865
|
+
const registryDir = options.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
|
|
2866
|
+
let files;
|
|
2867
|
+
try {
|
|
2868
|
+
files = await readdir(registryDir);
|
|
2869
|
+
} catch {
|
|
2870
|
+
return [];
|
|
2871
|
+
}
|
|
2872
|
+
const peers = [];
|
|
2873
|
+
for (const file of files) {
|
|
2874
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
2875
|
+
let entry;
|
|
2876
|
+
try {
|
|
2877
|
+
entry = JSON.parse(await readFile(join8(registryDir, file), "utf8"));
|
|
2878
|
+
} catch {
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
const socketPath = typeof entry.messagingSocketPath === "string" ? entry.messagingSocketPath : "";
|
|
2882
|
+
if (!socketPath || socketPath === options.excludeSocket) continue;
|
|
2883
|
+
peers.push({
|
|
2884
|
+
pid: typeof entry.pid === "number" ? entry.pid : void 0,
|
|
2885
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : void 0,
|
|
2886
|
+
name: typeof entry.name === "string" ? entry.name : `pid ${String(entry.pid ?? "unknown")}`,
|
|
2887
|
+
cwd: typeof entry.cwd === "string" ? entry.cwd : "?",
|
|
2888
|
+
status: typeof entry.status === "string" ? entry.status : "unknown",
|
|
2889
|
+
kind: typeof entry.kind === "string" ? entry.kind : void 0,
|
|
2890
|
+
startedAt: typeof entry.startedAt === "number" ? entry.startedAt : void 0,
|
|
2891
|
+
socketPath
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
await Promise.all(peers.map(async (peer) => {
|
|
2895
|
+
peer.live = await nativeSocketLive(peer.socketPath);
|
|
2896
|
+
}));
|
|
2897
|
+
return peers.filter((peer) => peer.live).sort((left, right) => (right.startedAt ?? 0) - (left.startedAt ?? 0));
|
|
2898
|
+
}
|
|
2899
|
+
async function waitForNativeClaudePeer(pid, options = {}) {
|
|
2900
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
2901
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
2902
|
+
const deadline = Date.now() + timeoutMs;
|
|
2903
|
+
do {
|
|
2904
|
+
const peer = (await listNativeClaudeSessions(options)).find((candidate) => candidate.pid === pid);
|
|
2905
|
+
if (peer) return peer;
|
|
2906
|
+
if (Date.now() >= deadline) break;
|
|
2907
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
2908
|
+
} while (true);
|
|
2909
|
+
throw new Error(`Claude process ${pid} did not publish a live native messaging socket within ${timeoutMs}ms`);
|
|
2910
|
+
}
|
|
2911
|
+
var ENVELOPE_TAG = "cross-session-message";
|
|
2912
|
+
var escapeEnvelopeBody = (body) => body.replace(new RegExp(`</(?=${ENVELOPE_TAG}(?:[>\\s/]|$))`, "gi"), "<\\/");
|
|
2913
|
+
var unescapeEnvelopeBody = (body) => body.replace(new RegExp(`<\\\\/(?=${ENVELOPE_TAG}(?:[>\\s/]|$))`, "gi"), "</");
|
|
2914
|
+
function buildNativeEnvelope(options) {
|
|
2915
|
+
const attributes = [];
|
|
2916
|
+
if (options.from) attributes.push(`from="${options.from}"`);
|
|
2917
|
+
if (options.fromName) attributes.push(`from-name="${options.fromName.replace(/["<>]/g, "")}"`);
|
|
2918
|
+
if (options.fromMode) attributes.push(`from-mode="${options.fromMode}"`);
|
|
2919
|
+
return `<${ENVELOPE_TAG}${attributes.length ? ` ${attributes.join(" ")}` : ""}>
|
|
2920
|
+
${escapeEnvelopeBody(options.body)}
|
|
2921
|
+
</${ENVELOPE_TAG}>`;
|
|
2922
|
+
}
|
|
2923
|
+
function stripNativeEnvelope(content) {
|
|
2924
|
+
if (typeof content !== "string") return { body: "" };
|
|
2925
|
+
const match = content.match(new RegExp(`^<${ENVELOPE_TAG}((?:\\s+[a-z-]+="[^"]*")*)>\\n([\\s\\S]*)\\n</${ENVELOPE_TAG}>$`));
|
|
2926
|
+
if (!match) return { body: content };
|
|
2927
|
+
const attributes = {};
|
|
2928
|
+
for (const attribute of match[1].matchAll(/([a-z-]+)="([^"]*)"/g)) attributes[attribute[1]] = attribute[2];
|
|
2929
|
+
return {
|
|
2930
|
+
body: unescapeEnvelopeBody(match[2]),
|
|
2931
|
+
from: attributes.from,
|
|
2932
|
+
fromName: attributes["from-name"],
|
|
2933
|
+
fromMode: attributes["from-mode"]
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
function buildNativeUserFrame(options) {
|
|
2937
|
+
return {
|
|
2938
|
+
msgV: 1,
|
|
2939
|
+
msg_id: randomUUID4(),
|
|
2940
|
+
type: "user",
|
|
2941
|
+
priority: options.priority || "next",
|
|
2942
|
+
...options.from ? { from: options.from } : {},
|
|
2943
|
+
...options.sessionId ? { session_id: options.sessionId } : {},
|
|
2944
|
+
message: { role: "user", content: options.content }
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
function sendNativeFrame(socketPath, frame, timeoutMs = 5e3) {
|
|
2948
|
+
return new Promise((resolve5, reject) => {
|
|
2949
|
+
const socket = connect({ path: socketPath });
|
|
2950
|
+
socket.setTimeout(timeoutMs, () => {
|
|
2951
|
+
socket.destroy();
|
|
2952
|
+
reject(new Error(`timed out connecting to ${socketPath}`));
|
|
2953
|
+
});
|
|
2954
|
+
socket.once("error", reject);
|
|
2955
|
+
socket.once("connect", () => socket.end(`${JSON.stringify(frame)}
|
|
2956
|
+
`, () => {
|
|
2957
|
+
const id = typeof frame === "object" && frame !== null && "msg_id" in frame ? String(frame.msg_id) : "";
|
|
2958
|
+
resolve5(id);
|
|
2959
|
+
}));
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
function sendNativeClaudeMessage(options) {
|
|
2963
|
+
const content = buildNativeEnvelope({ from: options.from, fromName: options.fromName, body: options.body });
|
|
2964
|
+
return sendNativeFrame(options.socketPath, buildNativeUserFrame({ content, from: options.from, priority: options.priority }));
|
|
2965
|
+
}
|
|
2966
|
+
function buildNativeReceipt(options) {
|
|
2967
|
+
return {
|
|
2968
|
+
msgV: 1,
|
|
2969
|
+
msg_id: randomUUID4(),
|
|
2970
|
+
type: "control",
|
|
2971
|
+
action: "peer_message_status",
|
|
2972
|
+
status: options.status,
|
|
2973
|
+
...options.reason ? { reason: options.reason } : {},
|
|
2974
|
+
...options.from ? { from: options.from } : {},
|
|
2975
|
+
...options.originalMessageId ? { orig_msg_id: options.originalMessageId } : {}
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
async function bindNativeClaudeSocket(socketPath, onFrame) {
|
|
2979
|
+
const directory = dirname6(socketPath);
|
|
2980
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
2981
|
+
await chmod(directory, 448).catch(() => void 0);
|
|
2982
|
+
await unlink(socketPath).catch(() => void 0);
|
|
2983
|
+
const server = createServer((connection) => {
|
|
2984
|
+
connection.setEncoding("utf8");
|
|
2985
|
+
let buffer = "";
|
|
2986
|
+
connection.on("data", (chunk) => {
|
|
2987
|
+
buffer += chunk;
|
|
2988
|
+
if (buffer.length > MAX_NATIVE_FRAME_LINE) {
|
|
2989
|
+
buffer = "";
|
|
2990
|
+
connection.destroy();
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
let newline;
|
|
2994
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
2995
|
+
const line = buffer.slice(0, newline);
|
|
2996
|
+
buffer = buffer.slice(newline + 1);
|
|
2997
|
+
if (!line.trim()) continue;
|
|
2998
|
+
try {
|
|
2999
|
+
const frame = JSON.parse(line);
|
|
3000
|
+
onFrame(frame, connection);
|
|
3001
|
+
} catch {
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
connection.on("end", () => connection.end());
|
|
3006
|
+
connection.on("error", () => void 0);
|
|
3007
|
+
});
|
|
3008
|
+
await new Promise((resolve5, reject) => {
|
|
3009
|
+
server.once("error", reject);
|
|
3010
|
+
server.listen(socketPath, () => {
|
|
3011
|
+
server.removeListener("error", reject);
|
|
3012
|
+
resolve5();
|
|
3013
|
+
});
|
|
3014
|
+
});
|
|
3015
|
+
await chmod(socketPath, 384).catch(() => void 0);
|
|
3016
|
+
return server;
|
|
3017
|
+
}
|
|
3018
|
+
function processStart(pid) {
|
|
3019
|
+
return new Promise((resolve5) => {
|
|
3020
|
+
execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => resolve5(error ? void 0 : stdout.trim() || void 0));
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
async function registerNativeClaudePeer(options) {
|
|
3024
|
+
const registryDir = options.registryDir ?? DEFAULT_CLAUDE_SESSION_REGISTRY;
|
|
3025
|
+
await mkdir(registryDir, { recursive: true, mode: 448 });
|
|
3026
|
+
const sessionId = options.sessionId || randomUUID4();
|
|
3027
|
+
const entry = {
|
|
3028
|
+
pid: options.pid,
|
|
3029
|
+
sessionId,
|
|
3030
|
+
cwd: options.cwd || process.cwd(),
|
|
3031
|
+
startedAt: Date.now(),
|
|
3032
|
+
procStart: await processStart(options.pid),
|
|
3033
|
+
version: "agent-intercom-claude",
|
|
3034
|
+
peerProtocol: 1,
|
|
3035
|
+
kind: "interactive",
|
|
3036
|
+
entrypoint: "agent-intercom",
|
|
3037
|
+
messagingSocketPath: options.socketPath,
|
|
3038
|
+
name: options.name,
|
|
3039
|
+
nameSource: "explicit",
|
|
3040
|
+
status: options.status || "idle"
|
|
3041
|
+
};
|
|
3042
|
+
await writeFile(join8(registryDir, `${options.pid}.json`), `${JSON.stringify(entry, null, 2)}
|
|
3043
|
+
`, { mode: 384 });
|
|
3044
|
+
return sessionId;
|
|
3045
|
+
}
|
|
3046
|
+
async function updateNativeClaudePeer(pid, patch, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY) {
|
|
3047
|
+
const path = join8(registryDir, `${pid}.json`);
|
|
3048
|
+
let current;
|
|
3049
|
+
try {
|
|
3050
|
+
current = JSON.parse(await readFile(path, "utf8"));
|
|
3051
|
+
} catch {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
await writeFile(path, `${JSON.stringify({ ...current, ...patch }, null, 2)}
|
|
3055
|
+
`, { mode: 384 });
|
|
3056
|
+
}
|
|
3057
|
+
async function deregisterNativeClaudePeer(pid, socketPath, registryDir = DEFAULT_CLAUDE_SESSION_REGISTRY) {
|
|
3058
|
+
await unlink(join8(registryDir, `${pid}.json`)).catch(() => void 0);
|
|
3059
|
+
if (socketPath) await unlink(socketPath).catch(() => void 0);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// claude/native-bridge.ts
|
|
3063
|
+
function closeServer(server) {
|
|
3064
|
+
return new Promise((resolve5) => server.close(() => resolve5()));
|
|
3065
|
+
}
|
|
3066
|
+
function nativePrompt(from, message) {
|
|
3067
|
+
const reply = message.expectsReply ? "\n\nThe sender is blocking. After acting, use Claude's built-in SendMessage tool to reply to this same native peer. A normal assistant response stays local and will not unblock the sender." : "\n\nAct if needed. To acknowledge or report a result, use Claude's built-in SendMessage tool to reply to this same native peer; a normal assistant response stays local.";
|
|
3068
|
+
return [
|
|
3069
|
+
`[Intercom message from ${formatSessionDisplay(from)} (${from.id})]`,
|
|
3070
|
+
message.content.text,
|
|
3071
|
+
formatAttachments(message.content.attachments),
|
|
3072
|
+
reply
|
|
3073
|
+
].join("\n");
|
|
3074
|
+
}
|
|
3075
|
+
var NativeClaudeBrokerBridge = class {
|
|
3076
|
+
constructor(identity, options = {}) {
|
|
3077
|
+
this.identity = identity;
|
|
3078
|
+
this.options = options;
|
|
3079
|
+
const scopeId = options.scopeId === void 0 ? intercomScopeIdFromEnvForRegistration(options.env ?? process.env) : parseIntercomScopeIdForRegistration(options.scopeId);
|
|
3080
|
+
const scopeEnv = scopeId === void 0 ? {} : { [INTERCOM_SCOPE_ENV]: scopeId };
|
|
3081
|
+
this.client = options.client ?? new IntercomClient({ env: scopeEnv });
|
|
3082
|
+
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
3083
|
+
const config = loadConfig();
|
|
3084
|
+
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
3085
|
+
});
|
|
3086
|
+
this.sendNative = options.sendNative ?? sendNativeClaudeMessage;
|
|
3087
|
+
this.sendFrame = options.sendFrame ?? sendNativeFrame;
|
|
3088
|
+
this.pid = identity.pid ?? process.pid;
|
|
3089
|
+
this.socketPath = options.socketPath ?? join9(claudeNativeSocketDir(options), `agent-intercom-${this.pid}.sock`);
|
|
3090
|
+
}
|
|
3091
|
+
identity;
|
|
3092
|
+
options;
|
|
3093
|
+
client;
|
|
3094
|
+
prepareConnection;
|
|
3095
|
+
sendNative;
|
|
3096
|
+
sendFrame;
|
|
3097
|
+
pid;
|
|
3098
|
+
socketPath;
|
|
3099
|
+
server;
|
|
3100
|
+
targetSocket = "";
|
|
3101
|
+
pending = [];
|
|
3102
|
+
started = false;
|
|
3103
|
+
get nativeSocketPath() {
|
|
3104
|
+
return this.socketPath;
|
|
3105
|
+
}
|
|
3106
|
+
async start(targetSocket) {
|
|
3107
|
+
if (this.started) {
|
|
3108
|
+
this.targetSocket = targetSocket;
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
this.targetSocket = targetSocket;
|
|
3112
|
+
this.server = await bindNativeClaudeSocket(this.socketPath, (frame) => this.handleNativeFrame(frame));
|
|
3113
|
+
await registerNativeClaudePeer({
|
|
3114
|
+
pid: this.pid,
|
|
3115
|
+
sessionId: this.identity.id,
|
|
3116
|
+
name: `${this.identity.name} bridge`,
|
|
3117
|
+
cwd: this.identity.cwd,
|
|
3118
|
+
socketPath: this.socketPath,
|
|
3119
|
+
status: "idle",
|
|
3120
|
+
registryDir: this.options.registryDir
|
|
3121
|
+
});
|
|
3122
|
+
await this.prepareConnection();
|
|
3123
|
+
this.client.on("message", (from, message, deliveryId) => {
|
|
3124
|
+
void this.handleBrokerMessage(from, message, deliveryId);
|
|
3125
|
+
});
|
|
3126
|
+
this.client.on("disconnected", () => {
|
|
3127
|
+
void updateNativeClaudePeer(this.pid, { status: "broker disconnected" }, this.options.registryDir);
|
|
3128
|
+
});
|
|
3129
|
+
await this.client.connect({
|
|
3130
|
+
name: this.identity.name,
|
|
3131
|
+
cwd: this.identity.cwd,
|
|
3132
|
+
model: this.identity.model ?? "claude",
|
|
3133
|
+
pid: this.pid,
|
|
3134
|
+
startedAt: Date.now(),
|
|
3135
|
+
lastActivity: Date.now(),
|
|
3136
|
+
status: "idle"
|
|
3137
|
+
}, this.identity.id);
|
|
3138
|
+
this.started = true;
|
|
3139
|
+
}
|
|
3140
|
+
async handleBrokerMessage(from, message, deliveryId) {
|
|
3141
|
+
try {
|
|
3142
|
+
this.client.updatePresence({ status: "active" });
|
|
3143
|
+
await this.sendNative({
|
|
3144
|
+
socketPath: this.targetSocket,
|
|
3145
|
+
body: nativePrompt(from, message),
|
|
3146
|
+
from: `uds:${this.socketPath}`,
|
|
3147
|
+
fromName: this.identity.name
|
|
3148
|
+
});
|
|
3149
|
+
this.pending.push({ from, message });
|
|
3150
|
+
this.client.acknowledgeMessage(deliveryId);
|
|
3151
|
+
} catch (error) {
|
|
3152
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3153
|
+
this.client.updatePresence({ status: `native delivery error: ${reason}` });
|
|
3154
|
+
if (message.expectsReply) {
|
|
3155
|
+
await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id }).catch(() => void 0);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
handleNativeFrame(frame) {
|
|
3160
|
+
if (frame.type !== "user") return;
|
|
3161
|
+
const message = frame.message;
|
|
3162
|
+
if (typeof message !== "object" || message === null || !("content" in message)) return;
|
|
3163
|
+
const envelope = stripNativeEnvelope(message.content);
|
|
3164
|
+
if (!envelope.body) return;
|
|
3165
|
+
const relay = this.pending.shift();
|
|
3166
|
+
const fromAddress = typeof frame.from === "string" ? frame.from : envelope.from;
|
|
3167
|
+
if (fromAddress?.startsWith("uds:")) {
|
|
3168
|
+
void this.sendFrame(fromAddress.slice(4), buildNativeReceipt({
|
|
3169
|
+
status: "delivered",
|
|
3170
|
+
from: `uds:${this.socketPath}`,
|
|
3171
|
+
originalMessageId: typeof frame.msg_id === "string" ? frame.msg_id : void 0,
|
|
3172
|
+
reason: "Relayed to Agent Intercom."
|
|
3173
|
+
})).catch(() => void 0);
|
|
3174
|
+
}
|
|
3175
|
+
if (!relay) return;
|
|
3176
|
+
void this.client.send(relay.from.id, {
|
|
3177
|
+
text: envelope.body,
|
|
3178
|
+
...relay.message.expectsReply ? { replyTo: relay.message.id } : {}
|
|
3179
|
+
}).then(() => {
|
|
3180
|
+
this.client.updatePresence({ status: "idle" });
|
|
3181
|
+
}).catch(() => void 0);
|
|
3182
|
+
}
|
|
3183
|
+
async stop() {
|
|
3184
|
+
this.started = false;
|
|
3185
|
+
this.pending = [];
|
|
3186
|
+
await this.client.disconnect().catch(() => void 0);
|
|
3187
|
+
if (this.server) await closeServer(this.server).catch(() => void 0);
|
|
3188
|
+
this.server = void 0;
|
|
3189
|
+
await deregisterNativeClaudePeer(this.pid, this.socketPath, this.options.registryDir);
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
|
|
3193
|
+
// claude/cci.ts
|
|
3194
|
+
function sanitizeSegment(value) {
|
|
3195
|
+
return value.replace(/[^a-zA-Z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "claude";
|
|
3196
|
+
}
|
|
3197
|
+
function shortHash(value) {
|
|
3198
|
+
return createHash2("sha1").update(value).digest("hex").slice(0, 8);
|
|
3199
|
+
}
|
|
3200
|
+
function gitString(cwd, args) {
|
|
3201
|
+
const result = spawnSync3("git", args, {
|
|
3202
|
+
cwd,
|
|
3203
|
+
encoding: "utf8",
|
|
3204
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3205
|
+
});
|
|
3206
|
+
if (result.status !== 0) return null;
|
|
3207
|
+
const trimmed = result.stdout.trim();
|
|
3208
|
+
return trimmed || null;
|
|
3209
|
+
}
|
|
3210
|
+
function createDefaultIdentity(input) {
|
|
3211
|
+
const root = input.gitRoot || input.cwd;
|
|
3212
|
+
const repo = basename3(root) || "claude";
|
|
3213
|
+
const branch = input.branch || "worktree";
|
|
3214
|
+
const readable = `${repo}:${branch}`;
|
|
3215
|
+
const suffix = `${shortHash(input.cwd)}-${input.pid}`;
|
|
3216
|
+
return {
|
|
3217
|
+
id: sanitizeSegment(`claude-${repo}-${branch}-${suffix}`),
|
|
3218
|
+
name: `claude:${readable}#${input.pid}`
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
function capturedScopeEnv(scopeId) {
|
|
3222
|
+
return scopeId === void 0 ? { [INTERCOM_SCOPE_ENV]: "" } : { [INTERCOM_SCOPE_ENV]: scopeId };
|
|
3223
|
+
}
|
|
3224
|
+
function detectIdentity(cwd) {
|
|
3225
|
+
return createDefaultIdentity({
|
|
3226
|
+
cwd,
|
|
3227
|
+
pid: process.pid,
|
|
3228
|
+
gitRoot: gitString(cwd, ["rev-parse", "--show-toplevel"]),
|
|
3229
|
+
branch: gitString(cwd, ["branch", "--show-current"])
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
function readValue(args, index, option) {
|
|
3233
|
+
const value = args[index + 1];
|
|
3234
|
+
if (!value || value.startsWith("--")) {
|
|
3235
|
+
throw new Error(`${option} requires a value`);
|
|
3236
|
+
}
|
|
3237
|
+
return value;
|
|
3238
|
+
}
|
|
3239
|
+
function parseCciArgs(argv, env = process.env) {
|
|
3240
|
+
const options = { addDirs: [] };
|
|
3241
|
+
let dangerouslySkipPermissions;
|
|
3242
|
+
let minimal = false;
|
|
3243
|
+
let tui = false;
|
|
3244
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
3245
|
+
const arg = argv[index];
|
|
3246
|
+
const [key, inlineValue] = arg.includes("=") ? arg.split(/=(.*)/s, 2) : [arg, void 0];
|
|
3247
|
+
const value = inlineValue ?? null;
|
|
3248
|
+
switch (key) {
|
|
3249
|
+
case "--name":
|
|
3250
|
+
options.name = value ?? readValue(argv, index++, key);
|
|
3251
|
+
break;
|
|
3252
|
+
case "--id":
|
|
3253
|
+
options.id = value ?? readValue(argv, index++, key);
|
|
3254
|
+
break;
|
|
3255
|
+
case "--cwd":
|
|
3256
|
+
options.cwd = resolve4(value ?? readValue(argv, index++, key));
|
|
3257
|
+
break;
|
|
3258
|
+
case "--instructions":
|
|
3259
|
+
options.instructions = value ?? readValue(argv, index++, key);
|
|
3260
|
+
break;
|
|
3261
|
+
case "--model":
|
|
3262
|
+
options.model = value ?? readValue(argv, index++, key);
|
|
3263
|
+
break;
|
|
3264
|
+
case "--effort":
|
|
3265
|
+
options.effort = value ?? readValue(argv, index++, key);
|
|
3266
|
+
break;
|
|
3267
|
+
case "--state":
|
|
3268
|
+
options.statePath = resolve4(value ?? readValue(argv, index++, key));
|
|
3269
|
+
break;
|
|
3270
|
+
case "--permission-mode":
|
|
3271
|
+
options.permissionMode = validateClaudePermissionMode(value ?? readValue(argv, index++, key), "--permission-mode");
|
|
3272
|
+
break;
|
|
3273
|
+
case "--add-dir":
|
|
3274
|
+
options.addDirs.push(resolve4(value ?? readValue(argv, index++, key)));
|
|
3275
|
+
break;
|
|
3276
|
+
case "--mcp-config":
|
|
3277
|
+
options.mcpConfig = value ?? readValue(argv, index++, key);
|
|
3278
|
+
break;
|
|
3279
|
+
case "--claude":
|
|
3280
|
+
options.claudeCommand = value ?? readValue(argv, index++, key);
|
|
3281
|
+
break;
|
|
3282
|
+
case "--transport":
|
|
3283
|
+
options.transport = parseClaudeIntercomTransport(value ?? readValue(argv, index++, key), key);
|
|
3284
|
+
break;
|
|
3285
|
+
case "--yolo":
|
|
3286
|
+
case "--dangerously-skip-permissions":
|
|
3287
|
+
dangerouslySkipPermissions = true;
|
|
3288
|
+
break;
|
|
3289
|
+
case "--safe":
|
|
3290
|
+
dangerouslySkipPermissions = false;
|
|
3291
|
+
options.permissionMode = options.permissionMode ?? "manual";
|
|
3292
|
+
break;
|
|
3293
|
+
case "--minimal":
|
|
3294
|
+
case "--bare":
|
|
3295
|
+
minimal = true;
|
|
3296
|
+
break;
|
|
3297
|
+
case "--tui":
|
|
3298
|
+
case "--live":
|
|
3299
|
+
tui = true;
|
|
3300
|
+
break;
|
|
3301
|
+
default:
|
|
3302
|
+
break;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
const permission = resolveClaudePermissionPolicy({
|
|
3306
|
+
permissionMode: dangerouslySkipPermissions ? void 0 : options.permissionMode,
|
|
3307
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions ?? false
|
|
3308
|
+
});
|
|
3309
|
+
return {
|
|
3310
|
+
cwd: resolve4(options.cwd ?? env.CLAUDE_INTERCOM_CWD ?? process.cwd()),
|
|
3311
|
+
id: options.id ?? env.CLAUDE_INTERCOM_SESSION_ID,
|
|
3312
|
+
name: options.name ?? env.CLAUDE_INTERCOM_NAME,
|
|
3313
|
+
instructions: options.instructions ?? env.CLAUDE_INTERCOM_INSTRUCTIONS,
|
|
3314
|
+
model: options.model ?? env.CLAUDE_INTERCOM_MODEL,
|
|
3315
|
+
effort: options.effort ?? env.CLAUDE_INTERCOM_EFFORT,
|
|
3316
|
+
statePath: options.statePath,
|
|
3317
|
+
permissionMode: permission.permissionMode,
|
|
3318
|
+
dangerouslySkipPermissions: permission.dangerouslySkipPermissions,
|
|
3319
|
+
addDirs: options.addDirs,
|
|
3320
|
+
mcpConfig: options.mcpConfig,
|
|
3321
|
+
minimal,
|
|
3322
|
+
tui,
|
|
3323
|
+
claudeCommand: options.claudeCommand || env.CLAUDE_INTERCOM_CLAUDE_COMMAND || "claude",
|
|
3324
|
+
transport: parseClaudeIntercomTransport(options.transport ?? env.CLAUDE_INTERCOM_TRANSPORT, "CLAUDE_INTERCOM_TRANSPORT"),
|
|
3325
|
+
intercomEnv: Object.hasOwn(env, INTERCOM_SCOPE_ENV) ? { [INTERCOM_SCOPE_ENV]: env[INTERCOM_SCOPE_ENV] } : {}
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function resolveIntercomSelection(selection, sessionCount) {
|
|
3329
|
+
const trimmed = selection.trim();
|
|
3330
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
3331
|
+
const index = Number(trimmed) - 1;
|
|
3332
|
+
return index >= 0 && index < sessionCount ? index : null;
|
|
3333
|
+
}
|
|
3334
|
+
async function openIntercomComposer(daemon) {
|
|
3335
|
+
const sessions = await daemon.listPrimaryPeers();
|
|
3336
|
+
if (!sessions.length) {
|
|
3337
|
+
process.stderr.write("No other intercom sessions are connected.\n");
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
process.stderr.write("\nIntercom sessions:\n");
|
|
3341
|
+
sessions.forEach((session, index) => {
|
|
3342
|
+
const status = session.status ? `, ${session.status}` : "";
|
|
3343
|
+
process.stderr.write(` ${index + 1}. ${session.name || "unnamed"} (${session.id.slice(0, 8)}) \u2014 ${session.cwd} [${session.model}${status}]
|
|
3344
|
+
`);
|
|
3345
|
+
});
|
|
3346
|
+
const terminal = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
3347
|
+
try {
|
|
3348
|
+
const selection = await terminal.question("Send to (number, blank to cancel): ");
|
|
3349
|
+
if (!selection.trim()) {
|
|
3350
|
+
process.stderr.write("Intercom message cancelled.\n");
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
const selectedIndex = resolveIntercomSelection(selection, sessions.length);
|
|
3354
|
+
if (selectedIndex === null) {
|
|
3355
|
+
process.stderr.write("Invalid intercom session selection.\n");
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
const message = await terminal.question("Message (blank to cancel): ");
|
|
3359
|
+
if (!message.trim()) {
|
|
3360
|
+
process.stderr.write("Intercom message cancelled.\n");
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
const status = await daemon.sendFromPrimary(sessions[selectedIndex].id, message);
|
|
3364
|
+
process.stderr.write(`${status}
|
|
3365
|
+
`);
|
|
3366
|
+
} finally {
|
|
3367
|
+
terminal.close();
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
function repoRoot() {
|
|
3371
|
+
return resolve4(dirname7(fileURLToPath2(import.meta.url)), "..");
|
|
3372
|
+
}
|
|
3373
|
+
function writeDefaultWorkerMcpConfig(root = repoRoot(), intercomDir = getIntercomDirPath()) {
|
|
3374
|
+
const serverPath = join10(root, "dist", "claude-server.mjs");
|
|
3375
|
+
if (!existsSync5(serverPath)) {
|
|
3376
|
+
throw new Error(`Claude intercom MCP server is missing. Run \`npm run build\` in ${root}.`);
|
|
3377
|
+
}
|
|
3378
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
3379
|
+
const path = join10(intercomDir, `claude-worker-mcp-${shortHash(root)}.json`);
|
|
3380
|
+
writeFileSync4(path, `${JSON.stringify({ mcpServers: { "claude-intercom": { command: process.execPath, args: [serverPath] } } }, null, 2)}
|
|
3381
|
+
`, { mode: 384 });
|
|
3382
|
+
restrictIntercomRuntimeFile(path);
|
|
3383
|
+
return path;
|
|
3384
|
+
}
|
|
3385
|
+
function buildTuiAppendSystemPrompt(name, id, transport = "mcp") {
|
|
3386
|
+
const common = `You are a Claude Code session connected to a local intercom as "${name}" (id ${id}).`;
|
|
3387
|
+
if (transport === "native") {
|
|
3388
|
+
return [
|
|
3389
|
+
common,
|
|
3390
|
+
`Other local coding-agent sessions can message you through Claude's native cross-session channel. Inbound requests begin with "[Intercom message from".`,
|
|
3391
|
+
"Treat each as a peer request. After acting, use Claude's built-in SendMessage tool to send your answer or acknowledgement back to the same native peer. A normal assistant response is visible only in this TUI and does not reach the blocking sender.",
|
|
3392
|
+
"Do not claim intercom_send or intercom_reply tools are available unless another configured plugin actually provides them."
|
|
3393
|
+
].join("\n");
|
|
3394
|
+
}
|
|
3395
|
+
return [
|
|
3396
|
+
common,
|
|
3397
|
+
'Other local coding-agent sessions can message you. Inbound messages are delivered automatically as monitor events that begin with "Intercom message from".',
|
|
3398
|
+
"When such an event arrives, treat it as a request from that peer:",
|
|
3399
|
+
'- If it is marked "[asking \u2014 awaiting your reply]", the sender is BLOCKING on your answer. Do the work if appropriate, then answer with the intercom_reply tool: intercom_reply({ message: "..." }).',
|
|
3400
|
+
"- Otherwise, act if needed and use intercom_send to respond or acknowledge.",
|
|
3401
|
+
"Use intercom_team to find your manager and managed coworkers. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise."
|
|
3402
|
+
].join("\n");
|
|
3403
|
+
}
|
|
3404
|
+
function nativeClaudeFeatureEnv(transport) {
|
|
3405
|
+
return transport === "native" ? { CLAUDE_CODE_HARBOR_KITE: "1" } : {};
|
|
3406
|
+
}
|
|
3407
|
+
async function waitForChildExit(child) {
|
|
3408
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3409
|
+
return [child.exitCode, child.signalCode];
|
|
3410
|
+
}
|
|
3411
|
+
return once2(child, "exit");
|
|
3412
|
+
}
|
|
3413
|
+
async function runCciTui(options, id, name, scopeId) {
|
|
3414
|
+
const scopeEnv = capturedScopeEnv(scopeId);
|
|
3415
|
+
const resolution = resolveClaudeIntercomTransport({
|
|
3416
|
+
requested: options.transport,
|
|
3417
|
+
claudeCommand: options.claudeCommand
|
|
3418
|
+
});
|
|
3419
|
+
const root = repoRoot();
|
|
3420
|
+
const serverPath = join10(root, "dist", "claude-server.mjs");
|
|
3421
|
+
const monitorPath = join10(root, "dist", "inbox-monitor.mjs");
|
|
3422
|
+
if (resolution.selected === "mcp" && (!existsSync5(serverPath) || !existsSync5(monitorPath))) {
|
|
3423
|
+
process.stderr.write(`cci --tui with MCP transport requires a build. Run \`npm run build\` in ${root} first.
|
|
3424
|
+
`);
|
|
3425
|
+
return 1;
|
|
3426
|
+
}
|
|
3427
|
+
if (options.minimal) {
|
|
3428
|
+
process.stderr.write(resolution.selected === "mcp" ? "cci --tui ignores --minimal: --safe-mode would disable the intercom MCP server and inbox monitor.\n" : "cci --tui ignores --minimal: live native mode runs an ordinary interactive Claude session.\n");
|
|
3429
|
+
}
|
|
3430
|
+
const inboxPath = defaultInboxPath(id);
|
|
3431
|
+
if (resolution.selected === "mcp") {
|
|
3432
|
+
rmSync(inboxPath, { force: true });
|
|
3433
|
+
}
|
|
3434
|
+
const args = ["--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
3435
|
+
if (resolution.selected === "mcp") args.unshift("--plugin-dir", root);
|
|
3436
|
+
const permission = resolveClaudePermissionPolicy(options);
|
|
3437
|
+
if (options.model) args.push("--model", options.model);
|
|
3438
|
+
if (options.effort) args.push("--effort", options.effort);
|
|
3439
|
+
if (permission.dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
|
3440
|
+
else if (permission.permissionMode) args.push("--permission-mode", permission.permissionMode);
|
|
3441
|
+
for (const dir of options.addDirs) args.push("--add-dir", dir);
|
|
3442
|
+
process.stderr.write(`cci --tui: live intercom session ${name} (${id}), ${resolution.selected} transport (${resolution.reason})
|
|
3443
|
+
`);
|
|
3444
|
+
process.stderr.write(resolution.selected === "native" ? "Inbound intercom messages use Claude's native cross-session channel; answer peers with Claude's built-in SendMessage tool.\n" : "Inbound intercom messages appear in this session automatically; reply with the intercom_reply tool.\n");
|
|
3445
|
+
const child = spawn4(options.claudeCommand, args, {
|
|
3446
|
+
cwd: options.cwd,
|
|
3447
|
+
stdio: "inherit",
|
|
3448
|
+
env: {
|
|
3449
|
+
...process.env,
|
|
3450
|
+
...scopeEnv,
|
|
3451
|
+
CLAUDE_INTERCOM_NAME: name,
|
|
3452
|
+
CLAUDE_INTERCOM_SESSION_ID: id,
|
|
3453
|
+
...options.model ? { CLAUDE_INTERCOM_MODEL: options.model } : {},
|
|
3454
|
+
...nativeClaudeFeatureEnv(resolution.selected),
|
|
3455
|
+
...resolution.selected === "mcp" ? { CLAUDE_INTERCOM_INBOX: inboxPath } : {}
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
let bridge;
|
|
3459
|
+
if (resolution.selected === "native") {
|
|
3460
|
+
try {
|
|
3461
|
+
if (!child.pid) throw new Error("Claude started without a process id; native transport cannot attach");
|
|
3462
|
+
const peer = await waitForNativeClaudePeer(child.pid);
|
|
3463
|
+
bridge = new NativeClaudeBrokerBridge({ id, name, cwd: options.cwd, model: options.model }, { scopeId });
|
|
3464
|
+
await bridge.start(peer.socketPath);
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
child.kill("SIGTERM");
|
|
3467
|
+
await waitForChildExit(child).catch(() => void 0);
|
|
3468
|
+
await bridge?.stop();
|
|
3469
|
+
if (resolution.requested !== "auto") throw error;
|
|
3470
|
+
process.stderr.write(`Native Claude transport did not attach (${error instanceof Error ? error.message : String(error)}); falling back to MCP.
|
|
3471
|
+
`);
|
|
3472
|
+
return runCciTui({ ...options, transport: "mcp" }, id, name, scopeId);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
const [code, signal] = await waitForChildExit(child);
|
|
3476
|
+
await bridge?.stop();
|
|
3477
|
+
if (resolution.selected === "mcp") rmSync(inboxPath, { force: true });
|
|
3478
|
+
if (typeof code === "number") return code;
|
|
3479
|
+
return signal === "SIGINT" ? 130 : 1;
|
|
3480
|
+
}
|
|
3481
|
+
async function runCci(options) {
|
|
3482
|
+
const tuiScopeId = options.tui ? intercomScopeIdFromEnvForRegistration(options.intercomEnv ?? process.env) : void 0;
|
|
3483
|
+
const identity = detectIdentity(options.cwd);
|
|
3484
|
+
const id = sanitizeSegment(options.id ?? identity.id);
|
|
3485
|
+
const name = options.name ?? identity.name;
|
|
3486
|
+
if (options.tui) {
|
|
3487
|
+
return runCciTui(options, id, name, tuiScopeId);
|
|
3488
|
+
}
|
|
3489
|
+
const statePath = options.statePath ?? DEFAULT_WORKER_STATE_PATH;
|
|
3490
|
+
const resolution = resolveClaudeIntercomTransport({
|
|
3491
|
+
requested: options.transport,
|
|
3492
|
+
claudeCommand: options.claudeCommand
|
|
3493
|
+
});
|
|
3494
|
+
const mcpConfig = options.mcpConfig ?? (resolution.selected === "mcp" && !options.minimal ? writeDefaultWorkerMcpConfig() : void 0);
|
|
3495
|
+
const claudeArgs = [
|
|
3496
|
+
...options.minimal ? ["--safe-mode"] : [],
|
|
3497
|
+
...options.effort ? ["--effort", options.effort] : []
|
|
3498
|
+
];
|
|
3499
|
+
const agent = {
|
|
3500
|
+
id,
|
|
3501
|
+
name,
|
|
3502
|
+
cwd: options.cwd,
|
|
3503
|
+
model: options.model,
|
|
3504
|
+
instructions: options.instructions,
|
|
3505
|
+
permissionMode: options.permissionMode,
|
|
3506
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
3507
|
+
addDirs: options.addDirs.length ? options.addDirs : void 0,
|
|
3508
|
+
mcpConfig,
|
|
3509
|
+
claudeArgs: claudeArgs.length ? claudeArgs : void 0,
|
|
3510
|
+
transport: resolution.selected
|
|
3511
|
+
};
|
|
3512
|
+
const config = {
|
|
3513
|
+
statePath,
|
|
3514
|
+
claudeCommand: options.claudeCommand,
|
|
3515
|
+
agents: [agent]
|
|
3516
|
+
};
|
|
3517
|
+
process.stderr.write(`cci intercom worker: ${name} (${id}), ${resolution.selected} transport (${resolution.reason})
|
|
3518
|
+
`);
|
|
3519
|
+
process.stderr.write(`Resume this worker's Claude session anytime with: claude --resume <session-id> (see ${statePath} once a turn has run)
|
|
3520
|
+
`);
|
|
3521
|
+
if (options.dangerouslySkipPermissions) {
|
|
3522
|
+
process.stderr.write("Running with explicitly requested --dangerously-skip-permissions (yolo).\n");
|
|
3523
|
+
}
|
|
3524
|
+
if (options.minimal) {
|
|
3525
|
+
process.stderr.write("Minimal mode: woken turns run with --safe-mode (no CLAUDE.md, skills, plugins, hooks, or MCP). Built-in tools and subagent delegation (Task tool) are retained.\n");
|
|
3526
|
+
}
|
|
3527
|
+
const daemon = new ClaudeWorkerDaemon(config);
|
|
3528
|
+
let cleaned = false;
|
|
3529
|
+
const cleanupOnce = async () => {
|
|
3530
|
+
if (cleaned) return;
|
|
3531
|
+
cleaned = true;
|
|
3532
|
+
await daemon.stop().catch(() => void 0);
|
|
3533
|
+
};
|
|
3534
|
+
process.once("SIGINT", () => {
|
|
3535
|
+
void cleanupOnce().finally(() => process.exit(130));
|
|
3536
|
+
});
|
|
3537
|
+
process.once("SIGTERM", () => {
|
|
3538
|
+
void cleanupOnce().finally(() => process.exit(143));
|
|
3539
|
+
});
|
|
3540
|
+
await daemon.start();
|
|
3541
|
+
let restoreInput;
|
|
3542
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
3543
|
+
process.stdin.setRawMode(true);
|
|
3544
|
+
process.stdin.resume();
|
|
3545
|
+
let composerOpen = false;
|
|
3546
|
+
const onData = (chunk) => {
|
|
3547
|
+
const input = chunk.toString("utf8");
|
|
3548
|
+
if (input === "\x1Bi" || input === "\x1BI") {
|
|
3549
|
+
void daemon.copyPrimaryContact().then((status) => process.stderr.write(`${status}
|
|
3550
|
+
`));
|
|
3551
|
+
} else if ((input === "\x1Bm" || input === "\x1BM") && !composerOpen) {
|
|
3552
|
+
composerOpen = true;
|
|
3553
|
+
process.stdin.off("data", onData);
|
|
3554
|
+
process.stdin.setRawMode(false);
|
|
3555
|
+
void openIntercomComposer(daemon).catch((error) => process.stderr.write(`Intercom: ${error instanceof Error ? error.message : String(error)}
|
|
3556
|
+
`)).finally(() => {
|
|
3557
|
+
composerOpen = false;
|
|
3558
|
+
process.stdin.setRawMode(true);
|
|
3559
|
+
process.stdin.on("data", onData);
|
|
3560
|
+
});
|
|
3561
|
+
} else if (input === "") {
|
|
3562
|
+
process.emit("SIGINT");
|
|
3563
|
+
}
|
|
3564
|
+
};
|
|
3565
|
+
process.stdin.on("data", onData);
|
|
3566
|
+
restoreInput = () => {
|
|
3567
|
+
process.stdin.off("data", onData);
|
|
3568
|
+
process.stdin.setRawMode(false);
|
|
3569
|
+
process.stdin.pause();
|
|
3570
|
+
};
|
|
3571
|
+
process.stderr.write("Press Alt+M to send an intercom message or Alt+I to copy this worker's contact target.\n");
|
|
3572
|
+
}
|
|
3573
|
+
await Promise.race([once2(process, "SIGINT"), once2(process, "SIGTERM")]);
|
|
3574
|
+
restoreInput?.();
|
|
3575
|
+
await cleanupOnce();
|
|
3576
|
+
return 0;
|
|
3577
|
+
}
|
|
3578
|
+
async function main2() {
|
|
3579
|
+
const options = parseCciArgs(process.argv.slice(2));
|
|
3580
|
+
const code = await runCci(options);
|
|
3581
|
+
process.exit(code);
|
|
3582
|
+
}
|
|
3583
|
+
function invokedFileBasename2() {
|
|
3584
|
+
try {
|
|
3585
|
+
return process.argv[1] ? basename3(realpathSync3(process.argv[1])) : "";
|
|
3586
|
+
} catch {
|
|
3587
|
+
return process.argv[1] ? basename3(process.argv[1]) : "";
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
if (invokedFileBasename2() === "cci.ts" || invokedFileBasename2() === "cci.mjs") {
|
|
3591
|
+
void main2().catch((error) => {
|
|
3592
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
3593
|
+
`);
|
|
3594
|
+
process.exit(1);
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
export {
|
|
3598
|
+
buildTuiAppendSystemPrompt,
|
|
3599
|
+
createDefaultIdentity,
|
|
3600
|
+
nativeClaudeFeatureEnv,
|
|
3601
|
+
parseCciArgs,
|
|
3602
|
+
resolveIntercomSelection,
|
|
3603
|
+
runCci,
|
|
3604
|
+
sanitizeSegment,
|
|
3605
|
+
waitForChildExit,
|
|
3606
|
+
writeDefaultWorkerMcpConfig
|
|
3607
|
+
};
|