@zeroroot-ai/zerocool-claude 0.6.2 → 0.7.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-run.js CHANGED
@@ -16,9 +16,12 @@ export async function runHook(input, env, deps = { readFile }) {
16
16
  return JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: block } });
17
17
  }
18
18
  case "SessionEnd": {
19
- const live = await readLive(dir, cwd);
20
- if (!live || !input.transcript_path || !input.session_id)
19
+ const read = await readLive(dir, cwd);
20
+ if (!read || !input.transcript_path || !input.session_id)
21
21
  return "";
22
+ if ("refused" in read)
23
+ return JSON.stringify({ systemMessage: `zerocool: session checkpoint skipped: ${read.refused}` });
24
+ const live = read.state;
22
25
  let data;
23
26
  try {
24
27
  data = await deps.readFile(input.transcript_path);
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export * from "./state.js";
2
- export { runHook, type HookInput } from "./hook-run.js";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // SPDX-License-Identifier: Elastic-2.0
2
2
  // Copyright 2026 Zero Root AI
3
- // @zeroroot-ai/zerocool-claude: the Claude Code host adapter (ADR-0008).
3
+ // @zeroroot-ai/zerocool-claude: the Claude Code host adapter (ADR-0158).
4
4
  //
5
5
  // The tools are not here. They come from the Gibson MCP server, which the
6
6
  // bundle's `.mcp.json` names. This package is the bundle around it: the
7
7
  // plugin manifest, the marketplace entry, and two hooks. The only bin is
8
- // the hook.
8
+ // the hook, and it imports `hook-run.ts` itself: nothing reaches the hook
9
+ // through this module.
9
10
  export * from "./state.js";
10
- export { runHook } from "./hook-run.js";
package/dist/state.d.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  * A hook runs as its own process and cannot reach the server's session, so
5
5
  * the server writes one small file per working directory in `~/.zerocool/`:
6
6
  * the ambient knowledge block for SessionStart, and the live-mission
7
- * coordinates for SessionEnd (ADR-0007's consequence, now served by the
8
- * server, ADR-0008).
7
+ * coordinates for SessionEnd (ADR-0157's consequence, now served by the
8
+ * server, ADR-0158).
9
9
  *
10
10
  * This file is the read half of that format, kept here because a hook must
11
11
  * not depend on the server package: the format is the contract, not the
12
- * import. It matches `@zeroroot-ai/gibson-mcp`'s `state.ts` exactly, and the
13
- * key is a hash of the working directory.
12
+ * import. It reads the files that `@zeroroot-ai/gibson-mcp`'s `state.ts`
13
+ * writes, and the key is a hash of the working directory.
14
14
  */
15
15
  export interface LiveState {
16
16
  missionId: string;
@@ -25,5 +25,31 @@ export declare function stateDir(env?: NodeJS.ProcessEnv): string;
25
25
  export declare function keyFor(cwd: string): string;
26
26
  /** The block SessionStart injects. Empty when the server wrote none. */
27
27
  export declare function readAmbient(dir: string, cwd: string): Promise<string>;
28
- /** The live mission SessionEnd checkpoints under. Undefined when there is none. */
29
- export declare function readLive(dir: string, cwd: string): Promise<LiveState | undefined>;
28
+ /** A live state file that passed the checks, or the reason it did not. */
29
+ export type LiveRead = {
30
+ state: LiveState;
31
+ } | {
32
+ refused: string;
33
+ };
34
+ /**
35
+ * The host of an endpoint, which is `host:port` or an `https://` URL. Empty
36
+ * when the endpoint has another shape, for example an `http://` URL.
37
+ */
38
+ export declare function endpointHost(endpoint: string): string;
39
+ /**
40
+ * Why the hook must not upload a transcript under this state, or "" when it
41
+ * may. The hook sends the whole session transcript to `endpoint` with
42
+ * `token`, so the file must be the server's own: owned by this user, readable
43
+ * by nobody else, and naming a TLS endpoint. TLS verification may be off only
44
+ * for a loopback endpoint.
45
+ */
46
+ export declare function liveRefusal(live: LiveState, file: {
47
+ uid: number;
48
+ mode: number;
49
+ }, uid: number | undefined): string;
50
+ /**
51
+ * The live mission SessionEnd checkpoints under. Undefined when there is none
52
+ * or the file does not parse. A file that parses but fails liveRefusal comes
53
+ * back as `refused`, so the hook can say why it uploaded nothing.
54
+ */
55
+ export declare function readLive(dir: string, cwd: string, uid?: number | undefined): Promise<LiveRead | undefined>;
package/dist/state.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: Elastic-2.0
2
2
  // Copyright 2026 Zero Root AI
3
- import { readFile } from "node:fs/promises";
3
+ import { readFile, stat } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { createHash } from "node:crypto";
@@ -19,12 +19,72 @@ export async function readAmbient(dir, cwd) {
19
19
  return "";
20
20
  }
21
21
  }
22
- /** The live mission SessionEnd checkpoints under. Undefined when there is none. */
23
- export async function readLive(dir, cwd) {
22
+ const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
23
+ /**
24
+ * The host of an endpoint, which is `host:port` or an `https://` URL. Empty
25
+ * when the endpoint has another shape, for example an `http://` URL.
26
+ */
27
+ export function endpointHost(endpoint) {
28
+ if (endpoint.includes("://")) {
29
+ let u;
30
+ try {
31
+ u = new URL(endpoint);
32
+ }
33
+ catch {
34
+ return "";
35
+ }
36
+ return u.protocol === "https:" ? u.hostname : "";
37
+ }
38
+ // host:port. The port is read from the text: URL drops a default port.
39
+ if (!/^[^/?#@\s]+:\d{1,5}$/.test(endpoint))
40
+ return "";
24
41
  try {
25
- return JSON.parse(await readFile(join(dir, `live-${keyFor(cwd)}.json`), "utf8"));
42
+ return new URL(`https://${endpoint}`).hostname;
43
+ }
44
+ catch {
45
+ return "";
46
+ }
47
+ }
48
+ /**
49
+ * Why the hook must not upload a transcript under this state, or "" when it
50
+ * may. The hook sends the whole session transcript to `endpoint` with
51
+ * `token`, so the file must be the server's own: owned by this user, readable
52
+ * by nobody else, and naming a TLS endpoint. TLS verification may be off only
53
+ * for a loopback endpoint.
54
+ */
55
+ export function liveRefusal(live, file, uid) {
56
+ if (uid !== undefined && file.uid !== uid)
57
+ return "the live state file is owned by another user";
58
+ if ((file.mode & 0o077) !== 0)
59
+ return "the live state file is readable or writable by other users; its mode must be 0600";
60
+ if (typeof live.endpoint !== "string" || typeof live.token !== "string" || typeof live.insecure !== "boolean") {
61
+ return "the live state file has the wrong shape";
62
+ }
63
+ const host = endpointHost(live.endpoint);
64
+ if (!host)
65
+ return `the endpoint ${JSON.stringify(live.endpoint)} is not host:port or an https URL`;
66
+ if (live.insecure && !LOOPBACK_HOSTS.includes(host))
67
+ return `TLS verification is off for the endpoint ${host}, which is not loopback`;
68
+ return "";
69
+ }
70
+ /**
71
+ * The live mission SessionEnd checkpoints under. Undefined when there is none
72
+ * or the file does not parse. A file that parses but fails liveRefusal comes
73
+ * back as `refused`, so the hook can say why it uploaded nothing.
74
+ */
75
+ export async function readLive(dir, cwd, uid = process.getuid?.()) {
76
+ const path = join(dir, `live-${keyFor(cwd)}.json`);
77
+ let live;
78
+ let file;
79
+ try {
80
+ file = await stat(path);
81
+ live = JSON.parse(await readFile(path, "utf8"));
26
82
  }
27
83
  catch {
28
84
  return undefined;
29
85
  }
86
+ if (typeof live !== "object" || live === null)
87
+ return undefined;
88
+ const refused = liveRefusal(live, file, uid);
89
+ return refused ? { refused } : { state: live };
30
90
  }
@@ -17,7 +17,7 @@
17
17
  // node_modules.
18
18
  import { spawn } from "node:child_process"
19
19
 
20
- const VERSION = "0.6.2" // x-release-please-version
20
+ const VERSION = "0.7.0" // x-release-please-version
21
21
  const PACKAGE = `@zeroroot-ai/zerocool-claude@${VERSION}`
22
22
  const BIN = "zerocool-claude-hook"
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroroot-ai/zerocool-claude",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Claude Code host adapter for Gibson (zerocool): the plugin bundle around the Gibson MCP server, plus SessionStart and SessionEnd hooks. The model stays on your Claude login.",
5
5
  "license": "Elastic-2.0",
6
6
  "repository": {