@ctliz/agent-intercom-codex 0.11.0-connect.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.codex-plugin/plugin.json +39 -0
  2. package/.mcp.json +10 -0
  3. package/COPYRIGHT +6 -0
  4. package/LICENSE +661 -0
  5. package/LICENSE_TRANSITION.md +25 -0
  6. package/README.md +572 -0
  7. package/THIRD_PARTY_NOTICES.md +17 -0
  8. package/assets/logo-generated.png +0 -0
  9. package/assets/logo.svg +22 -0
  10. package/boss-control-outbox.ts +195 -0
  11. package/broker/access-credential.ts +76 -0
  12. package/broker/access-registry.ts +464 -0
  13. package/broker/audit.ts +60 -0
  14. package/broker/authorization.ts +54 -0
  15. package/broker/boss-adapter.ts +531 -0
  16. package/broker/boss-control-ledger.ts +306 -0
  17. package/broker/broker.ts +2327 -0
  18. package/broker/client.ts +1111 -0
  19. package/broker/framing.ts +87 -0
  20. package/broker/ownership.ts +61 -0
  21. package/broker/paths.ts +161 -0
  22. package/broker/spawn.ts +453 -0
  23. package/codex/app-server-client.ts +562 -0
  24. package/codex/boss-client.ts +34 -0
  25. package/codex/bridge-config.ts +282 -0
  26. package/codex/bridge-daemon.ts +913 -0
  27. package/codex/clipboard.ts +142 -0
  28. package/codex/coi.ts +802 -0
  29. package/codex/contact.ts +54 -0
  30. package/codex/mcp-protocol.ts +248 -0
  31. package/codex/runtime.ts +488 -0
  32. package/codex/server.ts +60 -0
  33. package/codex/team.ts +249 -0
  34. package/codex/tui-input.ts +174 -0
  35. package/config.ts +195 -0
  36. package/dist/bridge-daemon.mjs +3781 -0
  37. package/dist/broker.mjs +3012 -0
  38. package/dist/build-info.json +12 -0
  39. package/dist/codex-server.mjs +2890 -0
  40. package/dist/coi.mjs +4686 -0
  41. package/durable-json.ts +25 -0
  42. package/licenses/MIT-pi-intercom.txt +21 -0
  43. package/outbound-outbox.ts +116 -0
  44. package/package.json +89 -0
  45. package/protocol-v4/contract.ts +27 -0
  46. package/provider/protected-service.ts +162 -0
  47. package/provider/provider.mjs +34 -0
  48. package/scripts/build-info.mjs +58 -0
  49. package/scripts/build.mjs +55 -0
  50. package/skills/codex-intercom/SKILL.md +104 -0
  51. package/tsconfig.json +16 -0
  52. package/types.ts +225 -0
