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/README.md +35 -2
- package/dist/cli.js +2425 -219
- package/dist/cli.js.map +4 -4
- package/dist/control/cli.d.ts +4 -0
- package/dist/control/cli.d.ts.map +1 -0
- package/dist/control/daemon.d.ts +172 -0
- package/dist/control/daemon.d.ts.map +1 -0
- package/dist/control/local.d.ts +63 -0
- package/dist/control/local.d.ts.map +1 -0
- package/dist/control/pair.d.ts +50 -0
- package/dist/control/pair.d.ts.map +1 -0
- package/dist/control/reply.d.ts +19 -0
- package/dist/control/reply.d.ts.map +1 -0
- package/dist/control/route.d.ts +41 -0
- package/dist/control/route.d.ts.map +1 -0
- package/dist/control/sdk.d.ts +7 -0
- package/dist/control/sdk.d.ts.map +1 -0
- package/dist/control/store.d.ts +76 -0
- package/dist/control/store.d.ts.map +1 -0
- package/dist/control/wait.d.ts +8 -0
- package/dist/control/wait.d.ts.map +1 -0
- package/dist/hook.js +114 -19
- package/dist/hook.js.map +4 -4
- package/dist/http-sink.d.ts +4 -0
- package/dist/http-sink.d.ts.map +1 -1
- package/dist/index.js +92 -28
- package/dist/index.js.map +4 -4
- package/dist/run.d.ts.map +1 -1
- package/dist/worker.js +87 -23
- package/dist/worker.js.map +4 -4
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -1,28 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.
|
|
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 {
|
|
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
|
-
|
|
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
|
|
15
|
-
const home =
|
|
57
|
+
if (env.SESSIONPIPE_STATE) return path2.resolve(env.SESSIONPIPE_STATE);
|
|
58
|
+
const home = os2.homedir();
|
|
16
59
|
if (process.platform === "win32")
|
|
17
|
-
return
|
|
18
|
-
return
|
|
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
|
|
22
|
-
const home =
|
|
64
|
+
if (env.SESSIONPIPE_CONFIG) return path2.resolve(env.SESSIONPIPE_CONFIG);
|
|
65
|
+
const home = os2.homedir();
|
|
23
66
|
if (process.platform === "win32")
|
|
24
|
-
return
|
|
25
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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;
|
|
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
|
}
|
package/dist/http-sink.d.ts
CHANGED
|
@@ -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;
|
package/dist/http-sink.d.ts.map
CHANGED
|
@@ -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
|
+
// sessionpipe 0.2.0 · Apache-2.0 · https://sessionpipe.org
|
|
2
2
|
|
|
3
3
|
// src/http-sink.ts
|
|
4
|
-
import {
|
|
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 =
|
|
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
|
|
101
|
-
import
|
|
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 = {
|
|
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-${
|
|
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
|
|
1250
|
-
if (!
|
|
1251
|
-
|
|
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 (!
|
|
1268
|
+
if (!ask2 && size > 16 * 1024 * 1024) state.save(NAME2, id, { first_ask_none: true });
|
|
1260
1269
|
}
|
|
1261
|
-
if (
|
|
1262
|
-
out.first_ask =
|
|
1263
|
-
if (!mem.first_ask) state.save(NAME2, id, { first_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 (
|
|
1269
|
-
out.title =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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(
|
|
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(/^~(?=\/|$)/,
|
|
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,
|
|
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:
|
|
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) =>
|
|
2861
|
+
var jobsDir = (state) => path12.join(state, "jobs");
|
|
2798
2862
|
export {
|
|
2799
2863
|
HttpSink,
|
|
2800
2864
|
buildSinks,
|