sessionpipe 0.1.1 → 0.2.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/dist/hook.js CHANGED
@@ -1,28 +1,71 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe 0.1.1 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/hook.ts
5
5
  import { spawn } from "node:child_process";
6
- import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
+ import { createHash } from "node:crypto";
7
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
+ import os2 from "node:os";
9
+ import path2 from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ // src/control/local.ts
13
+ import net from "node:net";
7
14
  import os from "node:os";
8
15
  import path from "node:path";
9
- import { fileURLToPath } from "node:url";
16
+ function socketPath(state2, env2 = process.env) {
17
+ if (env2.SESSIONPIPE_SOCKET) return env2.SESSIONPIPE_SOCKET;
18
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os.userInfo().username}`;
19
+ return path.join(state2, "control.sock");
20
+ }
21
+ function ask(sock, req, timeoutMs) {
22
+ return new Promise((resolve) => {
23
+ let buf = "";
24
+ let done = false;
25
+ const s = net.connect(sock);
26
+ const finish2 = (r) => {
27
+ if (done) return;
28
+ done = true;
29
+ clearTimeout(t);
30
+ s.destroy();
31
+ resolve(r);
32
+ };
33
+ const t = setTimeout(() => finish2(null), timeoutMs);
34
+ s.on("connect", () => s.write(`${JSON.stringify(req)}
35
+ `));
36
+ s.on("data", (d) => {
37
+ buf += d.toString("utf8");
38
+ const i = buf.indexOf("\n");
39
+ if (i < 0) return;
40
+ try {
41
+ finish2(JSON.parse(buf.slice(0, i)));
42
+ } catch {
43
+ finish2(null);
44
+ }
45
+ });
46
+ s.on("error", () => finish2(null));
47
+ s.on("close", () => finish2(null));
48
+ });
49
+ }
50
+
51
+ // src/hook.ts
10
52
  var t0 = process.hrtime.bigint();
53
+ var workMs = null;
11
54
  var [harness = "", event = "", ...rest] = process.argv.slice(2);
12
55
  var env = process.env;
13
56
  function stateDir() {
14
- if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);
15
- const home = os.homedir();
57
+ if (env.SESSIONPIPE_STATE) return path2.resolve(env.SESSIONPIPE_STATE);
58
+ const home = os2.homedir();
16
59
  if (process.platform === "win32")
17
- return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "sessionpipe");
18
- return path.join(env.XDG_STATE_HOME || path.join(home, ".local", "state"), "sessionpipe");
60
+ return path2.join(env.LOCALAPPDATA || path2.join(home, "AppData", "Local"), "sessionpipe");
61
+ return path2.join(env.XDG_STATE_HOME || path2.join(home, ".local", "state"), "sessionpipe");
19
62
  }
20
63
  function configFile() {
21
- if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);
22
- const home = os.homedir();
64
+ if (env.SESSIONPIPE_CONFIG) return path2.resolve(env.SESSIONPIPE_CONFIG);
65
+ const home = os2.homedir();
23
66
  if (process.platform === "win32")
24
- return path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "sessionpipe", "config.json");
25
- return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "sessionpipe", "config.json");
67
+ return path2.join(env.APPDATA || path2.join(home, "AppData", "Roaming"), "sessionpipe", "config.json");
68
+ return path2.join(env.XDG_CONFIG_HOME || path2.join(home, ".config"), "sessionpipe", "config.json");
26
69
  }
27
70
  if (harness === "antigravity") {
28
71
  try {
@@ -46,7 +89,8 @@ try {
46
89
  const id = sessionIdOf(stdin);
47
90
  if (!id || !beatDue(id)) finish(false);
48
91
  }
49
- mkdirSync(path.join(state, "jobs"), { recursive: true, mode: 448 });
92
+ const stopAnswer = event === "Stop" ? await (controlAsk(stdin) ?? Promise.resolve(null)) : null;
93
+ mkdirSync(path2.join(state, "jobs"), { recursive: true, mode: 448 });
50
94
  const job = {
51
95
  harness,
52
96
  event,
@@ -63,24 +107,75 @@ try {
63
107
  ppid: process.ppid,
64
108
  at: Date.now()
65
109
  };
66
- const jobFile = path.join(state, "jobs", `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
110
+ const jobFile = path2.join(state, "jobs", `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
67
111
  writeFileSync(jobFile, JSON.stringify(job), { mode: 384 });
68
112
  if (!env.SESSIONPIPE_NO_WORKER) {
69
- const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), "worker.js");
113
+ const worker = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "worker.js");
70
114
  const p = spawn(process.execPath, [worker, jobFile], { detached: true, stdio: "ignore", windowsHide: true, env });
71
115
  p.unref();
72
116
  }
117
+ if (stopAnswer) {
118
+ try {
119
+ writeFileSync(1, `${stopAnswer}
120
+ `);
121
+ } catch {
122
+ }
123
+ }
124
+ const ctl = event === "PermissionRequest" ? controlAsk(stdin) : null;
125
+ if (ctl) {
126
+ workMs = Number(process.hrtime.bigint() - t0) / 1e6;
127
+ const out = await ctl;
128
+ if (out) {
129
+ try {
130
+ writeFileSync(1, `${out}
131
+ `);
132
+ } catch {
133
+ }
134
+ }
135
+ }
73
136
  finish(true);
74
137
  } catch {
75
138
  finish(false);
76
139
  }
140
+ function controlAsk(stdin) {
141
+ if (harness !== "claude-code" || event !== "Stop" && event !== "PermissionRequest") return null;
142
+ if (env.SESSIONPIPE_CONTROL_JOB) return null;
143
+ const sock = socketPath(state);
144
+ if (process.platform === "win32" ? !existsSync(path2.join(path2.dirname(configFile()), "control.json")) : !existsSync(sock))
145
+ return null;
146
+ let s;
147
+ try {
148
+ s = JSON.parse(stdin);
149
+ } catch {
150
+ return null;
151
+ }
152
+ const id = typeof s.session_id === "string" ? s.session_id : "";
153
+ if (!id) return null;
154
+ const session = `claude-code:${id}`;
155
+ if (event === "Stop")
156
+ return ask(sock, { op: "stop", session, active: s.stop_hook_active === true }, 300).then(
157
+ (r) => r?.op === "block" ? JSON.stringify({ decision: "block", reason: r.reason }) : null
158
+ );
159
+ const tool = typeof s.tool_name === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(s.tool_name) ? s.tool_name : void 0;
160
+ const turn = typeof s.prompt_id === "string" ? s.prompt_id : void 0;
161
+ const attention = `perm-${createHash("sha256").update(`${turn ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
162
+ return ask(sock, { op: "permission", session, attention, ...tool ? { tool } : {} }, 125e3).then(
163
+ (r) => r?.op === "decision" ? JSON.stringify({
164
+ hookSpecificOutput: {
165
+ hookEventName: "PermissionRequest",
166
+ decision: r.behavior === "allow" ? { behavior: "allow" } : { behavior: "deny", message: r.message || "Denied by the owner (sessionpipe control)." }
167
+ }
168
+ }) : null
169
+ );
170
+ }
77
171
  function finish(spawned) {
78
- const ms = Number(process.hrtime.bigint() - t0) / 1e6;
172
+ const total = Number(process.hrtime.bigint() - t0) / 1e6;
173
+ const ms = workMs ?? total;
79
174
  try {
80
175
  mkdirSync(state, { recursive: true });
81
176
  appendFileSync(
82
- path.join(state, "timing.jsonl"),
83
- `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned })}
177
+ path2.join(state, "timing.jsonl"),
178
+ `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned, ...workMs !== null ? { control_wait_ms: Math.round(total - workMs) } : {} })}
84
179
  `