@@ -0,0 +1,453 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { createRequire } from "module";
6
+ import net from "net";
7
+ import { randomUUID } from "crypto";
8
+ import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
9
+ import { createMessageReader, writeMessage } from "./framing.ts";
10
+ import {
11
+ ensureIntercomRuntimeDir,
12
+ getAgentDirPath,
13
+ getBrokerConnectTarget,
14
+ getIntercomDirPath,
15
+ INTERCOM_PROTOCOL_NAME,
16
+ INTERCOM_PROTOCOL_VERSION,
17
+ INTERCOM_RUNTIME_FILE_MODE,
18
+ restrictIntercomRuntimeFile,
19
+ type BrokerConnectTarget,
20
+ } from "./paths.ts";
21
+
22
+ const INTERCOM_DIR = getIntercomDirPath();
23
+ const EXTENSION_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const BROKER_PID = join(INTERCOM_DIR, "broker.pid");
25
+ const BROKER_SPAWN_LOCK = join(INTERCOM_DIR, "broker.spawn.lock");
26
+
27
+ type BrokerLaunchSpec =
28
+ | {
29
+ kind: "direct";
30
+ command: string;
31
+ args: string[];
32
+ }
33
+ | {
34
+ kind: "windows-launcher";
35
+ command: string;
36
+ args: string[];
37
+ launcherPath: string;
38
+ launcherCommandLine: string;
39
+ };
40
+
41
+ function sleep(ms: number): Promise<void> {
42
+ return new Promise(resolve => setTimeout(resolve, ms));
43
+ }
44
+
45
+ export function getBrokerEntryPath(moduleUrl: string = import.meta.url): string {
46
+ const moduleDir = dirname(fileURLToPath(moduleUrl));
47
+ const bundledBroker = join(moduleDir, "broker.mjs");
48
+ return existsSync(bundledBroker) ? bundledBroker : join(moduleDir, "broker.ts");
49
+ }
50
+
51
+ export function getTsxCliPath(extensionDir: string = EXTENSION_DIR): string {
52
+ // Resolve tsx via Node's module resolution so it works regardless of whether
53
+ // tsx is bundled under extensionDir/node_modules or hoisted to a workspace
54
+ // root by npm. We resolve the tsx package main entry (its "exports" field
55
+ // does not expose ./dist/cli.mjs as a subpath) and then locate cli.mjs next
56
+ // to it. Falls back to the legacy relative path if resolution fails.
57
+ try {
58
+ const requireFromExtension = createRequire(import.meta.url);
59
+ const tsxMain = requireFromExtension.resolve("tsx");
60
+ return join(dirname(tsxMain), "cli.mjs");
61
+ } catch {
62
+ return join(extensionDir, "node_modules", "tsx", "dist", "cli.mjs");
63
+ }
64
+ }
65
+
66
+ function quoteWindowsArg(value: string): string {
67
+ return `"${value.replace(/"/g, '""')}"`;
68
+ }
69
+
70
+ export function getWindowsHiddenLauncherPath(intercomDir: string = INTERCOM_DIR): string {
71
+ return join(intercomDir, "broker-launch.vbs");
72
+ }
73
+
74
+ function usesDefaultBrokerCommand(brokerCommand: string, brokerArgs: string[]): boolean {
75
+ return brokerCommand === "npx"
76
+ && brokerArgs.length === 2
77
+ && brokerArgs[0] === "--no-install"
78
+ && brokerArgs[1] === "tsx";
79
+ }
80
+
81
+ export function getWindowsBrokerCommandLine(
82
+ brokerPath: string,
83
+ extensionDir: string = EXTENSION_DIR,
84
+ nodePath: string = process.execPath,
85
+ brokerCommand = "npx",
86
+ brokerArgs: string[] = ["--no-install", "tsx"],
87
+ ): string {
88
+ if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
89
+ return [quoteWindowsArg(nodePath), quoteWindowsArg(getTsxCliPath(extensionDir)), quoteWindowsArg(brokerPath)].join(" ");
90
+ }
91
+
92
+ return [quoteWindowsArg(brokerCommand), ...brokerArgs.map(quoteWindowsArg), quoteWindowsArg(brokerPath)].join(" ");
93
+ }
94
+
95
+ export function getWindowsHiddenLauncherScript(commandLine: string): string {
96
+ return [
97
+ 'Set WshShell = CreateObject("WScript.Shell")',
98
+ `WshShell.Run "${commandLine.replace(/"/g, '""')}", 0, False`,
99
+ 'Set WshShell = Nothing',
100
+ '',
101
+ ].join("\r\n");
102
+ }
103
+
104
+ export function isBrokerHealthOkMessage(message: unknown, requestId: string): boolean {
105
+ if (typeof message !== "object" || message === null || !("type" in message)) {
106
+ return false;
107
+ }
108
+ const response = message as Record<string, unknown>;
109
+ if (
110
+ response.type !== "health_ok"
111
+ || response.requestId !== requestId
112
+ || response.protocol !== INTERCOM_PROTOCOL_NAME
113
+ || response.version !== INTERCOM_PROTOCOL_VERSION
114
+ || response.endpoint !== "local"
115
+ ) return false;
116
+ const remoteAccess = response.remoteAccess;
117
+ if (typeof remoteAccess !== "object" || remoteAccess === null || Array.isArray(remoteAccess)) return false;
118
+ const contract = remoteAccess as Record<string, unknown>;
119
+ return contract.feature === "remote-access-v1"
120
+ && contract.policySemanticsVersion === POLICY_SEMANTICS_VERSION
121
+ && contract.policySemanticsHash === POLICY_SEMANTICS_HASH;
122
+ }
123
+
124
+ function writeWindowsHiddenLauncher(
125
+ commandLine: string,
126
+ launcherPath: string = getWindowsHiddenLauncherPath(),
127
+ ): string {
128
+ ensureIntercomRuntimeDir(dirname(launcherPath));
129
+ writeFileSync(launcherPath, getWindowsHiddenLauncherScript(commandLine), {
130
+ encoding: "utf-8",
131
+ mode: INTERCOM_RUNTIME_FILE_MODE,
132
+ });
133
+ restrictIntercomRuntimeFile(launcherPath);
134
+ return launcherPath;
135
+ }
136
+
137
+ export function getBrokerLaunchSpec(
138
+ brokerPath: string,
139
+ brokerCommand: string,
140
+ brokerArgs: string[],
141
+ extensionDir: string = EXTENSION_DIR,
142
+ platform: NodeJS.Platform = process.platform,
143
+ intercomDir: string = INTERCOM_DIR,
144
+ nodePath: string = process.execPath,
145
+ ): BrokerLaunchSpec {
146
+ if (platform === "win32") {
147
+ const launcherPath = getWindowsHiddenLauncherPath(intercomDir);
148
+ return {
149
+ kind: "windows-launcher",
150
+ command: "wscript.exe",
151
+ args: [launcherPath],
152
+ launcherPath,
153
+ launcherCommandLine: getWindowsBrokerCommandLine(brokerPath, extensionDir, nodePath, brokerCommand, brokerArgs),
154
+ };
155
+ }
156
+
157
+ if (usesDefaultBrokerCommand(brokerCommand, brokerArgs)) {
158
+ return {
159
+ kind: "direct",
160
+ command: nodePath,
161
+ args: [getTsxCliPath(extensionDir), brokerPath],
162
+ };
163
+ }
164
+
165
+ return {
166
+ kind: "direct",
167
+ command: brokerCommand,
168
+ args: [...brokerArgs, brokerPath],
169
+ };
170
+ }
171
+
172
+ export function getBrokerSpawnOptions(
173
+ extensionDir: string = EXTENSION_DIR,
174
+ env: NodeJS.ProcessEnv = process.env,
175
+ ): {
176
+ detached: true;
177
+ stdio: "ignore";
178
+ cwd: string;
179
+ env: NodeJS.ProcessEnv;
180
+ windowsHide: true;
181
+ } {
182
+ return {
183
+ detached: true,
184
+ stdio: "ignore",
185
+ cwd: extensionDir,
186
+ env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
187
+ windowsHide: true,
188
+ };
189
+ }
190
+
191
+ function toError(error: unknown): Error {
192
+ return error instanceof Error ? error : new Error(String(error));
193
+ }
194
+
195
+ export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: string[]): Promise<void> {
196
+ ensureIntercomRuntimeDir(INTERCOM_DIR);
197
+
198
+ if (await isBrokerRunning()) {
199
+ return;
200
+ }
201
+
202
+ const ownsLock = acquireSpawnLock();
203
+ if (!ownsLock) {
204
+ await waitForBroker();
205
+ return;
206
+ }
207
+
208
+ try {
209
+ if (await isBrokerRunning()) {
210
+ return;
211
+ }
212
+ if (await checkBrokerHealth() === "incompatible") {
213
+ throw new Error(`Incompatible live intercom broker; expected ${INTERCOM_PROTOCOL_NAME} v${INTERCOM_PROTOCOL_VERSION}. Stop it explicitly during a coordinated migration.`);
214
+ }
215
+
216
+ const brokerPath = getBrokerEntryPath();
217
+ const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
218
+ if (launch.kind === "windows-launcher") {
219
+ writeWindowsHiddenLauncher(launch.launcherCommandLine, launch.launcherPath);
220
+ }
221
+ const child = spawn(launch.command, launch.args, getBrokerSpawnOptions());
222
+ child.unref();
223
+
224
+ await new Promise<void>((resolve, reject) => {
225
+ const cleanup = () => {
226
+ child.off("error", onError);
227
+ child.off("exit", onExit);
228
+ };
229
+
230
+ const onError = (error: Error) => {
231
+ cleanup();
232
+ reject(new Error(`Failed to spawn intercom broker: ${error.message}`, { cause: error }));
233
+ };
234
+
235
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
236
+ if (launch.kind === "windows-launcher" && code === 0 && signal === null) {
237
+ return;
238
+ }
239
+ cleanup();
240
+ if (signal) {
241
+ reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
242
+ return;
243
+ }
244
+ reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
245
+ };
246
+
247
+ child.once("error", onError);
248
+ child.once("exit", onExit);
249
+ waitForBroker().then(() => {
250
+ cleanup();
251
+ resolve();
252
+ }, (error) => {
253
+ cleanup();
254
+ reject(toError(error));
255
+ });
256
+ });
257
+ } finally {
258
+ releaseSpawnLock();
259
+ }
260
+ }
261
+
262
+ export async function stopBrokerProcess(pidFile = BROKER_PID, timeoutMs = 3000): Promise<void> {
263
+ if (!existsSync(pidFile)) return;
264
+ let pid: number;
265
+ try {
266
+ pid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
267
+ } catch {
268
+ return;
269
+ }
270
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) return;
271
+ try {
272
+ process.kill(pid, "SIGTERM");
273
+ } catch {
274
+ return;
275
+ }
276
+ const startedAt = Date.now();
277
+ while (Date.now() - startedAt < timeoutMs) {
278
+ try {
279
+ process.kill(pid, 0);
280
+ await sleep(50);
281
+ } catch {
282
+ return;
283
+ }
284
+ }
285
+ throw new Error(`Incompatible intercom broker ${pid} did not stop within ${timeoutMs}ms`);
286
+ }
287
+
288
+ async function isBrokerRunning(): Promise<boolean> {
289
+ if (await checkSocketConnectable()) {
290
+ return true;
291
+ }
292
+
293
+ if (!existsSync(BROKER_PID)) return false;
294
+
295
+ try {
296
+ const pid = parseInt(readFileSync(BROKER_PID, "utf-8").trim(), 10);
297
+ if (!Number.isFinite(pid)) return false;
298
+ process.kill(pid, 0);
299
+ return checkSocketConnectable();
300
+ } catch {
301
+ // Missing or unreadable PID state means there is no live broker to reuse.
302
+ return false;
303
+ }
304
+ }
305
+
306
+ function connectToBrokerTarget(target: BrokerConnectTarget): net.Socket {
307
+ return typeof target === "string"
308
+ ? net.connect(target)
309
+ : net.connect({ host: target.host, port: target.port });
310
+ }
311
+
312
+ type BrokerHealth = "compatible" | "incompatible" | "unreachable";
313
+
314
+ async function checkSocketConnectable(): Promise<boolean> {
315
+ return await checkBrokerHealth() === "compatible";
316
+ }
317
+
318
+ function checkBrokerHealth(): Promise<BrokerHealth> {
319
+ return new Promise((resolve) => {
320
+ let target: BrokerConnectTarget;
321
+ try {
322
+ target = getBrokerConnectTarget();
323
+ } catch {
324
+ resolve("unreachable");
325
+ return;
326
+ }
327
+
328
+ const socket = connectToBrokerTarget(target);
329
+ const requestId = randomUUID();
330
+ const expectedStateId = typeof target === "string" ? undefined : target.stateId;
331
+ let settled = false;
332
+ const finish = (health: BrokerHealth) => {
333
+ if (settled) {
334
+ return;
335
+ }
336
+ settled = true;
337
+ clearTimeout(timeout);
338
+ socket.off("connect", onConnect);
339
+ socket.off("error", onError);
340
+ socket.off("data", reader);
341
+ socket.destroy();
342
+ resolve(health);
343
+ };
344
+ const onConnect = () => {
345
+ try {
346
+ writeMessage(socket, {
347
+ type: "health",
348
+ requestId,
349
+ ...(expectedStateId ? { stateId: expectedStateId } : {}),
350
+ });
351
+ } catch {
352
+ finish("unreachable");
353
+ }
354
+ };
355
+ const onError = () => finish("unreachable");
356
+ const reader = createMessageReader((message) => {
357
+ if (isBrokerHealthOkMessage(message, requestId)) {
358
+ finish("compatible");
359
+ return;
360
+ }
361
+ if (
362
+ typeof message === "object"
363
+ && message !== null
364
+ && "type" in message
365
+ && message.type === "health_ok"
366
+ && "requestId" in message
367
+ && message.requestId === requestId
368
+ ) {
369
+ finish("incompatible");
370
+ return;
371
+ }
372
+ finish("unreachable");
373
+ }, () => finish("unreachable"));
374
+ socket.on("connect", onConnect);
375
+ socket.on("error", onError);
376
+ socket.on("data", reader);
377
+ const timeout = setTimeout(() => finish("unreachable"), 1000);
378
+ });
379
+ }
380
+
381
+ function acquireSpawnLock(): boolean {
382
+ const maxRetries = 5;
383
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
384
+ try {
385
+ writeFileSync(BROKER_SPAWN_LOCK, `${process.pid}\n${Date.now()}\n`, {
386
+ flag: "wx",
387
+ mode: INTERCOM_RUNTIME_FILE_MODE,
388
+ });
389
+ restrictIntercomRuntimeFile(BROKER_SPAWN_LOCK);
390
+ return true;
391
+ } catch (error) {
392
+ if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") {
393
+ throw error;
394
+ }
395
+ if (isSpawnLockStale()) {
396
+ try {
397
+ unlinkSync(BROKER_SPAWN_LOCK);
398
+ } catch {
399
+ // If we can't delete the stale lock, retry a few times before giving up
400
+ }
401
+ continue;
402
+ }
403
+ return false;
404
+ }
405
+ }
406
+ return false;
407
+ }
408
+
409
+ function isSpawnLockStale(): boolean {
410
+ if (!existsSync(BROKER_SPAWN_LOCK)) {
411
+ return false;
412
+ }
413
+
414
+ try {
415
+ const [pidLine = "", createdAtLine = "0"] = readFileSync(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
416
+ const pid = Number.parseInt(pidLine, 10);
417
+ const createdAt = Number.parseInt(createdAtLine, 10);
418
+ const ageMs = Date.now() - createdAt;
419
+
420
+ if (Number.isFinite(pid)) {
421
+ try {
422
+ process.kill(pid, 0);
423
+ } catch {
424
+ // The process that created the lock is gone.
425
+ return true;
426
+ }
427
+ }
428
+
429
+ return !Number.isFinite(createdAt) || ageMs > 10_000;
430
+ } catch {
431
+ // Unreadable lock contents are treated as stale so a new broker can start.
432
+ return true;
433
+ }
434
+ }
435
+
436
+ function releaseSpawnLock(): void {
437
+ try {
438
+ unlinkSync(BROKER_SPAWN_LOCK);
439
+ } catch {
440
+ // Another cleanup path may already have removed the lock.
441
+ }
442
+ }
443
+
444
+ async function waitForBroker(timeoutMs = 5000): Promise<void> {
445
+ const start = Date.now();
446
+ while (Date.now() - start < timeoutMs) {
447
+ if (await checkSocketConnectable()) {
448
+ return;
449
+ }
450
+ await sleep(100);
451
+ }
452
+ throw new Error("Broker failed to start within timeout");
453
+ }