@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 +5 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +3 -3
- package/dist/state.d.ts +32 -6
- package/dist/state.js +64 -4
- package/hooks/zerocool-claude-hook.mjs +1 -1
- package/package.json +1 -1
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
|
|
20
|
-
if (!
|
|
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
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-
|
|
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-
|
|
8
|
-
* server, ADR-
|
|
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
|
|
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
|
-
/**
|
|
29
|
-
export
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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": {
|