85
180
  );
86
181
  } catch {
@@ -105,13 +200,13 @@ function sessionIdOf(stdin) {
105
200
  return m?.[1] ?? "";
106
201
  }
107
202
  function beatDue(id) {
108
- const stamp = path.join(state, "beats", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, "_")}`);
203
+ const stamp = path2.join(state, "beats", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, "_")}`);
109
204
  try {
110
205
  if (Date.now() - statSync(stamp).mtimeMs < 6e4) return false;
111
206
  } catch {
112
207
  }
113
208
  try {
114
- mkdirSync(path.dirname(stamp), { recursive: true, mode: 448 });
209
+ mkdirSync(path2.dirname(stamp), { recursive: true, mode: 448 });
115
210
  writeFileSync(stamp, "", { mode: 384 });
116
211
  } catch {
117
212
  }
package/dist/hook.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/hook.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n// SPDX-License-Identifier: Apache-2.0\n// The hook path. A harness runs `node hook.js <harness> <event>`; this reads stdin,\n// writes one job file, spawns the worker detached, prints what the harness needs\n// ({} for Antigravity) and exits 0 \u2014 in milliseconds, with nothing but node:\n// builtins imported (the build checks that). No network, no schema library, no\n// config parsing beyond one small file read for the throttle.\nimport { spawn } from \"node:child_process\";\nimport { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst t0 = process.hrtime.bigint();\nconst [harness = \"\", event = \"\", ...rest] = process.argv.slice(2);\nconst env = process.env;\n\nfunction stateDir(): string {\n if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\"), \"sessionpipe\");\n return path.join(env.XDG_STATE_HOME || path.join(home, \".local\", \"state\"), \"sessionpipe\");\n}\nfunction configFile(): string {\n if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.APPDATA || path.join(home, \"AppData\", \"Roaming\"), \"sessionpipe\", \"config.json\");\n return path.join(env.XDG_CONFIG_HOME || path.join(home, \".config\"), \"sessionpipe\", \"config.json\");\n}\n\n// Antigravity parses stdout as the hook's answer; `{}` is \"carry on\" for every event.\nif (harness === \"antigravity\") {\n try {\n writeFileSync(1, \"{}\\n\");\n } catch {}\n}\n\nconst state = stateDir();\ntry {\n if (!harness || !event) throw new Error(\"usage: hook <harness> <event>\");\n let stdin = \"\";\n if (harness !== \"codex\" || event !== \"notify\") {\n try {\n const buf = readFileSync(0);\n stdin = buf.subarray(0, 1024 * 1024).toString(\"utf8\");\n } catch {}\n }\n // Tool events are the bulk of what a harness fires. When no sink takes tier \u2265 1\n // they are only a \"still working\" signal: one heartbeat a minute per session.\n const TOOL = /^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/;\n if (TOOL.test(event) && !anySinkAbove(0)) {\n const id = sessionIdOf(stdin);\n if (!id || !beatDue(id)) finish(false);\n }\n mkdirSync(path.join(state, \"jobs\"), { recursive: true, mode: 0o700 });\n const job = {\n harness,\n event,\n argv: [harness, event, ...rest],\n stdin,\n cwd: process.cwd(),\n env: pick([\n \"CLAUDE_CODE_HOST_SESSION_ID\",\n \"CLAUDE_CONFIG_DIR\",\n \"CLAUDE_PROJECT_DIR\",\n \"CODEX_HOME\",\n \"GEMINI_CLI_HOME\",\n ]),\n ppid: process.ppid,\n at: Date.now(),\n };\n const jobFile = path.join(state, \"jobs\", `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);\n writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });\n if (!env.SESSIONPIPE_NO_WORKER) {\n const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), \"worker.js\");\n const p = spawn(process.execPath, [worker, jobFile], { detached: true, stdio: \"ignore\", windowsHide: true, env });\n p.unref();\n }\n finish(true);\n} catch {\n finish(false); // the hook must be invisible to the harness\n}\n\nfunction finish(spawned: boolean): never {\n const ms = Number(process.hrtime.bigint() - t0) / 1e6;\n try {\n mkdirSync(state, { recursive: true });\n appendFileSync(\n path.join(state, \"timing.jsonl\"),\n `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned })}\\n`,\n );\n } catch {}\n process.exit(0);\n}\nfunction pick(names: string[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const n of names) if (env[n] !== undefined) out[n] = env[n] as string;\n return out;\n}\nfunction anySinkAbove(tier: number): boolean {\n try {\n const c = JSON.parse(readFileSync(configFile(), \"utf8\")) as { sinks?: { tier?: number; paused?: string }[] };\n return (c.sinks ?? []).some((s) => Number(s.tier) > tier && !s.paused);\n } catch {\n return false;\n }\n}\nfunction sessionIdOf(stdin: string): string {\n const m = /\"(?:session_id|sessionId|conversationId)\"\\s*:\\s*\"([^\"]{1,128})\"/.exec(stdin);\n return m?.[1] ?? \"\";\n}\n/** One heartbeat per minute per session: the stamp file's mtime is the clock. */\nfunction beatDue(id: string): boolean {\n const stamp = path.join(state, \"beats\", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, \"_\")}`);\n try {\n if (Date.now() - statSync(stamp).mtimeMs < 60_000) return false;\n } catch {}\n try {\n mkdirSync(path.dirname(stamp), { recursive: true, mode: 0o700 });\n writeFileSync(stamp, \"\", { mode: 0o600 });\n } catch {}\n return true;\n}\n"],
5
- "mappings": ";;;;AAOA,SAAS,aAAa;AACtB,SAAS,gBAAgB,WAAW,cAAc,UAAU,qBAAqB;AACjF,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,KAAK,QAAQ,OAAO,OAAO;AACjC,IAAM,CAAC,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC;AAChE,IAAM,MAAM,QAAQ;AAEpB,SAAS,WAAmB;AAC1B,MAAI,IAAI,kBAAmB,QAAO,KAAK,QAAQ,IAAI,iBAAiB;AACpE,QAAM,OAAO,GAAG,QAAQ;AACxB,MAAI,QAAQ,aAAa;AACvB,WAAO,KAAK,KAAK,IAAI,gBAAgB,KAAK,KAAK,MAAM,WAAW,OAAO,GAAG,aAAa;AACzF,SAAO,KAAK,KAAK,IAAI,kBAAkB,KAAK,KAAK,MAAM,UAAU,OAAO,GAAG,aAAa;AAC1F;AACA,SAAS,aAAqB;AAC5B,MAAI,IAAI,mBAAoB,QAAO,KAAK,QAAQ,IAAI,kBAAkB;AACtE,QAAM,OAAO,GAAG,QAAQ;AACxB,MAAI,QAAQ,aAAa;AACvB,WAAO,KAAK,KAAK,IAAI,WAAW,KAAK,KAAK,MAAM,WAAW,SAAS,GAAG,eAAe,aAAa;AACrG,SAAO,KAAK,KAAK,IAAI,mBAAmB,KAAK,KAAK,MAAM,SAAS,GAAG,eAAe,aAAa;AAClG;AAGA,IAAI,YAAY,eAAe;AAC7B,MAAI;AACF,kBAAc,GAAG,MAAM;AAAA,EACzB,QAAQ;AAAA,EAAC;AACX;AAEA,IAAM,QAAQ,SAAS;AACvB,IAAI;AACF,MAAI,CAAC,WAAW,CAAC,MAAO,OAAM,IAAI,MAAM,+BAA+B;AACvE,MAAI,QAAQ;AACZ,MAAI,YAAY,WAAW,UAAU,UAAU;AAC7C,QAAI;AACF,YAAM,MAAM,aAAa,CAAC;AAC1B,cAAQ,IAAI,SAAS,GAAG,OAAO,IAAI,EAAE,SAAS,MAAM;AAAA,IACtD,QAAQ;AAAA,IAAC;AAAA,EACX;AAGA,QAAM,OAAO;AACb,MAAI,KAAK,KAAK,KAAK,KAAK,CAAC,aAAa,CAAC,GAAG;AACxC,UAAM,KAAK,YAAY,KAAK;AAC5B,QAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAG,QAAO,KAAK;AAAA,EACvC;AACA,YAAU,KAAK,KAAK,OAAO,MAAM,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACpE,QAAM,MAAM;AAAA,IACV;AAAA,IACA;AAAA,IACA,MAAM,CAAC,SAAS,OAAO,GAAG,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,QAAQ,IAAI;AAAA,IACjB,KAAK,KAAK;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,IACD,MAAM,QAAQ;AAAA,IACd,IAAI,KAAK,IAAI;AAAA,EACf;AACA,QAAM,UAAU,KAAK,KAAK,OAAO,QAAQ,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO;AACpH,gBAAc,SAAS,KAAK,UAAU,GAAG,GAAG,EAAE,MAAM,IAAM,CAAC;AAC3D,MAAI,CAAC,IAAI,uBAAuB;AAC9B,UAAM,SAAS,KAAK,KAAK,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,WAAW;AAClF,UAAM,IAAI,MAAM,QAAQ,UAAU,CAAC,QAAQ,OAAO,GAAG,EAAE,UAAU,MAAM,OAAO,UAAU,aAAa,MAAM,IAAI,CAAC;AAChH,MAAE,MAAM;AAAA,EACV;AACA,SAAO,IAAI;AACb,QAAQ;AACN,SAAO,KAAK;AACd;AAEA,SAAS,OAAO,SAAyB;AACvC,QAAM,KAAK,OAAO,QAAQ,OAAO,OAAO,IAAI,EAAE,IAAI;AAClD,MAAI;AACF,cAAU,OAAO,EAAE,WAAW,KAAK,CAAC;AACpC;AAAA,MACE,KAAK,KAAK,OAAO,cAAc;AAAA,MAC/B,GAAG,KAAK,UAAU,EAAE,IAAI,KAAK,IAAI,GAAG,SAAS,OAAO,IAAI,KAAK,MAAM,KAAK,EAAE,IAAI,IAAI,QAAQ,CAAC,CAAC;AAAA;AAAA,IAC9F;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,UAAQ,KAAK,CAAC;AAChB;AACA,SAAS,KAAK,OAAyC;AACrD,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,MAAO,KAAI,IAAI,CAAC,MAAM,OAAW,KAAI,CAAC,IAAI,IAAI,CAAC;AAC/D,SAAO;AACT;AACA,SAAS,aAAa,MAAuB;AAC3C,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,aAAa,WAAW,GAAG,MAAM,CAAC;AACvD,YAAQ,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,MAAM,OAAO,EAAE,IAAI,IAAI,QAAQ,CAAC,EAAE,MAAM;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AACA,SAAS,YAAY,OAAuB;AAC1C,QAAM,IAAI,kEAAkE,KAAK,KAAK;AACtF,SAAO,IAAI,CAAC,KAAK;AACnB;AAEA,SAAS,QAAQ,IAAqB;AACpC,QAAM,QAAQ,KAAK,KAAK,OAAO,SAAS,GAAG,OAAO,IAAI,GAAG,QAAQ,mBAAmB,GAAG,CAAC,EAAE;AAC1F,MAAI;AACF,QAAI,KAAK,IAAI,IAAI,SAAS,KAAK,EAAE,UAAU,IAAQ,QAAO;AAAA,EAC5D,QAAQ;AAAA,EAAC;AACT,MAAI;AACF,cAAU,KAAK,QAAQ,KAAK,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC/D,kBAAc,OAAO,IAAI,EAAE,MAAM,IAAM,CAAC;AAAA,EAC1C,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;",
6
- "names": []
3
+ "sources": ["../src/hook.ts", "../src/control/local.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n// SPDX-License-Identifier: Apache-2.0\n// The hook path. A harness runs `node hook.js <harness> <event>`; this reads stdin,\n// writes one job file, spawns the worker detached, prints what the harness needs\n// ({} for Antigravity) and exits 0 \u2014 in milliseconds, with nothing but node:\n// builtins imported (the build checks that). No network, no schema library, no\n// config parsing beyond one small file read for the throttle.\nimport { spawn } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ask, socketPath } from \"./control/local.js\";\n\nconst t0 = process.hrtime.bigint();\n/** The hook's own work, when a control wait follows it (the wait is not the hook's cost). */\nlet workMs: number | null = null;\nconst [harness = \"\", event = \"\", ...rest] = process.argv.slice(2);\nconst env = process.env;\n\nfunction stateDir(): string {\n if (env.SESSIONPIPE_STATE) return path.resolve(env.SESSIONPIPE_STATE);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.LOCALAPPDATA || path.join(home, \"AppData\", \"Local\"), \"sessionpipe\");\n return path.join(env.XDG_STATE_HOME || path.join(home, \".local\", \"state\"), \"sessionpipe\");\n}\nfunction configFile(): string {\n if (env.SESSIONPIPE_CONFIG) return path.resolve(env.SESSIONPIPE_CONFIG);\n const home = os.homedir();\n if (process.platform === \"win32\")\n return path.join(env.APPDATA || path.join(home, \"AppData\", \"Roaming\"), \"sessionpipe\", \"config.json\");\n return path.join(env.XDG_CONFIG_HOME || path.join(home, \".config\"), \"sessionpipe\", \"config.json\");\n}\n\n// Antigravity parses stdout as the hook's answer; `{}` is \"carry on\" for every event.\nif (harness === \"antigravity\") {\n try {\n writeFileSync(1, \"{}\\n\");\n } catch {}\n}\n\nconst state = stateDir();\ntry {\n if (!harness || !event) throw new Error(\"usage: hook <harness> <event>\");\n let stdin = \"\";\n if (harness !== \"codex\" || event !== \"notify\") {\n try {\n const buf = readFileSync(0);\n stdin = buf.subarray(0, 1024 * 1024).toString(\"utf8\");\n } catch {}\n }\n // Tool events are the bulk of what a harness fires. When no sink takes tier \u2265 1\n // they are only a \"still working\" signal: one heartbeat a minute per session.\n const TOOL = /^(PreToolUse|PostToolUse|PostToolUseFailure|BeforeTool|AfterTool|PostInvocation)$/;\n if (TOOL.test(event) && !anySinkAbove(0)) {\n const id = sessionIdOf(stdin);\n if (!id || !beatDue(id)) finish(false);\n }\n // A Stop asks the daemon BEFORE its event goes to the worker: the worker reports the\n // Stop to the daemon too, and a message queued for this Stop must be taken here,\n // not rerouted because the event got there first.\n const stopAnswer = event === \"Stop\" ? await (controlAsk(stdin) ?? Promise.resolve(null)) : null;\n mkdirSync(path.join(state, \"jobs\"), { recursive: true, mode: 0o700 });\n const job = {\n harness,\n event,\n argv: [harness, event, ...rest],\n stdin,\n cwd: process.cwd(),\n env: pick([\n \"CLAUDE_CODE_HOST_SESSION_ID\",\n \"CLAUDE_CONFIG_DIR\",\n \"CLAUDE_PROJECT_DIR\",\n \"CODEX_HOME\",\n \"GEMINI_CLI_HOME\",\n ]),\n ppid: process.ppid,\n at: Date.now(),\n };\n const jobFile = path.join(state, \"jobs\", `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);\n writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });\n if (!env.SESSIONPIPE_NO_WORKER) {\n const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), \"worker.js\");\n const p = spawn(process.execPath, [worker, jobFile], { detached: true, stdio: \"ignore\", windowsHide: true, env });\n p.unref();\n }\n if (stopAnswer) {\n try {\n writeFileSync(1, `${stopAnswer}\\n`);\n } catch {}\n }\n // A PermissionRequest waits AFTER its event is on its way, so the receiver already\n // shows the Allow / Deny this answer is for.\n const ctl = event === \"PermissionRequest\" ? controlAsk(stdin) : null;\n if (ctl) {\n workMs = Number(process.hrtime.bigint() - t0) / 1e6;\n const out = await ctl;\n if (out) {\n try {\n writeFileSync(1, `${out}\\n`);\n } catch {}\n }\n }\n finish(true);\n} catch {\n finish(false); // the hook must be invisible to the harness\n}\n\n/** Control (spec/CONTROL.md \u00A76, \u00A79): only on a machine with control paired, and only\n * for a message the daemon itself verified. A Stop hook asks for a queued message and\n * answers a block with it; a PermissionRequest hook waits for the person's signed\n * answer while the terminal shows its own prompt, first answer wins. Anything that\n * goes wrong (no daemon, a timeout) prints nothing: the harness carries on as usual. */\nfunction controlAsk(stdin: string): Promise<string | null> | null {\n if (harness !== \"claude-code\" || (event !== \"Stop\" && event !== \"PermissionRequest\")) return null;\n if (env.SESSIONPIPE_CONTROL_JOB) return null; // a headless turn the daemon itself runs\n const sock = socketPath(state);\n if (\n process.platform === \"win32\"\n ? !existsSync(path.join(path.dirname(configFile()), \"control.json\"))\n : !existsSync(sock)\n )\n return null;\n let s: Record<string, unknown>;\n try {\n s = JSON.parse(stdin) as Record<string, unknown>;\n } catch {\n return null;\n }\n const id = typeof s.session_id === \"string\" ? s.session_id : \"\";\n if (!id) return null;\n const session = `claude-code:${id}`;\n if (event === \"Stop\")\n return ask(sock, { op: \"stop\", session, active: s.stop_hook_active === true }, 300).then((r) =>\n r?.op === \"block\" ? JSON.stringify({ decision: \"block\", reason: r.reason }) : null,\n );\n // The attention id the adapter puts on attention.needed (claude-code.ts), so the\n // receiver's Allow / Deny names this very prompt.\n const tool =\n typeof s.tool_name === \"string\" && /^[A-Za-z0-9_.:-]{1,120}$/.test(s.tool_name) ? s.tool_name : undefined;\n const turn = typeof s.prompt_id === \"string\" ? s.prompt_id : undefined;\n const attention = `perm-${createHash(\"sha256\")\n .update(`${turn ?? \"\"}|${tool ?? \"\"}|${JSON.stringify(s.tool_input ?? null)}`)\n .digest(\"hex\")\n .slice(0, 16)}`;\n return ask(sock, { op: \"permission\", session, attention, ...(tool ? { tool } : {}) }, 125_000).then((r) =>\n r?.op === \"decision\"\n ? JSON.stringify({\n hookSpecificOutput: {\n hookEventName: \"PermissionRequest\",\n decision:\n r.behavior === \"allow\"\n ? { behavior: \"allow\" }\n : { behavior: \"deny\", message: r.message || \"Denied by the owner (sessionpipe control).\" },\n },\n })\n : null,\n );\n}\n\nfunction finish(spawned: boolean): never {\n const total = Number(process.hrtime.bigint() - t0) / 1e6;\n const ms = workMs ?? total;\n try {\n mkdirSync(state, { recursive: true });\n appendFileSync(\n path.join(state, \"timing.jsonl\"),\n `${JSON.stringify({ at: Date.now(), harness, event, ms: Math.round(ms * 10) / 10, spawned, ...(workMs !== null ? { control_wait_ms: Math.round(total - workMs) } : {}) })}\\n`,\n );\n } catch {}\n process.exit(0);\n}\nfunction pick(names: string[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const n of names) if (env[n] !== undefined) out[n] = env[n] as string;\n return out;\n}\nfunction anySinkAbove(tier: number): boolean {\n try {\n const c = JSON.parse(readFileSync(configFile(), \"utf8\")) as { sinks?: { tier?: number; paused?: string }[] };\n return (c.sinks ?? []).some((s) => Number(s.tier) > tier && !s.paused);\n } catch {\n return false;\n }\n}\nfunction sessionIdOf(stdin: string): string {\n const m = /\"(?:session_id|sessionId|conversationId)\"\\s*:\\s*\"([^\"]{1,128})\"/.exec(stdin);\n return m?.[1] ?? \"\";\n}\n/** One heartbeat per minute per session: the stamp file's mtime is the clock. */\nfunction beatDue(id: string): boolean {\n const stamp = path.join(state, \"beats\", `${harness}-${id.replace(/[^A-Za-z0-9_-]/g, \"_\")}`);\n try {\n if (Date.now() - statSync(stamp).mtimeMs < 60_000) return false;\n } catch {}\n try {\n mkdirSync(path.dirname(stamp), { recursive: true, mode: 0o700 });\n writeFileSync(stamp, \"\", { mode: 0o600 });\n } catch {}\n return true;\n}\n", "// SPDX-License-Identifier: Apache-2.0\n// The daemon's local socket (spec/CONTROL.md \u00A78): how the waiter, the Stop and\n// PermissionRequest hooks and the worker talk to the one daemon on this machine.\n// Line-delimited JSON, one request per connection. A Unix socket in the state\n// directory (0600, in a 0700 dir); a per-user named pipe on Windows. Only node:\n// builtins \u2014 the hook bundle imports this.\nimport net from \"node:net\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nexport type LocalRequest =\n /** A waiter parks until a message for its session arrives. */\n | { op: \"wait\"; session: string; pid?: number }\n /** A Stop hook asks for a queued message (turn mode). */\n | { op: \"stop\"; session: string; active: boolean }\n /** A PermissionRequest hook parks until a verified answer, or until it's released. */\n | { op: \"permission\"; session: string; attention: string; tool?: string }\n /** The worker reports a hook event: turn state and attention closure. */\n | { op: \"event\"; session: string; event: string; transcript?: string; cwd?: string }\n /** `sessionpipe control status`. */\n | { op: \"status\" };\n\nexport type LocalReply =\n | { op: \"message\"; text: string; id: string }\n | { op: \"block\"; reason: string; id: string }\n | { op: \"decision\"; behavior: \"allow\" | \"deny\"; message?: string; id: string }\n | { op: \"none\" }\n /** A newer waiter for the same session took over; this one stops. */\n | { op: \"replaced\" }\n | { op: \"ok\" }\n | { op: \"status\"; status: unknown };\n\n/** Where the daemon listens. `SESSIONPIPE_SOCKET` overrides (tests). */\nexport function socketPath(state: string, env: NodeJS.ProcessEnv = process.env): string {\n if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;\n if (process.platform === \"win32\") return `\\\\\\\\.\\\\pipe\\\\sessionpipe-control-${os.userInfo().username}`;\n return path.join(state, \"control.sock\");\n}\n\n/** One request, one reply. `timeoutMs` bounds the whole exchange; null on any\n * failure (no daemon, a timeout, a garbled reply) \u2014 callers treat that as \"none\". */\nexport function ask(sock: string, req: LocalRequest, timeoutMs: number): Promise<LocalReply | null> {\n return new Promise((resolve) => {\n let buf = \"\";\n let done = false;\n const s = net.connect(sock);\n const finish = (r: LocalReply | null) => {\n if (done) return;\n done = true;\n clearTimeout(t);\n s.destroy();\n resolve(r);\n };\n const t = setTimeout(() => finish(null), timeoutMs);\n s.on(\"connect\", () => s.write(`${JSON.stringify(req)}\\n`));\n s.on(\"data\", (d) => {\n buf += d.toString(\"utf8\");\n const i = buf.indexOf(\"\\n\");\n if (i < 0) return;\n try {\n finish(JSON.parse(buf.slice(0, i)) as LocalReply);\n } catch {\n finish(null);\n }\n });\n s.on(\"error\", () => finish(null));\n s.on(\"close\", () => finish(null));\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["os", "path", "state", "env", "finish", "path", "os"]
7
7
  }
