cotal-ai 0.92.1 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/seeded-connectors/claude/README.md +5 -2
- package/seeded-connectors/claude/dist/app-host.d.ts +2 -5
- package/seeded-connectors/claude/dist/app-host.d.ts.map +1 -1
- package/seeded-connectors/claude/dist/claude-events.d.ts +8 -0
- package/seeded-connectors/claude/dist/claude-events.d.ts.map +1 -0
- package/seeded-connectors/claude/dist/hook.cjs +264 -13
- package/seeded-connectors/claude/dist/hooks.d.ts +5 -0
- package/seeded-connectors/claude/dist/hooks.d.ts.map +1 -1
- package/seeded-connectors/claude/dist/index.js +37 -7
- package/seeded-connectors/claude/dist/mcp.cjs +1066 -421
- package/seeded-connectors/claude/dist/mcp.d.ts.map +1 -1
- package/seeded-connectors/claude/dist/monitor.cjs +286 -9
- package/seeded-connectors/claude/dist/native-binding.d.ts +7 -0
- package/seeded-connectors/claude/dist/native-binding.d.ts.map +1 -0
- package/seeded-connectors/claude/dist/native-deliveries.d.ts +31 -0
- package/seeded-connectors/claude/dist/native-deliveries.d.ts.map +1 -0
- package/seeded-connectors/claude/dist/wake-feed.d.ts +11 -3
- package/seeded-connectors/claude/dist/wake-feed.d.ts.map +1 -1
- package/seeded-connectors/claude/hooks/native-wake.js +204 -19
- package/seeded-connectors/claude/package.json +2 -2
- package/seeded-connectors/codex/dist/host.js +318 -188
- package/seeded-connectors/codex/dist/index.js +37 -7
- package/seeded-connectors/codex/package.json +1 -1
- package/seeded-connectors/hermes/dist/index.js +37 -7
- package/seeded-connectors/hermes/dist/launch.js +299 -179
- package/seeded-connectors/hermes/package.json +1 -1
- package/seeded-connectors/hermes/plugin/cotal/_sidecar/standalone.cjs +299 -179
- package/seeded-connectors/jcode/dist/host.js +318 -188
- package/seeded-connectors/jcode/dist/index.js +37 -7
- package/seeded-connectors/jcode/dist/mcp.js +108 -29
- package/seeded-connectors/jcode/package.json +1 -1
- package/seeded-connectors/opencode/dist/index.js +37 -7
- package/seeded-connectors/opencode/dist/plugin/index.js +318 -188
- package/seeded-connectors/opencode/package.json +1 -1
- package/seeded-connectors/pi/dist/index.js +295 -185
- package/seeded-connectors/pi/dist/standalone.js +318 -188
- package/seeded-connectors/pi/package.json +1 -1
- 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.
|
|
3
|
+
"version": "0.93.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.
|
|
37
|
-
"@cotal-ai/cli": "0.
|
|
38
|
-
"@cotal-ai/connector-core": "0.
|
|
39
|
-
"@cotal-ai/core": "0.
|
|
40
|
-
"@cotal-ai/delivery": "0.
|
|
41
|
-
"@cotal-ai/manager": "0.
|
|
42
|
-
"@cotal-ai/runtime": "0.
|
|
43
|
-
"@cotal-ai/workspace": "0.
|
|
36
|
+
"@cotal-ai/auth": "0.93.0",
|
|
37
|
+
"@cotal-ai/cli": "0.93.0",
|
|
38
|
+
"@cotal-ai/connector-core": "0.93.0",
|
|
39
|
+
"@cotal-ai/core": "0.93.0",
|
|
40
|
+
"@cotal-ai/delivery": "0.93.0",
|
|
41
|
+
"@cotal-ai/manager": "0.93.0",
|
|
42
|
+
"@cotal-ai/runtime": "0.93.0",
|
|
43
|
+
"@cotal-ai/workspace": "0.93.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@cotal-ai/connector-claude-code": "0.
|
|
47
|
-
"@cotal-ai/connector-codex": "0.
|
|
48
|
-
"@cotal-ai/connector-hermes": "0.
|
|
49
|
-
"@cotal-ai/connector-jcode": "0.
|
|
50
|
-
"@cotal-ai/connector-opencode": "0.
|
|
51
|
-
"@cotal-ai/linear": "0.
|
|
52
|
-
"@cotal-ai/pi": "0.
|
|
46
|
+
"@cotal-ai/connector-claude-code": "0.93.0",
|
|
47
|
+
"@cotal-ai/connector-codex": "0.93.0",
|
|
48
|
+
"@cotal-ai/connector-hermes": "0.93.0",
|
|
49
|
+
"@cotal-ai/connector-jcode": "0.93.0",
|
|
50
|
+
"@cotal-ai/connector-opencode": "0.93.0",
|
|
51
|
+
"@cotal-ai/linear": "0.93.0",
|
|
52
|
+
"@cotal-ai/pi": "0.93.0",
|
|
53
53
|
"@cotal-ai/smoke-kit": "0.0.0",
|
|
54
|
-
"@cotal-ai/web": "0.
|
|
54
|
+
"@cotal-ai/web": "0.93.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
|
|
22
|
-
|
|
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":"
|
|
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,CA+IxD"}
|
|
@@ -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
|
|
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,
|
|
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,
|
|
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
|
|
313
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
347
|
-
(0,
|
|
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,
|
|
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(
|
|
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;
|
|
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
|
|
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
|
|