@cotal-ai/connector-claude-code 0.115.4 → 0.116.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.d.cts CHANGED
@@ -31,4 +31,14 @@ export interface ClaudeSession {
31
31
  * the call, so one process can serve several sessions. A startup failure closes what was started
32
32
  * before the call rejects. */
33
33
  export declare function serveClaudeSession(opts: ClaudeSessionOptions): Promise<ClaudeSession>;
34
+ /**
35
+ * Serve this process as a seat: {@link main} plus the one fatal handler a process entry owes its
36
+ * operator.
37
+ *
38
+ * Exported because `dist/seat.cjs` is the entry Claude Code launches, and it loads this bundle to
39
+ * get the compile cache in front of the compile (see `@cotal-ai/connector-core/seat-cache`). Loaded
40
+ * that way the bundle is not the main module, so the guard below cannot start it and the entry calls
41
+ * this instead. Both paths run the same body.
42
+ */
43
+ export declare function runClaudeSeat(): Promise<void>;
34
44
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts CHANGED
@@ -31,4 +31,14 @@ export interface ClaudeSession {
31
31
  * the call, so one process can serve several sessions. A startup failure closes what was started
32
32
  * before the call rejects. */
33
33
  export declare function serveClaudeSession(opts: ClaudeSessionOptions): Promise<ClaudeSession>;
34
+ /**
35
+ * Serve this process as a seat: {@link main} plus the one fatal handler a process entry owes its
36
+ * operator.
37
+ *
38
+ * Exported because `dist/seat.cjs` is the entry Claude Code launches, and it loads this bundle to
39
+ * get the compile cache in front of the compile (see `@cotal-ai/connector-core/seat-cache`). Loaded
40
+ * that way the bundle is not the main module, so the guard below cannot start it and the entry calls
41
+ * this instead. Both paths run the same body.
42
+ */
43
+ export declare function runClaudeSeat(): Promise<void>;
34
44
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAuKtD,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB;uDACmD;IACnD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;gFAC4E;IAC5E,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;uDACmD;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;+BAE+B;AAC/B,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQ3F"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAuKtD,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB;uDACmD;IACnD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;gFAC4E;IAC5E,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;uDACmD;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;+BAE+B;AAC/B,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQ3F;AA6KD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAOnD"}
package/dist/monitor.cjs CHANGED
@@ -1,415 +1,12 @@
1
- "use strict";
2
-
3
- // src/monitor.ts
4
- var import_node_net2 = require("node:net");
5
-
6
- // ../../packages/core/dist/secret-fs.js
7
- var import_node_fs = require("node:fs");
8
- var import_node_child_process = require("node:child_process");
9
- var import_node_path = require("node:path");
10
- var isWin = process.platform === "win32";
11
- function sys32(exe) {
12
- const root = process.env.SystemRoot || process.env.windir || "C:\\Windows";
13
- return (0, import_node_path.join)(root, "System32", exe);
14
- }
15
- var cachedSid;
16
- function ownerSid() {
17
- if (cachedSid)
18
- return cachedSid;
19
- const out = (0, import_node_child_process.execFileSync)(sys32("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
20
- const sid = out.trim().split(",").pop()?.replace(/^"|"$/g, "").trim();
21
- if (!sid || !/^S-1-/.test(sid))
22
- throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);
23
- cachedSid = sid;
24
- return sid;
25
- }
26
- var BROAD_SIDS = ["S-1-1-0", "S-1-5-11", "S-1-5-32-545"];
27
- function hardenPrivate(path, kind) {
28
- if (!isWin) {
29
- if (kind === "file")
30
- (0, import_node_fs.closeSync)(openPrivate(path, import_node_fs.constants.O_RDONLY));
31
- else
32
- (0, import_node_fs.chmodSync)(path, 448);
33
- return;
34
- }
35
- const perm = kind === "dir" ? "(OI)(CI)(F)" : "(F)";
36
- const removeBroad = BROAD_SIDS.flatMap((sid) => ["/remove:g", `*${sid}`]);
37
- const grant = (sid) => ["/grant:r", `*${sid}:${perm}`];
38
- try {
39
- (0, import_node_child_process.execFileSync)(sys32("icacls.exe"), [
40
- path,
41
- "/inheritance:r",
42
- ...removeBroad,
43
- ...grant(ownerSid()),
44
- ...grant("S-1-5-18"),
45
- ...grant("S-1-5-32-544")
46
- ], { stdio: ["ignore", "ignore", "pipe"] });
47
- } catch (e) {
48
- 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.`);
49
- }
50
- }
51
- function notPrivate(path, why) {
52
- const refused = new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);
53
- refused.code = "EPERM";
54
- return refused;
55
- }
56
- function openPrivate(path, flags) {
57
- if (isWin) {
58
- const fd2 = (0, import_node_fs.openSync)(path, flags, 384);
59
- try {
60
- (0, import_node_fs.fchmodSync)(fd2, 384);
61
- } catch (e) {
62
- (0, import_node_fs.closeSync)(fd2);
63
- throw e;
64
- }
65
- return fd2;
66
- }
67
- let fd;
68
- try {
69
- fd = (0, import_node_fs.openSync)(path, flags | import_node_fs.constants.O_NOFOLLOW | import_node_fs.constants.O_NONBLOCK, 384);
70
- } catch (e) {
71
- if (e.code === "ELOOP")
72
- throw notPrivate(path, "is a symlink");
73
- throw e;
74
- }
75
- try {
76
- const info = (0, import_node_fs.fstatSync)(fd);
77
- if (!info.isFile())
78
- throw notPrivate(path, "is not a regular file");
79
- if (info.uid !== process.getuid())
80
- throw notPrivate(path, `is owned by uid ${info.uid}, not this user`);
81
- (0, import_node_fs.fchmodSync)(fd, 384);
82
- return fd;
83
- } catch (e) {
84
- (0, import_node_fs.closeSync)(fd);
85
- throw e;
86
- }
87
- }
88
- function writeSecretFile(path, data) {
89
- writePrivate(path, data, false);
90
- }
91
- function writeBytes(path, data, exclusive) {
92
- const fd = openPrivate(path, import_node_fs.constants.O_WRONLY | import_node_fs.constants.O_CREAT | (exclusive ? import_node_fs.constants.O_EXCL : 0));
93
- try {
94
- (0, import_node_fs.ftruncateSync)(fd);
95
- (0, import_node_fs.writeFileSync)(fd, data);
96
- } finally {
97
- (0, import_node_fs.closeSync)(fd);
98
- }
99
- }
100
- function writePrivate(path, data, exclusive) {
101
- writeBytes(path, data, exclusive);
102
- if (!isWin)
103
- return;
104
- try {
105
- hardenPrivate(path, "file");
106
- } catch (e) {
107
- try {
108
- (0, import_node_fs.unlinkSync)(path);
109
- } catch {
110
- }
111
- throw e;
112
- }
113
- }
114
- function writeSecretFileAtomic(path, data) {
115
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
116
- try {
117
- writeSecretFile(tmp, data);
118
- (0, import_node_fs.renameSync)(tmp, path);
119
- } catch (e) {
120
- try {
121
- (0, import_node_fs.unlinkSync)(tmp);
122
- } catch {
123
- }
124
- throw e;
125
- }
126
- }
127
- function mkSecretDir(path) {
128
- (0, import_node_fs.mkdirSync)(path, { recursive: true, mode: 448 });
129
- hardenPrivate(path, "dir");
130
- }
131
-
132
- // ../connector-core/dist/session-env.js
133
- function hasIdentity(env = process.env) {
134
- return Boolean(env.COTAL_NAME?.trim() || env.COTAL_LINK?.trim() || env.COTAL_AGENT_FILE?.trim());
135
- }
136
-
137
- // ../connector-core/dist/control.js
138
- var MAX_FRAME_BYTES = 1 << 20;
139
-
140
- // src/approval-notice.ts
141
- var APPROVAL_ID = /^[a-z2-7]{26}$/;
142
- var STATUSES = /* @__PURE__ */ new Set(["approved", "denied", "expired"]);
143
- function approvalFrame(frame) {
144
- const approval = frame?.approval;
145
- if (!approval || typeof approval !== "object") return void 0;
146
- const { name, status, id } = approval;
147
- if (typeof name !== "string" || !name || name.length > 128 || /[\r\n]/.test(name) || name !== name.trim()) return void 0;
148
- if (typeof status !== "string" || !STATUSES.has(status)) return void 0;
149
- if (typeof id !== "string" || !APPROVAL_ID.test(id)) return void 0;
150
- return { name, status, id };
151
- }
152
- function approvalEvent(n) {
153
- const name = JSON.stringify(n.name);
154
- switch (n.status) {
155
- case "approved":
156
- return `Cotal: the person approved connection ${name}. Finish it: call cotal_connect with name ${name} and approval_id ${JSON.stringify(n.id)}.`;
157
- case "denied":
158
- return `Cotal: the person denied connection ${name}. Nothing was connected. Do not ask again unless the person asks you to.`;
159
- case "expired":
160
- return `Cotal: the approval for connection ${name} expired before the person decided. Nothing was connected. Call cotal_connect with name ${name} again to ask again.`;
161
- }
162
- }
163
-
164
- // src/wake-feed.ts
165
- var import_node_fs2 = require("node:fs");
166
- var import_node_os = require("node:os");
167
- var import_node_path2 = require("node:path");
168
-
169
- // src/native-deliveries.ts
170
- var ID = /^[A-Za-z0-9_-]{1,128}$/;
171
- var INLINE_FRAME_LIMIT = 4096;
172
- function isNativeInlineFrame(value) {
173
- if (!value || typeof value !== "object") return false;
174
- const frame = value;
175
- const item = frame.inline;
176
- return typeof frame.id === "string" && ID.test(frame.id) && typeof frame.route === "string" && !!item && item.v === 1 && typeof item.receipt === "string" && ID.test(item.receipt) && typeof item.session === "string" && ID.test(item.session) && typeof item.text === "string" && item.text.startsWith(`Cotal: inline messages for connection_id ${frame.id}.
177
- `) && Number.isInteger(item.count) && item.count > 0 && item.count <= 8 && Number.isInteger(item.remaining) && item.remaining >= 0 && JSON.stringify(frame).length <= INLINE_FRAME_LIMIT;
178
- }
179
-
180
- // src/wake-feed.ts
181
- var WAKE_ID = /^[A-Za-z0-9_-]{1,128}$/;
182
- var WIN_PIPES = "\\\\.\\pipe\\";
183
- var WIN_PREFIX = "cotal-claude-wake-";
184
- function wakeDir() {
185
- if (process.platform === "win32") return WIN_PIPES;
186
- const uid = process.getuid();
187
- const dir = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `cotal-claude-wake-${uid}`);
188
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true, mode: 448 });
189
- const st = (0, import_node_fs2.lstatSync)(dir);
190
- if (!st.isDirectory() || st.uid !== uid || (st.mode & 63) !== 0) throw new Error(`${dir} is not a private directory of this user's`);
191
- return dir;
192
- }
193
- var entryName = (claudePid, serverPid) => process.platform === "win32" ? `${WIN_PREFIX}${claudePid}-${serverPid}` : `${claudePid}-${serverPid}.sock`;
194
- var ENTRY = process.platform === "win32" ? new RegExp(`^${WIN_PREFIX}(\\d+)-(\\d+)$`) : /^(\d+)-(\d+)\.sock$/;
195
- function alive(pid) {
196
- try {
197
- process.kill(pid, 0);
198
- return true;
199
- } catch (e) {
200
- return e.code === "EPERM";
201
- }
202
- }
203
- function wakeSocketPath(claudePid, serverPid = process.pid) {
204
- const dir = wakeDir();
205
- return process.platform === "win32" ? `${dir}${entryName(claudePid, serverPid)}` : (0, import_node_path2.join)(dir, entryName(claudePid, serverPid));
206
- }
207
- function wakeSockets(claudePid) {
208
- const dir = wakeDir();
209
- return (0, import_node_fs2.readdirSync)(dir).flatMap((name) => {
210
- const m = ENTRY.exec(name);
211
- return m?.[1] === String(claudePid) && alive(Number(m[2])) ? [wakeSocketPath(claudePid, Number(m[2]))] : [];
212
- });
213
- }
214
- function nativeSocketPath(feed) {
215
- return `${feed}.native`;
216
- }
217
-
218
- // src/native-binding.ts
219
- var import_node_fs3 = require("node:fs");
220
- var import_node_net = require("node:net");
221
- var import_node_os2 = require("node:os");
222
- var import_node_path3 = require("node:path");
223
- function stateFile(session) {
224
- if (!WAKE_ID.test(session)) throw new Error("invalid native session identity");
225
- const root = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `cotal-claude-native-${process.getuid?.() ?? "local"}`);
226
- const st = (0, import_node_fs3.lstatSync)(root, { throwIfNoEntry: false });
227
- if (st && (!st.isDirectory() || process.getuid && st.uid !== process.getuid()))
228
- throw new Error("native session state is not this user's private directory");
229
- mkSecretDir(root);
230
- return (0, import_node_path3.join)(root, `${session}.json`);
231
- }
232
- function readRecord(file) {
233
- const st = (0, import_node_fs3.lstatSync)(file, { throwIfNoEntry: false });
234
- if (!st) return void 0;
235
- if (!st.isFile() || st.size > 16384 || process.getuid && (st.uid !== process.getuid() || (st.mode & 63) !== 0))
236
- throw new Error("native session state is not an owner-private bounded regular file");
237
- const value = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
238
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid native session state");
239
- return value;
240
- }
241
- function transcriptMatches(session, path) {
242
- return typeof path === "string" && (0, import_node_path3.isAbsolute)(path) && (0, import_node_path3.basename)(path) === `${session}.jsonl`;
243
- }
244
- function readPersonalBinding(session) {
245
- const event = readRecord(stateFile(session));
246
- if (!event || event.session_id !== session || !transcriptMatches(session, event.transcript_path))
247
- throw new Error("no native command-hook transcript binding for this session");
248
- return event;
249
- }
250
- async function forward(event, hostPid) {
251
- const paths = wakeSockets(hostPid);
252
- if (!paths.length) throw new Error("no local native lifecycle server");
253
- await Promise.all(paths.map((path) => new Promise((resolve, reject) => {
254
- const socket = (0, import_node_net.createConnection)(nativeSocketPath(path));
255
- let reply = "";
256
- socket.setEncoding("utf8");
257
- socket.setTimeout(2e3, () => socket.destroy(new Error("native lifecycle relay timed out")));
258
- socket.on("connect", () => socket.write(`${JSON.stringify({ op: "native", event })}
259
- `));
260
- socket.on("data", (data) => {
261
- reply += data;
262
- if (reply.length > 128) socket.destroy();
263
- });
264
- socket.on("error", reject);
265
- socket.on("close", () => {
266
- try {
267
- if (JSON.parse(reply).ok === true) resolve();
268
- else reject(new Error("native lifecycle binding refused"));
269
- } catch {
270
- reject(new Error("native lifecycle binding was not confirmed"));
271
- }
272
- });
273
- })));
274
- }
275
- async function bindPersonalSession(event, hostPid) {
276
- const session = event.session_id;
277
- if (typeof session !== "string" || !Number.isInteger(hostPid) || hostPid <= 0)
278
- throw new Error("invalid native lifecycle binding");
279
- const previous = readPersonalBinding(session);
280
- if (previous.transcript_path !== event.transcript_path)
281
- throw new Error("native lifecycle binding does not match the command hook");
282
- writeSecretFileAtomic(`${stateFile(session)}.host`, JSON.stringify({ hostPid }));
283
- await forward(event, hostPid);
284
- }
285
-
286
- // src/monitor.ts
287
- var SCAN_MS = 1e3;
288
- var wakeEvent = (id) => `Cotal: a new message for connection_id ${id}. Read it with cotal_inbox.`;
289
- var previewEvent = (id, preview) => `Cotal preview for connection_id ${id}: ${JSON.stringify(preview)}`;
290
- async function relayNative(claude, raw) {
291
- const event = JSON.parse(raw);
292
- if (typeof event.session_id !== "string" || !WAKE_ID.test(event.session_id) || raw.length > 8192)
293
- throw new Error("invalid native lifecycle frame");
294
- await bindPersonalSession(event, claude);
295
- }
296
- async function relayInline(claude, session) {
297
- let input = "";
298
- for await (const piece of process.stdin) {
299
- input += piece;
300
- if (input.length > 1024) throw new Error("inline receipt is too large");
301
- }
302
- const frame = JSON.parse(input);
303
- if (!session || frame.session !== session || typeof frame.route !== "string" || !wakeSockets(claude).includes(frame.route) || !["claim", "commit", "defer", "reject", "retry"].includes(String(frame.action)) || typeof frame.receipt !== "string" || !WAKE_ID.test(frame.receipt) || typeof frame.id !== "string" || !WAKE_ID.test(frame.id)) throw new Error("invalid inline receipt");
304
- const reply = await new Promise((resolve, reject) => {
305
- const socket = (0, import_node_net2.createConnection)(nativeSocketPath(frame.route));
306
- let buffer = "";
307
- socket.setEncoding("utf8");
308
- socket.setTimeout(2e3, () => socket.destroy(new Error("inline receipt timed out")));
309
- socket.on("connect", () => socket.write(`${JSON.stringify({ ...frame, op: "inline" })}
310
- `));
311
- socket.on("data", (piece) => {
312
- buffer += piece;
313
- if (buffer.length > INLINE_FRAME_LIMIT + 64) socket.destroy(new Error("inline reply is too large"));
314
- });
315
- socket.on("error", reject);
316
- socket.on("close", () => resolve(buffer));
317
- });
318
- const result = JSON.parse(reply);
319
- if (result.ok !== true && result.ok !== false || result.frame !== void 0 && !isNativeInlineFrame(result.frame))
320
- throw new Error("invalid inline reply");
321
- process.stdout.write(`${JSON.stringify(result)}
322
- `);
323
- }
324
- function main() {
325
- if (hasIdentity()) return;
326
- const claude = process.argv.includes("--native") ? process.ppid : Number(process.env.CLAUDE_PID);
327
- if (!Number.isInteger(claude) || claude <= 0) {
328
- process.stderr.write("[cotal-monitor] no CLAUDE_PID, so this is not a Claude Code session\n");
329
- process.exitCode = 1;
330
- return;
331
- }
332
- const bindingAt = process.argv.indexOf("--native-binding");
333
- if (bindingAt >= 0) {
334
- try {
335
- process.stdout.write(`${JSON.stringify(readPersonalBinding(process.argv[bindingAt + 1] ?? ""))}
336
- `);
337
- } catch {
338
- process.exitCode = 1;
339
- }
340
- return;
341
- }
342
- const eventAt = process.argv.indexOf("--native-event");
343
- if (eventAt >= 0) {
344
- void relayNative(claude, process.argv[eventAt + 1] ?? "").catch(() => {
345
- process.stderr.write("[cotal-monitor] native lifecycle binding was not confirmed\n");
346
- process.exitCode = 1;
347
- });
348
- return;
349
- }
350
- const sessionAt = process.argv.indexOf("--session");
351
- const session = sessionAt >= 0 ? process.argv[sessionAt + 1] : void 0;
352
- if (session !== void 0 && !WAKE_ID.test(session)) throw new Error("invalid native session identity");
353
- if (process.argv.includes("--native-delivery")) {
354
- void relayInline(claude, session).catch(() => {
355
- process.exitCode = 1;
356
- });
357
- return;
358
- }
359
- const reading = /* @__PURE__ */ new Set();
360
- const read = (path) => {
361
- reading.add(path);
362
- let buf = "";
363
- const s = (0, import_node_net2.createConnection)(path);
364
- s.setEncoding("utf8");
365
- if (session) s.on("connect", () => s.write(`${JSON.stringify({ op: "watch", session, inline: 1 })}
366
- `));
367
- s.on("data", (d) => {
368
- buf += d;
369
- for (let i; (i = buf.indexOf("\n")) >= 0; ) {
370
- const line = buf.slice(0, i);
371
- buf = buf.slice(i + 1);
372
- try {
373
- if (line.length > INLINE_FRAME_LIMIT) continue;
374
- const frame = JSON.parse(line);
375
- const decided = approvalFrame(frame);
376
- if (decided) {
377
- process.stdout.write(`${approvalEvent(decided)}
378
- `);
379
- continue;
380
- }
381
- if (isNativeInlineFrame(frame)) {
382
- if (frame.inline.session === session && frame.route === path)
383
- process.stdout.write(`Cotal inline delivery: ${JSON.stringify(frame)}
384
- `);
385
- continue;
386
- }
387
- if (typeof frame.id !== "string" || !WAKE_ID.test(frame.id)) continue;
388
- const preview = frame.preview;
389
- if (!preview || !Number.isInteger(preview.unread) || !Array.isArray(preview.rows) || preview.rows.length > 8 || !preview.rows.every((row) => typeof row === "string" && Array.from(row).length <= 240)) continue;
390
- process.stdout.write(`${previewEvent(frame.id, preview)}
1
+ "use strict";var import_node_net2=require("node:net");var import_node_fs=require("node:fs");var import_node_child_process=require("node:child_process");var import_node_path=require("node:path");var isWin=process.platform==="win32";function sys32(exe){const root=process.env.SystemRoot||process.env.windir||"C:\\Windows";return(0,import_node_path.join)(root,"System32",exe)}var cachedSid;function ownerSid(){if(cachedSid)return cachedSid;const out=(0,import_node_child_process.execFileSync)(sys32("whoami.exe"),["/user","/fo","csv","/nh"],{encoding:"utf8"});const sid=out.trim().split(",").pop()?.replace(/^"|"$/g,"").trim();if(!sid||!/^S-1-/.test(sid))throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);cachedSid=sid;return sid}var BROAD_SIDS=["S-1-1-0","S-1-5-11","S-1-5-32-545"];function hardenPrivate(path,kind){if(!isWin){if(kind==="file")(0,import_node_fs.closeSync)(openPrivate(path,import_node_fs.constants.O_RDONLY));else(0,import_node_fs.chmodSync)(path,448);return}const perm=kind==="dir"?"(OI)(CI)(F)":"(F)";const removeBroad=BROAD_SIDS.flatMap(sid=>["/remove:g",`*${sid}`]);const grant=sid=>["/grant:r",`*${sid}:${perm}`];try{(0,import_node_child_process.execFileSync)(sys32("icacls.exe"),[path,"/inheritance:r",...removeBroad,...grant(ownerSid()),...grant("S-1-5-18"),...grant("S-1-5-32-544")],{stdio:["ignore","ignore","pipe"]})}catch(e){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.`)}}function notPrivate(path,why){const refused=new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);refused.code="EPERM";return refused}function openPrivate(path,flags){if(isWin){const fd2=(0,import_node_fs.openSync)(path,flags,384);try{(0,import_node_fs.fchmodSync)(fd2,384)}catch(e){(0,import_node_fs.closeSync)(fd2);throw e}return fd2}let fd;try{fd=(0,import_node_fs.openSync)(path,flags|import_node_fs.constants.O_NOFOLLOW|import_node_fs.constants.O_NONBLOCK,384)}catch(e){if(e.code==="ELOOP")throw notPrivate(path,"is a symlink");throw e}try{const info=(0,import_node_fs.fstatSync)(fd);if(!info.isFile())throw notPrivate(path,"is not a regular file");if(info.uid!==process.getuid())throw notPrivate(path,`is owned by uid ${info.uid}, not this user`);(0,import_node_fs.fchmodSync)(fd,384);return fd}catch(e){(0,import_node_fs.closeSync)(fd);throw e}}function writeSecretFile(path,data){writePrivate(path,data,false)}function writeBytes(path,data,exclusive){const fd=openPrivate(path,import_node_fs.constants.O_WRONLY|import_node_fs.constants.O_CREAT|(exclusive?import_node_fs.constants.O_EXCL:0));try{(0,import_node_fs.ftruncateSync)(fd);(0,import_node_fs.writeFileSync)(fd,data)}finally{(0,import_node_fs.closeSync)(fd)}}function writePrivate(path,data,exclusive){writeBytes(path,data,exclusive);if(!isWin)return;try{hardenPrivate(path,"file")}catch(e){try{(0,import_node_fs.unlinkSync)(path)}catch{}throw e}}function writeSecretFileAtomic(path,data){const tmp=`${path}.${process.pid}.${Date.now()}.tmp`;try{writeSecretFile(tmp,data);(0,import_node_fs.renameSync)(tmp,path)}catch(e){try{(0,import_node_fs.unlinkSync)(tmp)}catch{}throw e}}function mkSecretDir(path){(0,import_node_fs.mkdirSync)(path,{recursive:true,mode:448});hardenPrivate(path,"dir")}function hasIdentity(env=process.env){return Boolean(env.COTAL_NAME?.trim()||env.COTAL_LINK?.trim()||env.COTAL_AGENT_FILE?.trim())}var MAX_FRAME_BYTES=1<<20;var APPROVAL_ID=/^[a-z2-7]{26}$/;var STATUSES=new Set(["approved","denied","expired"]);function approvalFrame(frame){const approval=frame?.approval;if(!approval||typeof approval!=="object")return void 0;const{name,status,id}=approval;if(typeof name!=="string"||!name||name.length>128||/[\r\n]/.test(name)||name!==name.trim())return void 0;if(typeof status!=="string"||!STATUSES.has(status))return void 0;if(typeof id!=="string"||!APPROVAL_ID.test(id))return void 0;return{name,status,id}}function approvalEvent(n){const name=JSON.stringify(n.name);switch(n.status){case"approved":return`Cotal: the person approved connection ${name}. Finish it: call cotal_connect with name ${name} and approval_id ${JSON.stringify(n.id)}.`;case"denied":return`Cotal: the person denied connection ${name}. Nothing was connected. Do not ask again unless the person asks you to.`;case"expired":return`Cotal: the approval for connection ${name} expired before the person decided. Nothing was connected. Call cotal_connect with name ${name} again to ask again.`}}var import_node_fs2=require("node:fs");var import_node_os=require("node:os");var import_node_path2=require("node:path");var ID=/^[A-Za-z0-9_-]{1,128}$/;var INLINE_FRAME_LIMIT=4096;function isNativeInlineFrame(value){if(!value||typeof value!=="object")return false;const frame=value;const item=frame.inline;return typeof frame.id==="string"&&ID.test(frame.id)&&typeof frame.route==="string"&&!!item&&item.v===1&&typeof item.receipt==="string"&&ID.test(item.receipt)&&typeof item.session==="string"&&ID.test(item.session)&&typeof item.text==="string"&&item.text.startsWith(`Cotal: inline messages for connection_id ${frame.id}.
2
+ `)&&Number.isInteger(item.count)&&item.count>0&&item.count<=8&&Number.isInteger(item.remaining)&&item.remaining>=0&&JSON.stringify(frame).length<=INLINE_FRAME_LIMIT}var WAKE_ID=/^[A-Za-z0-9_-]{1,128}$/;var WIN_PIPES="\\\\.\\pipe\\";var WIN_PREFIX="cotal-claude-wake-";function wakeDir(){if(process.platform==="win32")return WIN_PIPES;const uid=process.getuid();const dir=(0,import_node_path2.join)((0,import_node_os.tmpdir)(),`cotal-claude-wake-${uid}`);(0,import_node_fs2.mkdirSync)(dir,{recursive:true,mode:448});const st=(0,import_node_fs2.lstatSync)(dir);if(!st.isDirectory()||st.uid!==uid||(st.mode&63)!==0)throw new Error(`${dir} is not a private directory of this user's`);return dir}var entryName=(claudePid,serverPid)=>process.platform==="win32"?`${WIN_PREFIX}${claudePid}-${serverPid}`:`${claudePid}-${serverPid}.sock`;var ENTRY=process.platform==="win32"?new RegExp(`^${WIN_PREFIX}(\\d+)-(\\d+)$`):/^(\d+)-(\d+)\.sock$/;function alive(pid){try{process.kill(pid,0);return true}catch(e){return e.code==="EPERM"}}function wakeSocketPath(claudePid,serverPid=process.pid){const dir=wakeDir();return process.platform==="win32"?`${dir}${entryName(claudePid,serverPid)}`:(0,import_node_path2.join)(dir,entryName(claudePid,serverPid))}function wakeSockets(claudePid){const dir=wakeDir();return(0,import_node_fs2.readdirSync)(dir).flatMap(name=>{const m=ENTRY.exec(name);return m?.[1]===String(claudePid)&&alive(Number(m[2]))?[wakeSocketPath(claudePid,Number(m[2]))]:[]})}function nativeSocketPath(feed){return`${feed}.native`}var import_node_fs3=require("node:fs");var import_node_net=require("node:net");var import_node_os2=require("node:os");var import_node_path3=require("node:path");function stateFile(session){if(!WAKE_ID.test(session))throw new Error("invalid native session identity");const root=(0,import_node_path3.join)((0,import_node_os2.tmpdir)(),`cotal-claude-native-${process.getuid?.()??"local"}`);const st=(0,import_node_fs3.lstatSync)(root,{throwIfNoEntry:false});if(st&&(!st.isDirectory()||process.getuid&&st.uid!==process.getuid()))throw new Error("native session state is not this user's private directory");mkSecretDir(root);return(0,import_node_path3.join)(root,`${session}.json`)}function readRecord(file){const st=(0,import_node_fs3.lstatSync)(file,{throwIfNoEntry:false});if(!st)return void 0;if(!st.isFile()||st.size>16384||process.getuid&&(st.uid!==process.getuid()||(st.mode&63)!==0))throw new Error("native session state is not an owner-private bounded regular file");const value=JSON.parse((0,import_node_fs3.readFileSync)(file,"utf8"));if(!value||typeof value!=="object"||Array.isArray(value))throw new Error("invalid native session state");return value}function transcriptMatches(session,path){return typeof path==="string"&&(0,import_node_path3.isAbsolute)(path)&&(0,import_node_path3.basename)(path)===`${session}.jsonl`}function readPersonalBinding(session){const event=readRecord(stateFile(session));if(!event||event.session_id!==session||!transcriptMatches(session,event.transcript_path))throw new Error("no native command-hook transcript binding for this session");return event}async function forward(event,hostPid){const paths=wakeSockets(hostPid);if(!paths.length)throw new Error("no local native lifecycle server");await Promise.all(paths.map(path=>new Promise((resolve,reject)=>{const socket=(0,import_node_net.createConnection)(nativeSocketPath(path));let reply="";socket.setEncoding("utf8");socket.setTimeout(2e3,()=>socket.destroy(new Error("native lifecycle relay timed out")));socket.on("connect",()=>socket.write(`${JSON.stringify({op:"native",event})}
3
+ `));socket.on("data",data=>{reply+=data;if(reply.length>128)socket.destroy()});socket.on("error",reject);socket.on("close",()=>{try{if(JSON.parse(reply).ok===true)resolve();else reject(new Error("native lifecycle binding refused"))}catch{reject(new Error("native lifecycle binding was not confirmed"))}})})))}async function bindPersonalSession(event,hostPid){const session=event.session_id;if(typeof session!=="string"||!Number.isInteger(hostPid)||hostPid<=0)throw new Error("invalid native lifecycle binding");const previous=readPersonalBinding(session);if(previous.transcript_path!==event.transcript_path)throw new Error("native lifecycle binding does not match the command hook");writeSecretFileAtomic(`${stateFile(session)}.host`,JSON.stringify({hostPid}));await forward(event,hostPid)}var SCAN_MS=1e3;var wakeEvent=id=>`Cotal: a new message for connection_id ${id}. Read it with cotal_inbox.`;var previewEvent=(id,preview)=>`Cotal preview for connection_id ${id}: ${JSON.stringify(preview)}`;async function relayNative(claude,raw){const event=JSON.parse(raw);if(typeof event.session_id!=="string"||!WAKE_ID.test(event.session_id)||raw.length>8192)throw new Error("invalid native lifecycle frame");await bindPersonalSession(event,claude)}async function relayInline(claude,session){let input="";for await(const piece of process.stdin){input+=piece;if(input.length>1024)throw new Error("inline receipt is too large")}const frame=JSON.parse(input);if(!session||frame.session!==session||typeof frame.route!=="string"||!wakeSockets(claude).includes(frame.route)||!["claim","commit","defer","reject","retry"].includes(String(frame.action))||typeof frame.receipt!=="string"||!WAKE_ID.test(frame.receipt)||typeof frame.id!=="string"||!WAKE_ID.test(frame.id))throw new Error("invalid inline receipt");const reply=await new Promise((resolve,reject)=>{const socket=(0,import_node_net2.createConnection)(nativeSocketPath(frame.route));let buffer="";socket.setEncoding("utf8");socket.setTimeout(2e3,()=>socket.destroy(new Error("inline receipt timed out")));socket.on("connect",()=>socket.write(`${JSON.stringify({...frame,op:"inline"})}
4
+ `));socket.on("data",piece=>{buffer+=piece;if(buffer.length>INLINE_FRAME_LIMIT+64)socket.destroy(new Error("inline reply is too large"))});socket.on("error",reject);socket.on("close",()=>resolve(buffer))});const result=JSON.parse(reply);if(result.ok!==true&&result.ok!==false||result.frame!==void 0&&!isNativeInlineFrame(result.frame))throw new Error("invalid inline reply");process.stdout.write(`${JSON.stringify(result)}
5
+ `)}function main(){if(hasIdentity())return;const claude=process.argv.includes("--native")?process.ppid:Number(process.env.CLAUDE_PID);if(!Number.isInteger(claude)||claude<=0){process.stderr.write("[cotal-monitor] no CLAUDE_PID, so this is not a Claude Code session\n");process.exitCode=1;return}const bindingAt=process.argv.indexOf("--native-binding");if(bindingAt>=0){try{process.stdout.write(`${JSON.stringify(readPersonalBinding(process.argv[bindingAt+1]??""))}
6
+ `)}catch{process.exitCode=1}return}const eventAt=process.argv.indexOf("--native-event");if(eventAt>=0){void relayNative(claude,process.argv[eventAt+1]??"").catch(()=>{process.stderr.write("[cotal-monitor] native lifecycle binding was not confirmed\n");process.exitCode=1});return}const sessionAt=process.argv.indexOf("--session");const session=sessionAt>=0?process.argv[sessionAt+1]:void 0;if(session!==void 0&&!WAKE_ID.test(session))throw new Error("invalid native session identity");if(process.argv.includes("--native-delivery")){void relayInline(claude,session).catch(()=>{process.exitCode=1});return}const reading=new Set;const read=path=>{reading.add(path);let buf="";const s=(0,import_node_net2.createConnection)(path);s.setEncoding("utf8");if(session)s.on("connect",()=>s.write(`${JSON.stringify({op:"watch",session,inline:1})}
7
+ `));s.on("data",d=>{buf+=d;for(let i;(i=buf.indexOf("\n"))>=0;){const line=buf.slice(0,i);buf=buf.slice(i+1);try{if(line.length>INLINE_FRAME_LIMIT)continue;const frame=JSON.parse(line);const decided=approvalFrame(frame);if(decided){process.stdout.write(`${approvalEvent(decided)}
8
+ `);continue}if(isNativeInlineFrame(frame)){if(frame.inline.session===session&&frame.route===path)process.stdout.write(`Cotal inline delivery: ${JSON.stringify(frame)}
9
+ `);continue}if(typeof frame.id!=="string"||!WAKE_ID.test(frame.id))continue;const preview=frame.preview;if(!preview||!Number.isInteger(preview.unread)||!Array.isArray(preview.rows)||preview.rows.length>8||!preview.rows.every(row=>typeof row==="string"&&Array.from(row).length<=240))continue;process.stdout.write(`${previewEvent(frame.id,preview)}
391
10
  ${wakeEvent(frame.id)}
392
- `);
393
- } catch {
394
- if (WAKE_ID.test(line)) process.stdout.write(`${wakeEvent(line)}
395
- `);
396
- }
397
- }
398
- if (buf.length > 4096) s.destroy();
399
- });
400
- s.on("error", () => {
401
- });
402
- s.on("close", () => reading.delete(path));
403
- };
404
- const scan = () => {
405
- try {
406
- process.kill(claude, 0);
407
- } catch {
408
- process.exit(0);
409
- }
410
- for (const path of wakeSockets(claude)) if (!reading.has(path)) read(path);
411
- };
412
- scan();
413
- setInterval(scan, SCAN_MS);
414
- }
415
- main();
11
+ `)}catch{if(WAKE_ID.test(line))process.stdout.write(`${wakeEvent(line)}
12
+ `)}}if(buf.length>4096)s.destroy()});s.on("error",()=>{});s.on("close",()=>reading.delete(path))};const scan=()=>{try{process.kill(claude,0)}catch{process.exit(0)}for(const path of wakeSockets(claude))if(!reading.has(path))read(path)};scan();setInterval(scan,SCAN_MS)}main();
package/dist/seat.cjs ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";var import_node_module2=require("node:module");var import_node_module=require("node:module");var import_node_os=require("node:os");var import_node_path2=require("node:path");var import_node_fs=require("node:fs");var import_node_child_process=require("node:child_process");var import_node_path=require("node:path");var isWin=process.platform==="win32";function sys32(exe){const root=process.env.SystemRoot||process.env.windir||"C:\\Windows";return(0,import_node_path.join)(root,"System32",exe)}var cachedSid;function ownerSid(){if(cachedSid)return cachedSid;const out=(0,import_node_child_process.execFileSync)(sys32("whoami.exe"),["/user","/fo","csv","/nh"],{encoding:"utf8"});const sid=out.trim().split(",").pop()?.replace(/^"|"$/g,"").trim();if(!sid||!/^S-1-/.test(sid))throw new Error(`could not determine the current user's SID from \`whoami\` (got: ${out.trim()})`);cachedSid=sid;return sid}var BROAD_SIDS=["S-1-1-0","S-1-5-11","S-1-5-32-545"];function hardenPrivate(path,kind){if(!isWin){if(kind==="file")(0,import_node_fs.closeSync)(openPrivate(path,import_node_fs.constants.O_RDONLY));else(0,import_node_fs.chmodSync)(path,448);return}const perm=kind==="dir"?"(OI)(CI)(F)":"(F)";const removeBroad=BROAD_SIDS.flatMap(sid=>["/remove:g",`*${sid}`]);const grant=sid=>["/grant:r",`*${sid}:${perm}`];try{(0,import_node_child_process.execFileSync)(sys32("icacls.exe"),[path,"/inheritance:r",...removeBroad,...grant(ownerSid()),...grant("S-1-5-18"),...grant("S-1-5-32-544")],{stdio:["ignore","ignore","pipe"]})}catch(e){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.`)}}function notPrivate(path,why){const refused=new Error(`refusing to write "${path}": it ${why}; it was left unchanged`);refused.code="EPERM";return refused}function openPrivate(path,flags){if(isWin){const fd2=(0,import_node_fs.openSync)(path,flags,384);try{(0,import_node_fs.fchmodSync)(fd2,384)}catch(e){(0,import_node_fs.closeSync)(fd2);throw e}return fd2}let fd;try{fd=(0,import_node_fs.openSync)(path,flags|import_node_fs.constants.O_NOFOLLOW|import_node_fs.constants.O_NONBLOCK,384)}catch(e){if(e.code==="ELOOP")throw notPrivate(path,"is a symlink");throw e}try{const info=(0,import_node_fs.fstatSync)(fd);if(!info.isFile())throw notPrivate(path,"is not a regular file");if(info.uid!==process.getuid())throw notPrivate(path,`is owned by uid ${info.uid}, not this user`);(0,import_node_fs.fchmodSync)(fd,384);return fd}catch(e){(0,import_node_fs.closeSync)(fd);throw e}}function mkSecretDir(path){(0,import_node_fs.mkdirSync)(path,{recursive:true,mode:448});hardenPrivate(path,"dir")}var SEAT_NODE_FLOOR="22.10.0";function seatCompileCacheDir(){const user=process.platform==="win32"&&process.env.LOCALAPPDATA?(0,import_node_path2.join)(process.env.LOCALAPPDATA,"Cotal"):(0,import_node_path2.join)((0,import_node_os.homedir)(),".cotal");return(0,import_node_path2.join)(user,"compile-cache")}function assertSeatRuntime(label){const reason=unsupportedRuntime();if(!reason)return;process.stderr.write(`[${label}] fatal: unsupported_node: a Cotal seat needs Node >= ${SEAT_NODE_FLOOR}, ${reason}. Upgrade Node, or run the server bundle dist/mcp.cjs, which needs no compile cache.
2
+ `);process.exit(1)}function unsupportedRuntime(){const[floorMajor,floorMinor]=SEAT_NODE_FLOOR.split(".").map(Number);const[major,minor]=process.versions.node.split(".").map(Number);if(major<floorMajor||major===floorMajor&&minor<floorMinor)return`this is v${process.versions.node}`;if(typeof import_node_module.enableCompileCache!=="function"||typeof import_node_module.flushCompileCache!=="function")return`v${process.versions.node} reports the version but has no module compile cache`;return void 0}function enableSeatCompileCache(label){if(process.env.NODE_COMPILE_CACHE||process.env.NODE_DISABLE_COMPILE_CACHE)return;const dir=seatCompileCacheDir();try{mkSecretDir(dir)}catch(e){unavailable(label,e.message);return}const result=(0,import_node_module.enableCompileCache)(dir);if(result.status===import_node_module.constants.compileCacheStatus.FAILED)unavailable(label,result.message??"no reason reported")}function unavailable(label,reason){process.stderr.write(`[${label}] compile cache unavailable, continuing without it: ${reason}
3
+ `)}function flushSeatCompileCache(){(0,import_node_module.flushCompileCache)()}assertSeatRuntime("cotal-connector");enableSeatCompileCache("cotal-connector");var seat=(0,import_node_module2.createRequire)(__filename)("./mcp.cjs");flushSeatCompileCache();void seat.runClaudeSeat();
package/dist/seat.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=seat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seat.d.ts","sourceRoot":"","sources":["../src/seat.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/connector-claude-code",
3
3
  "description": "Cotal connector for Claude Code: an installed plugin that joins a session to the mesh.",
4
- "version": "0.115.4",
4
+ "version": "0.116.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,8 +25,8 @@
25
25
  "@cotal-ai/core": ">=0.1.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@cotal-ai/connector-core": "0.115.4",
29
- "@cotal-ai/core": "0.115.4",
28
+ "@cotal-ai/connector-core": "0.116.1",
29
+ "@cotal-ai/core": "0.116.1",
30
30
  "@cotal-ai/smoke-kit": "0.0.0",
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
32
  "@nats-io/transport-node": "^3.4.0",
@@ -50,6 +50,6 @@
50
50
  "test:upgrade": "tsx smoke/plugin-upgrade-live.smoke.ts",
51
51
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.smoke.json",
52
52
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json --emitDeclarationOnly && node -e \"require('node:fs').copyFileSync('dist/mcp.d.ts','dist/mcp.d.cts')\" && pnpm run bundle",
53
- "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:@cotal-ai/core && esbuild src/mcp.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/mcp.cjs --footer:js=\"module.exports = { serveClaudeSession: module.exports.serveClaudeSession };\" && esbuild src/hook.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/hook.cjs && esbuild src/monitor.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/monitor.cjs"
53
+ "bundle": "esbuild src/index.ts --bundle --minify-whitespace --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:@cotal-ai/core && esbuild src/mcp.ts --bundle --minify-whitespace --platform=node --format=cjs --target=node20 --outfile=dist/mcp.cjs --footer:js=\"module.exports = { serveClaudeSession: module.exports.serveClaudeSession, runClaudeSeat: module.exports.runClaudeSeat };\" && esbuild src/seat.ts --bundle --minify-whitespace --platform=node --format=cjs --target=node20 --outfile=dist/seat.cjs && esbuild src/hook.ts --bundle --minify-whitespace --platform=node --format=cjs --target=node20 --outfile=dist/hook.cjs && esbuild src/monitor.ts --bundle --minify-whitespace --platform=node --format=cjs --target=node20 --outfile=dist/monitor.cjs"
54
54
  }
55
55
  }