cotal-ai 0.92.2 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +17 -17
  2. package/seeded-connectors/claude/README.md +5 -2
  3. package/seeded-connectors/claude/dist/app-host.d.ts +2 -5
  4. package/seeded-connectors/claude/dist/app-host.d.ts.map +1 -1
  5. package/seeded-connectors/claude/dist/claude-events.d.ts +8 -0
  6. package/seeded-connectors/claude/dist/claude-events.d.ts.map +1 -0
  7. package/seeded-connectors/claude/dist/hook.cjs +264 -13
  8. package/seeded-connectors/claude/dist/hooks.d.ts +5 -0
  9. package/seeded-connectors/claude/dist/hooks.d.ts.map +1 -1
  10. package/seeded-connectors/claude/dist/index.js +37 -7
  11. package/seeded-connectors/claude/dist/mcp.cjs +1061 -416
  12. package/seeded-connectors/claude/dist/mcp.d.ts.map +1 -1
  13. package/seeded-connectors/claude/dist/monitor.cjs +286 -9
  14. package/seeded-connectors/claude/dist/native-binding.d.ts +7 -0
  15. package/seeded-connectors/claude/dist/native-binding.d.ts.map +1 -0
  16. package/seeded-connectors/claude/dist/native-deliveries.d.ts +31 -0
  17. package/seeded-connectors/claude/dist/native-deliveries.d.ts.map +1 -0
  18. package/seeded-connectors/claude/dist/wake-feed.d.ts +11 -3
  19. package/seeded-connectors/claude/dist/wake-feed.d.ts.map +1 -1
  20. package/seeded-connectors/claude/hooks/native-wake.js +204 -19
  21. package/seeded-connectors/claude/package.json +2 -2
  22. package/seeded-connectors/codex/dist/host.js +313 -183
  23. package/seeded-connectors/codex/dist/index.js +37 -7
  24. package/seeded-connectors/codex/package.json +1 -1
  25. package/seeded-connectors/hermes/dist/index.js +37 -7
  26. package/seeded-connectors/hermes/dist/launch.js +294 -174
  27. package/seeded-connectors/hermes/package.json +1 -1
  28. package/seeded-connectors/hermes/plugin/cotal/_sidecar/standalone.cjs +294 -174
  29. package/seeded-connectors/jcode/dist/host.js +313 -183
  30. package/seeded-connectors/jcode/dist/index.js +37 -7
  31. package/seeded-connectors/jcode/dist/mcp.js +103 -24
  32. package/seeded-connectors/jcode/package.json +1 -1
  33. package/seeded-connectors/opencode/dist/index.js +37 -7
  34. package/seeded-connectors/opencode/dist/plugin/index.js +313 -183
  35. package/seeded-connectors/opencode/package.json +1 -1
  36. package/seeded-connectors/pi/dist/index.js +290 -180
  37. package/seeded-connectors/pi/dist/standalone.js +313 -183
  38. package/seeded-connectors/pi/package.json +1 -1
  39. package/seeded-connectors/web/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cotal-ai",
3
- "version": "0.92.2",
3
+ "version": "0.94.0",
4
4
  "description": "Cotal — lateral agent coordination over NATS. Run `npx cotal-ai` for the CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -33,25 +33,25 @@
33
33
  "node": ">=22"
34
34
  },
