sessionpipe 0.6.0 → 0.6.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/cli.js +20 -19
- package/dist/cli.js.map +3 -3
- package/dist/hook.js +5 -2
- package/dist/hook.js.map +2 -2
- package/dist/index.js +16 -15
- package/dist/index.js.map +2 -2
- package/dist/launcher.d.ts +2 -1
- package/dist/launcher.d.ts.map +1 -1
- package/dist/worker.js +16 -15
- package/dist/worker.js.map +2 -2
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// sessionpipe 0.6.
|
|
2
|
+
// sessionpipe 0.6.1 · Apache-2.0 · https://sessionpipe.org
|
|
3
3
|
|
|
4
4
|
// src/hook.ts
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -69,7 +69,10 @@ function configFile() {
|
|
|
69
69
|
}
|
|
70
70
|
if (harness === "antigravity") {
|
|
71
71
|
try {
|
|
72
|
-
writeFileSync(
|
|
72
|
+
writeFileSync(
|
|
73
|
+
1,
|
|
74
|
+
event === "PreToolUse" ? '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n' : "{}\n"
|
|
75
|
+
);
|
|
73
76
|
} catch {
|
|
74
77
|
}
|
|
75
78
|
}
|
package/dist/hook.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
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 /** `sessionpipe update` installed a new copy: restart onto it once nothing is in hand. */\n | { op: \"restart\" };\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": ";;;;AAOA,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,YAAY,WAAW,cAAc,UAAU,qBAAqB;AAC7F,OAAOA,SAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,qBAAqB;;;ACN9B,OAAO,SAAS;AAChB,OAAO,QAAQ;AACf,OAAO,UAAU;AA2BV,SAAS,WAAWC,QAAeC,OAAyB,QAAQ,KAAa;AACtF,MAAIA,KAAI,mBAAoB,QAAOA,KAAI;AACvC,MAAI,QAAQ,aAAa,QAAS,QAAO,oCAAoC,GAAG,SAAS,EAAE,QAAQ;AACnG,SAAO,KAAK,KAAKD,QAAO,cAAc;AACxC;AAIO,SAAS,IAAI,MAAc,KAAmB,WAA+C;AAClG,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,MAAM;AACV,QAAI,OAAO;AACX,UAAM,IAAI,IAAI,QAAQ,IAAI;AAC1B,UAAME,UAAS,CAAC,MAAyB;AACvC,UAAI,KAAM;AACV,aAAO;AACP,mBAAa,CAAC;AACd,QAAE,QAAQ;AACV,cAAQ,CAAC;AAAA,IACX;AACA,UAAM,IAAI,WAAW,MAAMA,QAAO,IAAI,GAAG,SAAS;AAClD,MAAE,GAAG,WAAW,MAAM,EAAE,MAAM,GAAG,KAAK,UAAU,GAAG,CAAC;AAAA,CAAI,CAAC;AACzD,MAAE,GAAG,QAAQ,CAAC,MAAM;AAClB,aAAO,EAAE,SAAS,MAAM;AACxB,YAAM,IAAI,IAAI,QAAQ,IAAI;AAC1B,UAAI,IAAI,EAAG;AACX,UAAI;AACF,QAAAA,QAAO,KAAK,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,CAAe;AAAA,MAClD,QAAQ;AACN,QAAAA,QAAO,IAAI;AAAA,MACb;AAAA,IACF,CAAC;AACD,MAAE,GAAG,SAAS,MAAMA,QAAO,IAAI,CAAC;AAChC,MAAE,GAAG,SAAS,MAAMA,QAAO,IAAI,CAAC;AAAA,EAClC,CAAC;AACH;;;ADvDA,IAAM,KAAK,QAAQ,OAAO,OAAO;AAEjC,IAAI,SAAwB;AAC5B,IAAM,CAAC,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC;AAChE,IAAM,MAAM,QAAQ;AAEpB,SAAS,WAAmB;AAC1B,MAAI,IAAI,kBAAmB,QAAOC,MAAK,QAAQ,IAAI,iBAAiB;AACpE,QAAM,OAAOC,IAAG,QAAQ;AACxB,MAAI,QAAQ,aAAa;AACvB,WAAOD,MAAK,KAAK,IAAI,gBAAgBA,MAAK,KAAK,MAAM,WAAW,OAAO,GAAG,aAAa;AACzF,SAAOA,MAAK,KAAK,IAAI,kBAAkBA,MAAK,KAAK,MAAM,UAAU,OAAO,GAAG,aAAa;AAC1F;AACA,SAAS,aAAqB;AAC5B,MAAI,IAAI,mBAAoB,QAAOA,MAAK,QAAQ,IAAI,kBAAkB;AACtE,QAAM,OAAOC,IAAG,QAAQ;AACxB,MAAI,QAAQ,aAAa;AACvB,WAAOD,MAAK,KAAK,IAAI,WAAWA,MAAK,KAAK,MAAM,WAAW,SAAS,GAAG,eAAe,aAAa;AACrG,SAAOA,MAAK,KAAK,IAAI,mBAAmBA,MAAK,KAAK,MAAM,SAAS,GAAG,eAAe,aAAa;AAClG;
|
|
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// (Antigravity's answer) 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 we\n// register. PreToolUse is not one of them: `{}` there is a deny, so an entry an older\n// install left behind asks the person instead (core's AG_STALE_PRETOOL, same bytes).\nif (harness === \"antigravity\") {\n try {\n writeFileSync(\n 1,\n event === \"PreToolUse\"\n ? '{\"decision\":\"ask\",\"reason\":\"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it.\"}\\n'\n : \"{}\\n\",\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 /** `sessionpipe update` installed a new copy: restart onto it once nothing is in hand. */\n | { op: \"restart\" };\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
6
|
"names": ["os", "path", "state", "env", "finish", "path", "os"]
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// sessionpipe 0.6.
|
|
1
|
+
// sessionpipe 0.6.1 · Apache-2.0 · https://sessionpipe.org
|
|
2
2
|
|
|
3
3
|
// src/http-sink.ts
|
|
4
4
|
import { createHash, createHmac } from "node:crypto";
|
|
@@ -471,20 +471,19 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
|
|
|
471
471
|
// ../core/src/adapters/antigravity.ts
|
|
472
472
|
var NAME = "antigravity";
|
|
473
473
|
var HOOK_NAME = "sessionpipe";
|
|
474
|
-
var AG_EVENTS = ["PreInvocation", "PostInvocation", "
|
|
474
|
+
var AG_EVENTS = ["PreInvocation", "PostInvocation", "PostToolUse", "Stop"];
|
|
475
|
+
var AG_STALE_PRETOOL = '{"decision":"ask","reason":"sessionpipe: an outdated PreToolUse hook is still loaded. Restart this conversation (or run sessionpipe install) to drop it."}\n';
|
|
476
|
+
var antigravityAnswer = (event) => event === "PreToolUse" ? AG_STALE_PRETOOL : "{}\n";
|
|
475
477
|
var gemini = (env) => env.GEMINI_CLI_HOME || path4.join(HOME, ".gemini");
|
|
476
478
|
var hooksFile = (env) => path4.join(gemini(env), "config", "hooks.json");
|
|
477
479
|
var projectsDir = (env) => path4.join(gemini(env), "config", "projects");
|
|
478
480
|
var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path4.join(gemini(env), d));
|
|
479
481
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
480
482
|
var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
|
|
481
|
-
function entry(cmd
|
|
483
|
+
function entry(cmd) {
|
|
482
484
|
const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
|
|
483
485
|
const out = { enabled: true };
|
|
484
|
-
for (const e of AG_EVENTS) {
|
|
485
|
-
if (lean && e === "PreToolUse") continue;
|
|
486
|
-
out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
487
|
-
}
|
|
486
|
+
for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
|
|
488
487
|
return out;
|
|
489
488
|
}
|
|
490
489
|
var antigravity = {
|
|
@@ -492,12 +491,12 @@ var antigravity = {
|
|
|
492
491
|
events: AG_EVENTS,
|
|
493
492
|
detect: (env = process.env) => existsSync2(path4.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
|
|
494
493
|
configFiles: (env = process.env) => [hooksFile(env)],
|
|
495
|
-
install(cmd, env = process.env
|
|
494
|
+
install(cmd, env = process.env) {
|
|
496
495
|
const file = hooksFile(env);
|
|
497
496
|
const f = readJsonFile(file);
|
|
498
497
|
if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
|
|
499
498
|
const hooks = f.value;
|
|
500
|
-
const e = entry(cmd
|
|
499
|
+
const e = entry(cmd);
|
|
501
500
|
if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
|
|
502
501
|
hooks[HOOK_NAME] = e;
|
|
503
502
|
mkdirSync2(path4.dirname(file), { recursive: true });
|
|
@@ -526,14 +525,14 @@ var antigravity = {
|
|
|
526
525
|
writeFileSync2(file, formatJson(hooks, f));
|
|
527
526
|
return [{ file, changed: true }];
|
|
528
527
|
},
|
|
529
|
-
installed(cmd, env = process.env
|
|
528
|
+
installed(cmd, env = process.env) {
|
|
530
529
|
const file = hooksFile(env);
|
|
531
530
|
const f = readJsonFile(file);
|
|
532
531
|
const have = f.ok ? f.value[HOOK_NAME] : void 0;
|
|
533
532
|
return [
|
|
534
533
|
{
|
|
535
534
|
file,
|
|
536
|
-
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd
|
|
535
|
+
state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale"
|
|
537
536
|
}
|
|
538
537
|
];
|
|
539
538
|
},
|
|
@@ -545,7 +544,8 @@ var antigravity = {
|
|
|
545
544
|
} catch {
|
|
546
545
|
}
|
|
547
546
|
const id = s.conversationId ?? s.session_id ?? s.sessionId;
|
|
548
|
-
|
|
547
|
+
const stdout = antigravityAnswer(event);
|
|
548
|
+
if (!id) return { session: { id: "" }, events: [], stdout };
|
|
549
549
|
const workspaces = agWorkspaces(s.workspacePaths);
|
|
550
550
|
const session = { id: String(id), cwd: workspaces[0] ?? input.cwd };
|
|
551
551
|
if (typeof s.modelName === "string" && s.modelName !== "auto") session.model = s.modelName;
|
|
@@ -566,7 +566,8 @@ var antigravity = {
|
|
|
566
566
|
if (tool) events.push(ev("tool.started", { tool, input: toolCall?.args }));
|
|
567
567
|
break;
|
|
568
568
|
case "PostToolUse":
|
|
569
|
-
if (tool)
|
|
569
|
+
if (!tool) events.push(ev("session.heartbeat", {}));
|
|
570
|
+
else
|
|
570
571
|
events.push(
|
|
571
572
|
ev("tool.ended", {
|
|
572
573
|
tool,
|
|
@@ -580,9 +581,9 @@ var antigravity = {
|
|
|
580
581
|
events.push(ev("turn.ended", { reason: s.error ? "error" : "stop" }));
|
|
581
582
|
break;
|
|
582
583
|
default:
|
|
583
|
-
return { session, events: [], stdout
|
|
584
|
+
return { session, events: [], stdout };
|
|
584
585
|
}
|
|
585
|
-
const r = { session, events, stdout
|
|
586
|
+
const r = { session, events, stdout, hints: { workspaces } };
|
|
586
587
|
if (typeof s.transcriptPath === "string") r.transcript = s.transcriptPath;
|
|
587
588
|
return r;
|
|
588
589
|
},
|