@@ -1,4 +1,8 @@
1
1
  import type { DeliveryResult, Event, Sink, SinkConfig, Tier } from "@sessionpipe/core";
2
+ /** The Standard Webhooks message id (HTTP.md §2): the same batch sent again carries
3
+ * the same id, so a receiver that dedupes on webhook-id sees a retry as one message.
4
+ * Derived from the events' own ids, which the outbox keeps across retries. */
5
+ export declare function webhookId(events: Pick<Event, "id">[]): string;
2
6
  export declare class HttpSink implements Sink {
3
7
  private readonly cfg;
4
8
  readonly tier: Tier;
@@ -1 +1 @@
1
- {"version":3,"file":"http-sink.d.ts","sourceRoot":"","sources":["../src/http-sink.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIvF,qBAAa,QAAS,YAAW,IAAI;IAKjC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,IAAI;IALrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEV,GAAG,EAAE,UAAU,EACvB,IAAI,EAAE,IAAI;IAOrB,OAAO,CAAC,SAAS;IAOX,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;CA8DrD"}
1
+ {"version":3,"file":"http-sink.d.ts","sourceRoot":"","sources":["../src/http-sink.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIvF;;+EAE+E;AAC/E,wBAAgB,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,CAK7D;AAED,qBAAa,QAAS,YAAW,IAAI;IAKjC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,QAAQ,CAAC,IAAI,EAAE,IAAI;IALrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEV,GAAG,EAAE,UAAU,EACvB,IAAI,EAAE,IAAI;IAOrB,OAAO,CAAC,SAAS;IAOX,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;CA8DrD"}
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
- // sessionpipe 0.1.1 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/http-sink.ts
4
- import { createHmac, randomUUID } from "node:crypto";
4
+ import { createHash, createHmac } from "node:crypto";
5
5
  var TIMEOUT_MS = 15e3;
6
+ function webhookId(events) {
7
+ return `msg_${createHash("sha256").update(events.map((e) => e.id).join(",")).digest("base64url").slice(0, 32)}`;
8
+ }
6
9
  var HttpSink = class {
7
10
  constructor(cfg, tier) {
8
11
  this.cfg = cfg;
@@ -25,7 +28,7 @@ var HttpSink = class {
25
28
  const headers = { "content-type": "application/json", "sessionpipe-protocol": "1" };
26
29
  if (this.cfg.token) headers.authorization = `Bearer ${this.cfg.token}`;
27
30
  if (this.cfg.secret?.startsWith("whsec_")) {
28
- const id = `msg_${randomUUID()}`;
31
+ const id = webhookId(events);
29
32
  const ts = Math.floor(Date.now() / 1e3);
30
33
  const key = Buffer.from(this.cfg.secret.slice(6), "base64");
31
34
  const sig = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
@@ -97,8 +100,8 @@ import {
97
100
  unlinkSync as unlinkSync4,
98
101
  writeFileSync as writeFileSync6
99
102
  } from "node:fs";
100
- import os2 from "node:os";
101
- import path11 from "node:path";
103
+ import os3 from "node:os";
104
+ import path12 from "node:path";
102
105
 
103
106
  // ../core/src/adapters/antigravity.ts
104
107
  import {
@@ -378,7 +381,7 @@ function entryFor(event, harness, cmd, o) {
378
381
  const matcher = o.matcherFor(event);
379
382
  return {
380
383
  ...matcher !== void 0 ? { matcher } : {},
381
- hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeout }]
384
+ hooks: [{ type: "command", command: cmd([harness, event]).command, timeout: o.timeoutFor?.(event) ?? o.timeout }]
382
385
  };
383
386
  }
384
387
  function installClaudeShaped(file, harness, events, cmd, o) {
@@ -745,7 +748,7 @@ function antigravityFacts(file, opts) {
745
748
 
746
749
  // ../core/src/adapters/claude-code.ts
747
750
  import { execFileSync as execFileSync2 } from "node:child_process";
748
- import { createHash } from "node:crypto";
751
+ import { createHash as createHash2 } from "node:crypto";
749
752
  import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
750
753
  import path5 from "node:path";
751
754
 
@@ -802,7 +805,13 @@ var CLAUDE_EVENTS = [
802
805
  ];
803
806
  var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
804
807
  var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
805
- var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
808
+ var OPTS = {
809
+ matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0,
810
+ timeout: 5,
811
+ // The hook waits up to 125 s for a verified control answer, and only when control
812
+ // is paired on this machine; everywhere else it exits in milliseconds as before.
813
+ timeoutFor: (e) => e === "PermissionRequest" ? 130 : void 0
814
+ };
806
815
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
807
816
  function claudeDirs(env = process.env, home = HOME) {
808
817
  const out = [];
@@ -859,7 +868,7 @@ var claudeCode = {
859
868
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
860
869
  const ms = typeof s.duration_ms === "number" ? Math.round(s.duration_ms) : void 0;
861
870
  const agent_id = typeof s.agent_id === "string" ? s.agent_id : void 0;
862
- const permissionId = () => `perm-${createHash("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
871
+ const permissionId = () => `perm-${createHash2("sha256").update(`${turn_id ?? ""}|${tool ?? ""}|${JSON.stringify(s.tool_input ?? null)}`).digest("hex").slice(0, 16)}`;
863
872
  const ev = (type, data) => ({ type, data: prune2(data), harnessEvent: event });
864
873
  const events = [];
865
874
  switch (event) {
@@ -1246,9 +1255,9 @@ function claudeFacts(file, sessionId, hints, state) {
1246
1255
  }
1247
1256
  const name = live?.name ? { title: live.name, source: live.name_source } : typeof mem.name === "string" ? { title: mem.name, source: mem.name_source } : null;
1248
1257
  const pick = name && name.source === "custom" && name || custom && { title: custom, source: "custom" } || name || ai && { title: ai, source: "harness" };
1249
- let ask = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1250
- if (!ask && size > 64 * 1024 && !mem.first_ask_none) {
1251
- ask = scanLines(file, (l) => {
1258
+ let ask2 = typeof mem.first_ask === "string" ? mem.first_ask : firstAsk(H);
1259
+ if (!ask2 && size > 64 * 1024 && !mem.first_ask_none) {
1260
+ ask2 = scanLines(file, (l) => {
1252
1261
  try {
1253
1262
  const t = askText(JSON.parse(l));
1254
1263
  return t ? appTitle(t) : null;
@@ -1256,17 +1265,17 @@ function claudeFacts(file, sessionId, hints, state) {
1256
1265
  return null;
1257
1266
  }
1258
1267
  });
1259
- if (!ask && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1268
+ if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
1260
1269
  }
1261
- if (ask) {
1262
- out.first_ask = ask;
1263
- if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask });
1270
+ if (ask2) {
1271
+ out.first_ask = ask2;
1272
+ if (!mem.first_ask) state.save(NAME2, id, { first_ask: ask2 });
1264
1273
  }
1265
1274
  if (pick) {
1266
1275
  out.title = String(pick.title).slice(0, 120);
1267
1276
  out.title_source = pick.source;
1268
- } else if (ask) {
1269
- out.title = ask;
1277
+ } else if (ask2) {
1278
+ out.title = ask2;
1270
1279
  out.title_source = "first-ask";
1271
1280
  }
1272
1281
  const acct = claudeAccount(dir);
@@ -2227,7 +2236,7 @@ function takeLock(root, ref, staleMs = 5 * 6e4) {
2227
2236
  }
2228
2237
 
2229
2238
  // ../core/src/privacy/pii.ts
2230
- import { createHash as createHash2 } from "node:crypto";
2239
+ import { createHash as createHash3 } from "node:crypto";
2231
2240
  var RULESET_PII = "pii@1";
2232
2241
  var EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
2233
2242
  var PHONE = /(?<![\w.])(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d{2,4}(?:[ .-]?\d{2,4}){1,3}(?!\w)/g;
@@ -2249,7 +2258,7 @@ function reducePii(text, home) {
2249
2258
  return out;
2250
2259
  }
2251
2260
  function hashMachine(name) {
2252
- return createHash2("sha256").update(name).digest("hex").slice(0, 8);
2261
+ return createHash3("sha256").update(name).digest("hex").slice(0, 8);
2253
2262
  }
2254
2263
  function reducePiiDeep(v, home) {
2255
2264
  if (typeof v === "string") return reducePii(v, home);
@@ -2566,18 +2575,72 @@ var StdoutSink = class {
2566
2575
  }
2567
2576
  };
2568
2577
 
2578
+ // src/control/local.ts
2579
+ import net from "node:net";
2580
+ import os2 from "node:os";
2581
+ import path11 from "node:path";
2582
+ function socketPath(state, env = process.env) {
2583
+ if (env.SESSIONPIPE_SOCKET) return env.SESSIONPIPE_SOCKET;
2584
+ if (process.platform === "win32") return `\\\\.\\pipe\\sessionpipe-control-${os2.userInfo().username}`;
2585
+ return path11.join(state, "control.sock");
2586
+ }
2587
+ function ask(sock, req, timeoutMs) {
2588
+ return new Promise((resolve) => {
2589
+ let buf = "";
2590
+ let done = false;
2591
+ const s = net.connect(sock);
2592
+ const finish = (r) => {
2593
+ if (done) return;
2594
+ done = true;
2595
+ clearTimeout(t);
2596
+ s.destroy();
2597
+ resolve(r);
2598
+ };
2599
+ const t = setTimeout(() => finish(null), timeoutMs);
2600
+ s.on("connect", () => s.write(`${JSON.stringify(req)}
2601
+ `));
2602
+ s.on("data", (d) => {
2603
+ buf += d.toString("utf8");
2604
+ const i = buf.indexOf("\n");
2605
+ if (i < 0) return;
2606
+ try {
2607
+ finish(JSON.parse(buf.slice(0, i)));
2608
+ } catch {
2609
+ finish(null);
2610
+ }
2611
+ });
2612
+ s.on("error", () => finish(null));
2613
+ s.on("close", () => finish(null));
2614
+ });
2615
+ }
2616
+
2569
2617
  // src/run.ts
2618
+ async function tellDaemon(state, job, session, transcript) {
2619
+ const sock = socketPath(state);
2620
+ if (process.platform !== "win32" && !existsSync8(sock)) return;
2621
+ await ask(
2622
+ sock,
2623
+ {
2624
+ op: "event",
2625
+ session: `claude-code:${session}`,
2626
+ event: job.event,
2627
+ cwd: job.cwd,
2628
+ ...transcript ? { transcript } : {}
2629
+ },
2630
+ 300
2631
+ );
2632
+ }
2570
2633
  function enqueueJob(state, job) {
2571
2634
  const jobs = jobsDir(state);
2572
2635
  mkdirSync7(jobs, { recursive: true, mode: 448 });
2573
- const f = path11.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2636
+ const f = path12.join(jobs, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`);
2574
2637
  writeFileSync6(f, JSON.stringify(job), { mode: 384 });
2575
2638
  return f;
2576
2639
  }
2577
2640
  function log(state, line) {
2578
2641
  try {
2579
2642
  mkdirSync7(state, { recursive: true, mode: 448 });
2580
- const f = path11.join(state, "log");
2643
+ const f = path12.join(state, "log");
2581
2644
  try {
2582
2645
  if (statSync5(f).size > 1024 * 1024) renameSync2(f, `${f}.1`);
2583
2646
  } catch {
@@ -2588,7 +2651,7 @@ function log(state, line) {
2588
2651
  }
2589
2652
  }
2590
2653
  function factsState(state) {
2591
- const file = (h, s) => path11.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2654
+ const file = (h, s) => path12.join(state, "facts", safeId(h), `${safeId(s)}.json`);
2592
2655
  return {
2593
2656
  get(h, s) {
2594
2657
  try {
@@ -2601,7 +2664,7 @@ function factsState(state) {
2601
2664
  const cur = this.get(h, s);
2602
2665
  const next = { ...cur, ...patch };
2603
2666
  if (JSON.stringify(next) === JSON.stringify(cur)) return;
2604
- mkdirSync7(path11.dirname(file(h, s)), { recursive: true, mode: 448 });
2667
+ mkdirSync7(path12.dirname(file(h, s)), { recursive: true, mode: 448 });
2605
2668
  writeFileSync6(file(h, s), JSON.stringify(next), { mode: 384 });
2606
2669
  }
2607
2670
  };
@@ -2613,7 +2676,7 @@ function buildSinks(sinks) {
2613
2676
  const tier = Math.min(s.tier, s.max_tier ?? 3);
2614
2677
  if (s.url === "stdout") out.push(new StdoutSink(s.name, tier, !!s.pii));
2615
2678
  else if (s.url.startsWith("file:"))
2616
- out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os2.homedir()), tier, !!s.pii));
2679
+ out.push(new FileSink(s.name, s.url.slice(5).replace(/^~(?=\/|$)/, os3.homedir()), tier, !!s.pii));
2617
2680
  else if (/^https?:\/\//.test(s.url)) out.push(new HttpSink(s, tier));
2618
2681
  }
2619
2682
  return out;
@@ -2625,6 +2688,7 @@ async function runJob(job, opts = {}) {
2625
2688
  if (!adapter) return { events: [], delivered: {} };
2626
2689
  const r = adapter.fromHook({ argv: job.argv, stdin: job.stdin, env: { ...process.env, ...job.env }, cwd: job.cwd });
2627
2690
  if (!r || !r.session.id) return { events: [], delivered: {} };
2691
+ if (job.harness === "claude-code") await tellDaemon(state, job, r.session.id, r.transcript);
2628
2692
  const outbox = new Outbox(state);
2629
2693
  const ref = { harness: job.harness, session: r.session.id };
2630
2694
  let release = takeLock(state, ref);
@@ -2659,7 +2723,7 @@ async function runJob(job, opts = {}) {
2659
2723
  ...learned.branch ? { branch: learned.branch } : {},
2660
2724
  id: r.session.id,
2661
2725
  seq: 0,
2662
- machine: machineName(cfg, os2.hostname()),
2726
+ machine: machineName(cfg, os3.hostname()),
2663
2727
  ...cwdRaw ? { cwd: tilde(cwdRaw) } : {},
2664
2728
  ...r.session.model ? { model: r.session.model } : {}
2665
2729
  };
@@ -2762,7 +2826,7 @@ async function flush(state, sinkConfigs, outbox, first) {
2762
2826
  const { events, offset: next } = outbox.read(ref, offset, room);
2763
2827
  if (!events.length) break;
2764
2828
  for (const e of events) {
2765
- const f = filterEvent(e, sink.tier, sink.pii, { home: os2.homedir() });
2829
+ const f = filterEvent(e, sink.tier, sink.pii, { home: os3.homedir() });
2766
2830
  if (f) batch.push(f);
2767
2831
  }
2768
2832
  offset = next;
@@ -2794,7 +2858,7 @@ function applySinkVerdict(name, res) {
2794
2858
  } catch {
2795
2859
  }
2796
2860
  }
2797
- var jobsDir = (state) => path11.join(state, "jobs");
2861
+ var jobsDir = (state) => path12.join(state, "jobs");
2798
2862
  export {
2799
2863
  HttpSink,
2800
2864
  buildSinks,