35
35
  "dependencies": {
36
- "@cotal-ai/auth": "0.92.2",
37
- "@cotal-ai/cli": "0.92.2",
38
- "@cotal-ai/connector-core": "0.92.2",
39
- "@cotal-ai/core": "0.92.2",
40
- "@cotal-ai/delivery": "0.92.2",
41
- "@cotal-ai/manager": "0.92.2",
42
- "@cotal-ai/runtime": "0.92.2",
43
- "@cotal-ai/workspace": "0.92.2"
36
+ "@cotal-ai/auth": "0.94.0",
37
+ "@cotal-ai/cli": "0.94.0",
38
+ "@cotal-ai/connector-core": "0.94.0",
39
+ "@cotal-ai/core": "0.94.0",
40
+ "@cotal-ai/delivery": "0.94.0",
41
+ "@cotal-ai/manager": "0.94.0",
42
+ "@cotal-ai/runtime": "0.94.0",
43
+ "@cotal-ai/workspace": "0.94.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@cotal-ai/connector-claude-code": "0.92.2",
47
- "@cotal-ai/connector-codex": "0.92.2",
48
- "@cotal-ai/connector-hermes": "0.92.2",
49
- "@cotal-ai/connector-jcode": "0.92.2",
50
- "@cotal-ai/connector-opencode": "0.92.2",
51
- "@cotal-ai/linear": "0.92.2",
52
- "@cotal-ai/pi": "0.92.2",
46
+ "@cotal-ai/connector-claude-code": "0.94.0",
47
+ "@cotal-ai/connector-codex": "0.94.0",
48
+ "@cotal-ai/connector-hermes": "0.94.0",
49
+ "@cotal-ai/connector-jcode": "0.94.0",
50
+ "@cotal-ai/connector-opencode": "0.94.0",
51
+ "@cotal-ai/linear": "0.94.0",
52
+ "@cotal-ai/pi": "0.94.0",
53
53
  "@cotal-ai/smoke-kit": "0.0.0",
54
- "@cotal-ai/web": "0.92.2",
54
+ "@cotal-ai/web": "0.94.0",
55
55
  "@nats-io/jetstream": "^3.4.0",
56
56
  "@nats-io/transport-node": "^3.4.0",
57
57
  "typescript": "^5.9.3"
@@ -18,8 +18,11 @@ Claude Code documents every hook event and its matchers in its
18
18
  The plugin registers the nine hooks below in [`hooks/hooks.json`](hooks/hooks.json). Each runs
19
19
  `dist/hook.cjs`, which relays the event to the session's connector over its authenticated control
20
20
  socket. A Cotal session, one that Cotal launched, carries a mesh identity in its environment. A
21
- plain session, such as a `claude` you started yourself, carries none and has no connector, so there
22
- the hook only runs the hosted connection checks in the `PreToolUse` and `PostToolUse` rows. The presence states are described in
21
+ plain session, such as a `claude` you started yourself, carries none until it connects. Its command
22
+ hooks capture session and transcript metadata, then relay lifecycle events to the personal gateway
23
+ after the native module binds that session. This does not intercept the `classic.*` events that
24
+ Claude's built-in security guard withholds from user plugins. The command hook still enforces the
25
+ hosted connection checks in the `PreToolUse` and `PostToolUse` rows. The presence states are described in
23
26
  [presence mapping](../../docs/connect-claude.md#presence-mapping).
24
27
 
25
28
  | Hook | Matcher | What the plugin does |
@@ -1,16 +1,13 @@
1
1
  import { type GatewayHost } from "@cotal-ai/connector-core";
2
2
  export interface AppHostOptions {
3
- /** The argv that runs the CLI. Read when a conversation connects, so a server whose record is
4
- * missing still answers MCP and refuses `cotal_connect` with the remedy. */
5
3
  cotal: () => readonly string[];
6
- /** Where `app-identity` runs. A Claude Code session passes its own directory, so a project's mesh
7
- * is found as `cotal spawn` finds it; Claude Desktop passes a directory that is no project. */
8
4
  cwd: string;
9
5
  env: NodeJS.ProcessEnv;
10
6
  }
11
7
  export interface AppHost extends GatewayHost {
12
- /** Disconnect and retire every connection. Safe to call more than once. */
13
8
  closeAll(): Promise<void>;
9
+ nativeEvent(event: Record<string, unknown>): Promise<void>;
10
+ sessionFor(id: string): string | undefined;
14
11
  }
15
12
  export declare function createAppHost(o: AppHostOptions): AppHost;
16
13
  //# sourceMappingURL=app-host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-host.d.ts","sourceRoot":"","sources":["../src/app-host.ts"],"names":[],"mappings":"AASA,OAAO,EAA+D,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAMzH,MAAM,WAAW,cAAc;IAC7B;iFAC6E;IAC7E,KAAK,EAAE,MAAM,SAAS,MAAM,EAAE,CAAC;IAC/B;oGACgG;IAChG,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,OAAQ,SAAQ,WAAW;IAC1C,2EAA2E;IAC3E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAkBD,wBAAgB,aAAa,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CA0ExD"}
1
+ {"version":3,"file":"app-host.d.ts","sourceRoot":"","sources":["../src/app-host.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiH,KAAK,WAAW,EAAkB,MAAM,0BAA0B,CAAC;AAS3L,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,SAAS,MAAM,EAAE,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,OAAQ,SAAQ,WAAW;IAC1C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC5C;AA4CD,wBAAgB,aAAa,CAAC,CAAC,EAAE,cAAc,GAAG,OAAO,CAmJxD"}
@@ -0,0 +1,8 @@
1
+ import { AguiEmitterHolder, type AgentConfig, type DurableSource, type MeshAgent } from "@cotal-ai/connector-core";
2
+ import { type ClaudeEntry } from "./agui-map.js";
3
+ /** The managed and personal native paths use the same transcript, WAL and recovery semantics. */
4
+ export declare function createClaudeEventPlane(agent: MeshAgent, config: AgentConfig, env: NodeJS.ProcessEnv, initialSource?: DurableSource<ClaudeEntry>): {
5
+ events: AguiEmitterHolder<ClaudeEntry, unknown>;
6
+ stop(): void;
7
+ };
8
+ //# sourceMappingURL=claude-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-events.d.ts","sourceRoot":"","sources":["../src/claude-events.ts"],"names":[],"mappings":"AAGA,OAAO,EACQ,iBAAiB,EACa,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAChG,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAsB,KAAK,WAAW,EAAqB,MAAM,eAAe,CAAC;AAGxF,iGAAiG;AACjG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAClG,aAAa,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG;IAAE,MAAM,EAAE,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,IAAI,IAAI,CAAA;CAAE,CA0C/G"}
@@ -4,10 +4,133 @@
4
4
  var import_node_net = require("node:net");
5
5
 
6
6
  // ../../packages/core/dist/launch-material.js
7
- var import_node_fs = require("node:fs");
7
+ var import_node_fs2 = require("node:fs");
8
8
 
9
9
  // ../../packages/core/dist/secret-fs.js
10
+ var import_node_fs = require("node:fs");
11
+ var import_node_child_process = require("node:child_process");
12
+ var import_node_path = require("node:path");
10
13
  var isWin = process.platform === "win32";
14
+ function sys32(exe) {
15
+ const root = process.env.SystemRoot || process.env.windir || "C:\\Windows";
16
+ return (0, import_node_path.join)(root, "System32", exe);
17
+ }
18
+ var cachedSid;
19
+ function ownerSid() {
20
+ if (cachedSid)
21
+ return cachedSid;
22
+ const out = (0, import_node_child_process.execFileSync)(sys32("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
23
+ const sid = out.trim().split(",").pop()?.replace(/^"|"$/g, "").trim();
24
+ if (!sid || !/^S-1-/.test(sid))
25
+ throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);
26
+ cachedSid = sid;
27
+ return sid;
28
+ }
29
+ var BROAD_SIDS = ["S-1-1-0", "S-1-5-11", "S-1-5-32-545"];
30
+ function hardenPrivate(path, kind) {
31
+ if (!isWin) {
32
+ if (kind === "file")
33
+ (0, import_node_fs.closeSync)(openPrivate(path, import_node_fs.constants.O_RDONLY));
34
+ else
35
+ (0, import_node_fs.chmodSync)(path, 448);
36
+ return;
37
+ }
38
+ const perm = kind === "dir" ? "(OI)(CI)(F)" : "(F)";
39
+ const removeBroad = BROAD_SIDS.flatMap((sid) => ["/remove:g", `*${sid}`]);
40
+ const grant = (sid) => ["/grant:r", `*${sid}:${perm}`];
41
+ try {
42
+ (0, import_node_child_process.execFileSync)(sys32("icacls.exe"), [
43
+ path,
44
+ "/inheritance:r",
45
+ ...removeBroad,
46
+ ...grant(ownerSid()),
47
+ ...grant("S-1-5-18"),
48
+ ...grant("S-1-5-32-544")
49
+ ], { stdio: ["ignore", "ignore", "pipe"] });
50
+ } catch (e) {
51
+ throw new Error(`failed to harden ${kind} "${path}" to private via icacls: ${e.message.trim()}. Cotal will not leave a secret with a permissive ACL - ensure the path is on an NTFS volume and %SystemRoot%\\System32\\icacls.exe is available.`);
52
+ }
53
+ }
54
+ function notPrivate(path, why) {
55
+ const refused = new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);
56
+ refused.code = "EPERM";
57
+ return refused;
58
+ }
59
+ function openPrivate(path, flags) {
60
+ if (isWin) {
61
+ const fd2 = (0, import_node_fs.openSync)(path, flags, 384);
62
+ try {
63
+ (0, import_node_fs.fchmodSync)(fd2, 384);
64
+ } catch (e) {
65
+ (0, import_node_fs.closeSync)(fd2);
66
+ throw e;
67
+ }
68
+ return fd2;
69
+ }
70
+ let fd;
71
+ try {
72
+ fd = (0, import_node_fs.openSync)(path, flags | import_node_fs.constants.O_NOFOLLOW | import_node_fs.constants.O_NONBLOCK, 384);
73
+ } catch (e) {
74
+ if (e.code === "ELOOP")
75
+ throw notPrivate(path, "is a symlink");
76
+ throw e;
77
+ }
78
+ try {
79
+ const info = (0, import_node_fs.fstatSync)(fd);
80
+ if (!info.isFile())
81
+ throw notPrivate(path, "is not a regular file");
82
+ if (info.uid !== process.getuid())
83
+ throw notPrivate(path, `is owned by uid ${info.uid}, not this user`);
84
+ (0, import_node_fs.fchmodSync)(fd, 384);
85
+ return fd;
86
+ } catch (e) {
87
+ (0, import_node_fs.closeSync)(fd);
88
+ throw e;
89
+ }
90
+ }
91
+ function writeSecretFile(path, data) {
92
+ writePrivate(path, data, false);
93
+ }
94
+ function writeBytes(path, data, exclusive) {
95
+ const fd = openPrivate(path, import_node_fs.constants.O_WRONLY | import_node_fs.constants.O_CREAT | (exclusive ? import_node_fs.constants.O_EXCL : 0));
96
+ try {
97
+ (0, import_node_fs.ftruncateSync)(fd);
98
+ (0, import_node_fs.writeFileSync)(fd, data);
99
+ } finally {
100
+ (0, import_node_fs.closeSync)(fd);
101
+ }
102
+ }
103
+ function writePrivate(path, data, exclusive) {
104
+ writeBytes(path, data, exclusive);
105
+ if (!isWin)
106
+ return;
107
+ try {
108
+ hardenPrivate(path, "file");
109
+ } catch (e) {
110
+ try {
111
+ (0, import_node_fs.unlinkSync)(path);
112
+ } catch {
113
+ }
114
+ throw e;
115
+ }
116
+ }
117
+ function writeSecretFileAtomic(path, data) {
118
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
119
+ try {
120
+ writeSecretFile(tmp, data);
121
+ (0, import_node_fs.renameSync)(tmp, path);
122
+ } catch (e) {
123
+ try {
124
+ (0, import_node_fs.unlinkSync)(tmp);
125
+ } catch {
126
+ }
127
+ throw e;
128
+ }
129
+ }
130
+ function mkSecretDir(path) {
131
+ (0, import_node_fs.mkdirSync)(path, { recursive: true, mode: 448 });
132
+ hardenPrivate(path, "dir");
133
+ }
11
134
 
12
135
  // ../../packages/core/dist/launch-material.js
13
136
  var LAUNCH_MATERIAL_ENV = "COTAL_LAUNCH_MATERIAL";
@@ -15,11 +138,11 @@ function readLaunchMaterial(path) {
15
138
  let raw;
16
139
  try {
17
140
  if (process.platform !== "win32") {
18
- const mode = (0, import_node_fs.statSync)(path).mode & 511;
141
+ const mode = (0, import_node_fs2.statSync)(path).mode & 511;
19
142
  if (mode & 63)
20
143
  throw new Error(`is readable beyond its owner (mode ${mode.toString(8)}) - refusing to read connection material out of a file other local users can open`);
21
144
  }
22
- raw = (0, import_node_fs.readFileSync)(path, "utf8");
145
+ raw = (0, import_node_fs2.readFileSync)(path, "utf8");
23
146
  } catch (e) {
24
147
  throw new Error(`launch material: cannot read ${path} (${e instanceof Error ? e.message : String(e)})`);
25
148
  }
@@ -170,9 +293,9 @@ function done(out, confirm) {
170
293
  });
171
294
  setTimeout(exit, 1e3);
172
295
  }
173
- async function runHookRelay(answer) {
296
+ async function runHookRelay(answer, observe) {
174
297
  const managed = hasIdentity();
175
- if (!managed && !answer)
298
+ if (!managed && !answer && !observe)
176
299
  return done("");
177
300
  const raw = (await readStdin()).trim() || "{}";
178
301
  let event = {};
@@ -187,6 +310,10 @@ async function runHookRelay(answer) {
187
310
  }
188
311
  if (local)
189
312
  return done(JSON.stringify(local));
313
+ try {
314
+ await observe?.(event, { managed });
315
+ } catch {
316
+ }
190
317
  if (!managed)
191
318
  return done("");
192
319
  let control;
@@ -309,8 +436,8 @@ var otherCotalPath = (event, session) => {
309
436
  };
310
437
 
311
438
  // src/connection-owner.ts
312
- var import_node_fs2 = require("node:fs");
313
- var import_node_path = require("node:path");
439
+ var import_node_fs3 = require("node:fs");
440
+ var import_node_path2 = require("node:path");
314
441
  var CONNECTED = /^connection_id: (\S+)$/m;
315
442
  var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
316
443
  var FORKED_REASON = "This connection_id was opened by another Claude Code session. This session is a fork or a copy of that one, so it needs its own connection: call cotal_connect, then use the connection_id it returns.";
@@ -324,7 +451,7 @@ function connectedId(response) {
324
451
  return void 0;
325
452
  }
326
453
  function connectionOwner(dataDir) {
327
- const dir = dataDir ? (0, import_node_path.join)(dataDir, "hosted-connections") : void 0;
454
+ const dir = dataDir ? (0, import_node_path2.join)(dataDir, "hosted-connections") : void 0;
328
455
  return (event, session) => {
329
456
  if (session.managed || !dir) return void 0;
330
457
  const ev = event;
@@ -335,7 +462,7 @@ function connectionOwner(dataDir) {
335
462
  const id = typeof given === "string" && SAFE_ID.test(given) ? given : void 0;
336
463
  const owner2 = () => {
337
464
  try {
338
- return (0, import_node_fs2.readFileSync)((0, import_node_path.join)(dir, id), "utf8");
465
+ return (0, import_node_fs3.readFileSync)((0, import_node_path2.join)(dir, id), "utf8");
339
466
  } catch {
340
467
  return void 0;
341
468
  }
@@ -343,13 +470,13 @@ function connectionOwner(dataDir) {
343
470
  if (ev?.hook_event_name === "PostToolUse" && tool === "cotal_connect") {
344
471
  const opened = connectedId(ev.tool_response);
345
472
  if (opened === void 0) return void 0;
346
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
347
- (0, import_node_fs2.writeFileSync)((0, import_node_path.join)(dir, opened), sessionId, { mode: 384 });
473
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true, mode: 448 });
474
+ (0, import_node_fs3.writeFileSync)((0, import_node_path2.join)(dir, opened), sessionId, { mode: 384 });
348
475
  return void 0;
349
476
  }
350
477
  if (id === void 0) return void 0;
351
478
  if (ev?.hook_event_name === "PostToolUse" && tool === "cotal_disconnect") {
352
- if (owner2() === sessionId) (0, import_node_fs2.rmSync)((0, import_node_path.join)(dir, id), { force: true });
479
+ if (owner2() === sessionId) (0, import_node_fs3.rmSync)((0, import_node_path2.join)(dir, id), { force: true });
353
480
  return void 0;
354
481
  }
355
482
  if (ev?.hook_event_name !== "PreToolUse") return void 0;
@@ -365,6 +492,130 @@ function connectionOwner(dataDir) {
365
492
  };
366
493
  }
367
494
 
495
+ // src/native-binding.ts
496
+ var import_node_fs5 = require("node:fs");
497
+ var import_node_net2 = require("node:net");
498
+ var import_node_os2 = require("node:os");
499
+ var import_node_path4 = require("node:path");
500
+
501
+ // src/wake-feed.ts
502
+ var import_node_fs4 = require("node:fs");
503
+ var import_node_os = require("node:os");
504
+ var import_node_path3 = require("node:path");
505
+ var WAKE_ID = /^[A-Za-z0-9_-]{1,128}$/;
506
+ var WIN_PIPES = "\\\\.\\pipe\\";
507
+ var WIN_PREFIX = "cotal-claude-wake-";
508
+ function wakeDir() {
509
+ if (process.platform === "win32") return WIN_PIPES;
510
+ const uid = process.getuid();
511
+ const dir = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), `cotal-claude-wake-${uid}`);
512
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
513
+ const st = (0, import_node_fs4.lstatSync)(dir);
514
+ if (!st.isDirectory() || st.uid !== uid || (st.mode & 63) !== 0) throw new Error(`${dir} is not a private directory of this user's`);
515
+ return dir;
516
+ }
517
+ var entryName = (claudePid, serverPid) => process.platform === "win32" ? `${WIN_PREFIX}${claudePid}-${serverPid}` : `${claudePid}-${serverPid}.sock`;
518
+ var ENTRY = process.platform === "win32" ? new RegExp(`^${WIN_PREFIX}(\\d+)-(\\d+)$`) : /^(\d+)-(\d+)\.sock$/;
519
+ function alive(pid) {
520
+ try {
521
+ process.kill(pid, 0);
522
+ return true;
523
+ } catch (e) {
524
+ return e.code === "EPERM";
525
+ }
526
+ }
527
+ function wakeSocketPath(claudePid, serverPid = process.pid) {
528
+ const dir = wakeDir();
529
+ return process.platform === "win32" ? `${dir}${entryName(claudePid, serverPid)}` : (0, import_node_path3.join)(dir, entryName(claudePid, serverPid));
530
+ }
531
+ function wakeSockets(claudePid) {
532
+ const dir = wakeDir();
533
+ return (0, import_node_fs4.readdirSync)(dir).flatMap((name) => {
534
+ const m = ENTRY.exec(name);
535
+ return m?.[1] === String(claudePid) && alive(Number(m[2])) ? [wakeSocketPath(claudePid, Number(m[2]))] : [];
536
+ });
537
+ }
538
+ function nativeSocketPath(feed) {
539
+ return `${feed}.native`;
540
+ }
541
+
542
+ // src/native-binding.ts
543
+ function stateFile(session) {
544
+ if (!WAKE_ID.test(session)) throw new Error("invalid native session identity");
545
+ const root = (0, import_node_path4.join)((0, import_node_os2.tmpdir)(), `cotal-claude-native-${process.getuid?.() ?? "local"}`);
546
+ const st = (0, import_node_fs5.lstatSync)(root, { throwIfNoEntry: false });
547
+ if (st && (!st.isDirectory() || process.getuid && st.uid !== process.getuid()))
548
+ throw new Error("native session state is not this user's private directory");
549
+ mkSecretDir(root);
550
+ return (0, import_node_path4.join)(root, `${session}.json`);
551
+ }
552
+ function readRecord(file) {
553
+ const st = (0, import_node_fs5.lstatSync)(file, { throwIfNoEntry: false });
554
+ if (!st) return void 0;
555
+ if (!st.isFile() || st.size > 16384 || process.getuid && (st.uid !== process.getuid() || (st.mode & 63) !== 0))
556
+ throw new Error("native session state is not an owner-private bounded regular file");
557
+ const value = JSON.parse((0, import_node_fs5.readFileSync)(file, "utf8"));
558
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid native session state");
559
+ return value;
560
+ }
561
+ function transcriptMatches(session, path) {
562
+ return typeof path === "string" && (0, import_node_path4.isAbsolute)(path) && (0, import_node_path4.basename)(path) === `${session}.jsonl`;
563
+ }
564
+ async function forward(event, hostPid) {
565
+ const paths = wakeSockets(hostPid);
566
+ if (!paths.length) throw new Error("no local native lifecycle server");
567
+ await Promise.all(paths.map((path) => new Promise((resolve, reject) => {
568
+ const socket = (0, import_node_net2.createConnection)(nativeSocketPath(path));
569
+ let reply = "";
570
+ socket.setEncoding("utf8");
571
+ socket.setTimeout(2e3, () => socket.destroy(new Error("native lifecycle relay timed out")));
572
+ socket.on("connect", () => socket.write(`${JSON.stringify({ op: "native", event })}
573
+ `));
574
+ socket.on("data", (data) => {
575
+ reply += data;
576
+ if (reply.length > 128) socket.destroy();
577
+ });
578
+ socket.on("error", reject);
579
+ socket.on("close", () => {
580
+ try {
581
+ if (JSON.parse(reply).ok === true) resolve();
582
+ else reject(new Error("native lifecycle binding refused"));
583
+ } catch {
584
+ reject(new Error("native lifecycle binding was not confirmed"));
585
+ }
586
+ });
587
+ })));
588
+ }
589
+ async function observePersonalHook(value) {
590
+ if (!value || typeof value !== "object") return;
591
+ const event = value;
592
+ if (event.agent_id !== void 0 || typeof event.session_id !== "string" || !WAKE_ID.test(event.session_id) || !transcriptMatches(event.session_id, event.transcript_path) || typeof event.hook_event_name !== "string") return;
593
+ if (!["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolBatch", "Notification", "Stop", "StopFailure", "SessionEnd"].includes(event.hook_event_name)) return;
594
+ const file = stateFile(event.session_id);
595
+ const previous = readRecord(file);
596
+ if (previous && previous.transcript_path !== event.transcript_path)
597
+ throw new Error("native command hook changed its session transcript");
598
+ const safe = {};
599
+ for (const key of ["hook_event_name", "session_id", "transcript_path", "cwd", "source", "notification_type", "error", "model"])
600
+ if (typeof event[key] === "string" && event[key].length <= 4096) safe[key] = event[key];
601
+ if (JSON.stringify(safe).length > 8192) throw new Error("native command-hook metadata is too large");
602
+ writeSecretFileAtomic(file, JSON.stringify(safe));
603
+ const target = readRecord(`${file}.host`);
604
+ if (target) {
605
+ if (!Number.isInteger(target.hostPid) || target.hostPid <= 0) throw new Error("invalid native lifecycle host");
606
+ await forward(safe, target.hostPid);
607
+ }
608
+ if (event.hook_event_name === "SessionEnd") {
609
+ (0, import_node_fs5.unlinkSync)(file);
610
+ if (target) (0, import_node_fs5.unlinkSync)(`${file}.host`);
611
+ }
612
+ }
613
+
368
614
  // src/hook.ts
369
615
  var owner = connectionOwner(process.env.CLAUDE_PLUGIN_DATA);
370
- void runHookRelay((event, session) => otherCotalPath(event, session) ?? owner(event, session)).catch(() => process.exit(0));
616
+ void runHookRelay(
617
+ (event, session) => otherCotalPath(event, session) ?? owner(event, session),
618
+ async (event, session) => {
619
+ if (!session.managed) await observePersonalHook(event);
620
+ }
621
+ ).catch(() => process.exit(0));
@@ -8,11 +8,16 @@ export interface ClaudeHandleDeps {
8
8
  events?: () => AguiEmitterHolder<ClaudeEntry, unknown> | undefined;
9
9
  /** Told which inbox items a frame surfaced; pass {@link WakePolicy.surfaced}. */
10
10
  surfaced?: (items: readonly InboxItem[]) => void;
11
+ /** False when native ingress owns delivery and these hooks observe lifecycle only. */
12
+ delivery?: boolean;
11
13
  }
12
14
  /** The hook side of the connector: the handler plus the delivery callback that commits what it
13
15
  * injected. Both must be wired into {@link startControlServer} — the handler surfaces, the
14
16
  * callback is the only place a peer message is ever acked. */
15
17
  export interface ClaudeHooks {
18
+ /** Adopt a native transcript when a personal connection starts mid-session. No synthetic
19
+ * lifecycle event is needed, and the source decides the pre-connection privacy boundary. */
20
+ adoptTranscript(path: string, source: unknown): void;
16
21
  handle: HookHandle;
17
22
  /** Pass as {@link ControlServerOpts.onReply}. */
18
23
  onReply: (ev: HookEvent, delivered: boolean) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAcA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA+EjD,MAAM,WAAW,gBAAgB;IAC/B,mGAAmG;IACnG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACnE,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,KAAK,IAAI,CAAC;CAClD;AAOD;;+DAE+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,gBAAqB,GAAG,WAAW,CAyU3E;AAED,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5H,MAAM,WAAW,UAAU;IACzB,+EAA+E;IAC/E,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACxC;+FAC2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5C,sCAAsC;IACtC,IAAI,IAAI,IAAI,CAAC;CACd;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAe,GAAG,UAAU,CAsH3H"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAcA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,UAAU,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA+EjD,MAAM,WAAW,gBAAgB;IAC/B,mGAAmG;IACnG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACnE,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,KAAK,IAAI,CAAC;IACjD,sFAAsF;IACtF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAOD;;+DAE+D;AAC/D,MAAM,WAAW,WAAW;IAC1B;gGAC4F;IAC5F,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACrD,MAAM,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,gBAAqB,GAAG,WAAW,CA2U3E;AAED,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5H,MAAM,WAAW,UAAU;IACzB,+EAA+E;IAC/E,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACxC;+FAC2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5C,sCAAsC;IACtC,IAAI,IAAI,IAAI,CAAC;CACd;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAe,GAAG,UAAU,CAsH3H"}
@@ -6906,7 +6906,7 @@ import { DEFAULT_SERVER, DEV_OWNER, LAUNCH_MATERIAL_ENV as LAUNCH_MATERIAL_ENV2,
6906
6906
  import { LAUNCH_MATERIAL_ENV, readLaunchMaterial } from "@cotal-ai/core/launch-material";
6907
6907
 
6908
6908
  // ../connector-core/dist/agent.js
6909
- import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, resolvePeer as resolvePeerInRoster, peerLabel as peerLabel2, CotalEndpoint, acquireLocalConsumer, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken2, EpEnvelopeError as EpEnvelopeError2, isPublishPermissionDenied, unansweredRequest, controlReplyFrom, controlReplyFromThrown, runAgentBearer, partsToText, assertValidRole } from "@cotal-ai/core";
6909
+ import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, eventChannelPrincipal, resolvePeer as resolvePeerInRoster, peerLabel as peerLabel2, CotalEndpoint, acquireLocalConsumer, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken2, EpEnvelopeError as EpEnvelopeError2, isPublishPermissionDenied, unansweredRequest, controlReplyFrom, controlReplyFromThrown, runAgentBearer, partsToText, assertValidRole } from "@cotal-ai/core";
6910
6910
 
6911
6911
  // ../connector-core/dist/framing.js
6912
6912
  import { peerLabel } from "@cotal-ai/core/peer-label";
@@ -6937,7 +6937,7 @@ function controlEndpoint(space, name, token = randomBytes(32).toString("base64ur
6937
6937
  // ../connector-core/dist/launch.js
6938
6938
  import { eventChannel as eventChannel2, LAUNCH_MATERIAL_ENV as LAUNCH_MATERIAL_ENV3, parsePrincipalKey, principalKey, writeLaunchMaterial } from "@cotal-ai/core";
6939
6939
  import { dirname } from "node:path";
6940
- import { EVENT_CHANNEL_PREFIX, eventChannel as eventChannel3, eventChannelPrincipal, isEventChannel } from "@cotal-ai/core";
6940
+ import { EVENT_CHANNEL_PREFIX, eventChannel as eventChannel3, eventChannelPrincipal as eventChannelPrincipal2, isEventChannel } from "@cotal-ai/core";
6941
6941
  var OS_ENV_ALLOW = [
6942
6942
  "PATH",
6943
6943
  "HOME",
@@ -7264,6 +7264,9 @@ function registerAguiFramePartRenderer() {
7264
7264
  }
7265
7265
  registerAguiFramePartRenderer();
7266
7266
 
7267
+ // ../connector-core/dist/control.js
7268
+ var MAX_FRAME_BYTES = 1 << 20;
7269
+
7267
7270
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
7268
7271
  var external_exports = {};
7269
7272
  __export(external_exports, {
@@ -23524,15 +23527,42 @@ if (typeof global.crypto === "undefined") {
23524
23527
  var MAX_DRAIN_BYTES = 64 * 1024 * 1024;
23525
23528
 
23526
23529
  // ../connector-core/dist/gateway.js
23527
- import { assertValidName } from "@cotal-ai/core";
23530
+ import { assertValidName, conversationSelection, SELECTION_MAX_ITEMS } from "@cotal-ai/core";
23531
+ var GATEWAY_TOOLS = /* @__PURE__ */ new Set([
23532
+ "cotal_orientation",
23533
+ "cotal_connection_status",
23534
+ "cotal_roster",
23535
+ "cotal_docs",
23536
+ "cotal_inbox",
23537
+ "cotal_send",
23538
+ "cotal_dm",
23539
+ "cotal_anycast",
23540
+ "cotal_status",
23541
+ "cotal_channel_info",
23542
+ "cotal_channels",
23543
+ "cotal_channel_mode",
23544
+ "cotal_join",
23545
+ "cotal_leave"
23546
+ ]);
23547
+ var NATIVE_GATEWAY_TOOLS = /* @__PURE__ */ new Set([
23548
+ ...GATEWAY_TOOLS,
23549
+ "cotal_spawn",
23550
+ "cotal_persona",
23551
+ "cotal_personas",
23552
+ "cotal_despawn",
23553
+ "cotal_run",
23554
+ "cotal_reconnect"
23555
+ ]);
23556
+ var SELECTION_SHAPE = {
23557
+ channels: external_exports.array(external_exports.strictObject({ channel: external_exports.string().min(1), access: external_exports.enum(["read", "read-post"]) })).max(SELECTION_MAX_ITEMS).optional().describe("Channels this connection may join and read, or read and post to. No wildcards."),
23558
+ role: external_exports.string().min(1).optional().describe("A role to hold, so role requests (cotal_anycast) reach this connection."),
23559
+ capabilities: external_exports.array(external_exports.enum(["spawn", "run"])).optional().describe("spawn: start and stop agents through a manager. run: start and manage workflow runs.")
23560
+ };
23528
23561
  var CONNECTION_ID = external_exports.string().min(1).describe("The connection_id cotal_connect returned in THIS conversation. Never reuse another conversation's.");
23529
23562
  var GATEWAY_LISTENING = "Messages reach you only when you call a tool: to listen, call cotal_wait, which returns as soon as a message arrives. When the person asks you to stay on Cotal or keep listening, call cotal_wait again each time it returns and handle each message as it arrives, until they ask you to stop.";
23530
- var GATEWAY_CONNECTING = "Cotal connects this conversation to the person's Cotal space. Call cotal_connect once with a short name for this conversation, then pass the returned connection_id on every other cotal tool call. Each conversation needs its own connection. Never reuse another conversation's connection_id.";
23563
+ var GATEWAY_CONNECTING = "Cotal connects this conversation to the person's Cotal space after they approve its permissions. Call cotal_connect with a short name. If approval is pending, show the person its link and wait for their decision; do not approve for them or poll. Then resume the same request as directed. Only a successful connection returns connection_id, required on every other cotal tool call. Each conversation needs its own connection. Never reuse another conversation's connection_id or approval_id.";
23531
23564
  var GATEWAY_INSTRUCTIONS = `${GATEWAY_CONNECTING} ${GATEWAY_LISTENING}`;
23532
23565
 
23533
- // ../connector-core/dist/control.js
23534
- var MAX_FRAME_BYTES = 1 << 20;
23535
-
23536
23566
  // src/cli-record.ts
23537
23567
  var CLI_RECORD = "cli.json";
23538
23568