@zeroroot-ai/zerocool-claude 0.1.0 → 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.
@@ -3,11 +3,17 @@
3
3
  "name": "zerocool",
4
4
  "displayName": "Zerocool (Gibson)",
5
5
  "description": "Gibson platform integration for Claude Code. The session is a live mission. Memory and knowledge in the tenant graph, findings, Gibson tools, delegation. The model stays on your Claude subscription.",
6
- "author": { "name": "ZeroRoot", "url": "https://zeroroot.ai" },
6
+ "author": {
7
+ "name": "ZeroRoot",
8
+ "url": "https://zeroroot.ai"
9
+ },
7
10
  "homepage": "https://github.com/zeroroot-ai/zerocool-plugins",
8
11
  "repository": "https://github.com/zeroroot-ai/zerocool-plugins",
9
12
  "license": "MIT",
10
- "keywords": ["gibson", "security", "knowledge-graph", "mcp"],
11
- "mcpServers": "./.mcp.json",
12
- "hooks": "./hooks/hooks.json"
13
+ "keywords": [
14
+ "gibson",
15
+ "security",
16
+ "knowledge-graph",
17
+ "mcp"
18
+ ]
13
19
  }
@@ -0,0 +1,30 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type CliOptions } from "./cli.js";
3
+ import { type Gibson, type OpenGibsonOptions } from "./session.js";
4
+ /**
5
+ * @zeroroot-ai/zerocool-claude: the MCP server Claude Code starts for a session.
6
+ *
7
+ * The model stays on the user's Claude subscription. This server adds Gibson:
8
+ * one live mission per session, memory and knowledge, findings, tools,
9
+ * delegation. It never routes LLM traffic and never reads an Anthropic key
10
+ * (ADR-0007).
11
+ *
12
+ * The connection is made in the session, not before it. `gibson_login` starts
13
+ * the CLI device flow and returns the URL and code. `gibson_connect` enrolls
14
+ * through the CLI session, trusts the private CA, checks in, picks the
15
+ * target, starts the live mission and swaps the tool set live. The settings
16
+ * it learns are persisted, so later sessions connect on their own.
17
+ */
18
+ export declare const log: (line: string) => void;
19
+ export interface BuildDeps {
20
+ open?: Pick<OpenGibsonOptions, "connect" | "start" | "hostKeyExists" | "trust">;
21
+ cli?: CliOptions;
22
+ }
23
+ export interface Surface {
24
+ server: McpServer;
25
+ /** The current connection. Changes on gibson_connect. */
26
+ current(): Gibson;
27
+ /** Replace the connection and re-register the posture's tools. */
28
+ upgrade(next: Gibson): Promise<void>;
29
+ }
30
+ export declare function buildServer(env: NodeJS.ProcessEnv, cwd: string, deps?: BuildDeps): Promise<Surface>;
package/dist/build.js ADDED
@@ -0,0 +1,191 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { z } from "zod";
6
+ import { ambientBlock } from "./ambient.js";
7
+ import { createTarget, enrollIdentity, listTargets, startLogin } from "./cli.js";
8
+ import { loadSettings, writeConfig } from "./config.js";
9
+ import { openGibson } from "./session.js";
10
+ import { stateDir, writeAmbient, writeLive } from "./state.js";
11
+ import { registerComponentizeTool, registerEnrollTool } from "./tools/componentize.js";
12
+ import { registerDelegationTools } from "./tools/delegate.js";
13
+ import { gibsonFindingsBackend, localFindingsBackend, registerFindingTools } from "./tools/findings.js";
14
+ import { registerGibsonTools } from "./tools/gibson-tools.js";
15
+ import { registerMemoryTools } from "./tools/memory.js";
16
+ import { failure, text } from "./tools/result.js";
17
+ import { describeGibson, registerStatusTool } from "./tools/status.js";
18
+ /**
19
+ * @zeroroot-ai/zerocool-claude: the MCP server Claude Code starts for a session.
20
+ *
21
+ * The model stays on the user's Claude subscription. This server adds Gibson:
22
+ * one live mission per session, memory and knowledge, findings, tools,
23
+ * delegation. It never routes LLM traffic and never reads an Anthropic key
24
+ * (ADR-0007).
25
+ *
26
+ * The connection is made in the session, not before it. `gibson_login` starts
27
+ * the CLI device flow and returns the URL and code. `gibson_connect` enrolls
28
+ * through the CLI session, trusts the private CA, checks in, picks the
29
+ * target, starts the live mission and swaps the tool set live. The settings
30
+ * it learns are persisted, so later sessions connect on their own.
31
+ */
32
+ export const log = (line) => {
33
+ process.stderr.write(`${line}\n`);
34
+ };
35
+ /** Snapshot of the tools a posture registered, so an upgrade can drop them. */
36
+ function collect(server, register) {
37
+ const before = new Set(Object.keys(toolTable(server)));
38
+ return Promise.resolve(register()).then(() => {
39
+ const after = toolTable(server);
40
+ return Object.keys(after)
41
+ .filter((n) => !before.has(n))
42
+ .map((n) => after[n]);
43
+ });
44
+ }
45
+ function toolTable(server) {
46
+ return server._registeredTools;
47
+ }
48
+ export async function buildServer(env, cwd, deps = {}) {
49
+ const settings = await loadSettings(env);
50
+ let gibson = await openGibson({ settings, log, ...deps.open });
51
+ const server = new McpServer({ name: "zerocool", version: "0.0.0" });
52
+ let posture = [];
53
+ const surface = {
54
+ server,
55
+ current: () => gibson,
56
+ upgrade: async (next) => {
57
+ for (const t of posture)
58
+ t.remove();
59
+ posture = await collect(server, () => registerPosture(server, next, env, cwd));
60
+ gibson = next;
61
+ },
62
+ };
63
+ registerStatusTool(server, () => gibson);
64
+ registerComponentizeTool(server, cwd);
65
+ registerConnectTools(server, surface, env, deps);
66
+ posture = await collect(server, () => registerPosture(server, gibson, env, cwd));
67
+ return surface;
68
+ }
69
+ /** The tools a posture carries, plus the handoff files for the hooks. */
70
+ async function registerPosture(server, gibson, env, cwd) {
71
+ if (!gibson.session) {
72
+ registerFindingTools(server, gibson, localFindingsBackend(env.ZEROCOOL_FINDINGS_LOG ?? join(homedir(), ".zerocool", "findings.jsonl")));
73
+ return;
74
+ }
75
+ registerFindingTools(server, gibson, gibsonFindingsBackend(gibson));
76
+ registerMemoryTools(server, gibson);
77
+ registerEnrollTool(server, gibson.session);
78
+ registerDelegationTools(server, gibson.session);
79
+ try {
80
+ const discovery = await registerGibsonTools(server, gibson.session);
81
+ log(discovery.note ? `[zerocool-claude] Gibson tool discovery: ${discovery.note}` : `[zerocool-claude] ${discovery.discovered} Gibson tool(s) registered`);
82
+ }
83
+ catch (e) {
84
+ // Discovery is a convenience. The generic gibson_call_tool is registered
85
+ // before discovery runs, so a failure here costs the per-tool wrappers only.
86
+ log(`[zerocool-claude] Gibson tool discovery failed: ${e.message}`);
87
+ }
88
+ const dir = stateDir(env);
89
+ if (gibson.knowledge) {
90
+ const block = await ambientBlock(gibson.knowledge, env.ZEROCOOL_AMBIENT_QUERY ?? "memories, prior findings and security facts for this codebase");
91
+ if (block)
92
+ await writeAmbient(dir, cwd, block).catch((e) => log(`[zerocool-claude] ambient handoff failed: ${e.message}`));
93
+ }
94
+ if (gibson.live) {
95
+ const live = gibson.live;
96
+ await writeLive(dir, cwd, {
97
+ missionId: live.missionId,
98
+ workId: live.workId,
99
+ endpoint: live.harness.endpoint,
100
+ token: live.harness.token(),
101
+ insecure: gibson.settings.callbackInsecure,
102
+ writtenAt: Date.now(),
103
+ }).catch((e) => log(`[zerocool-claude] live handoff failed: ${e.message}`));
104
+ }
105
+ }
106
+ /** gibson_login and gibson_connect: the in-session path to a platform. */
107
+ function registerConnectTools(server, surface, env, deps) {
108
+ const cli = (s) => ({ ...deps.cli, env, gibsonURL: s.platformURL, tenant: s.tenant });
109
+ server.registerTool("gibson_login", {
110
+ description: "Sign in to a Gibson platform as the person running this session. Starts the gibson CLI device " +
111
+ "flow and returns a URL and a code. The person opens the URL, confirms the code, and then calls " +
112
+ "gibson_connect. Needed once per host, and again when the login session expires.",
113
+ inputSchema: {
114
+ platform_url: z.string().optional().describe("Platform URL, e.g. https://api.example.com. Defaults to the last one used."),
115
+ },
116
+ }, async (args) => {
117
+ const s = surface.current().settings;
118
+ try {
119
+ const p = await startLogin({ ...deps.cli, env, gibsonURL: args.platform_url ?? s.platformURL });
120
+ return text("sign in", `Open ${p.url} and confirm the code ${p.code}. The CLI waits for the approval. When it is approved, call gibson_connect.`);
121
+ }
122
+ catch (e) {
123
+ return failure("gibson_login failed", e.message);
124
+ }
125
+ });
126
+ server.registerTool("gibson_connect", {
127
+ description: "Connect this session to a Gibson platform without a restart: enroll this host through the gibson " +
128
+ "CLI login session if needed, check in, pick the target, start the live mission, and add the " +
129
+ "platform tools (remember, recall, world_view, Gibson tools, delegation). The settings are kept, " +
130
+ "so later sessions connect on their own. Call gibson_status afterwards.",
131
+ inputSchema: {
132
+ platform_url: z.string().optional().describe("Platform URL. Defaults to the gibson CLI login session or the saved config."),
133
+ target_id: z.string().optional().describe("Target id for the live mission. Defaults to the only target in the tenant."),
134
+ create_target: z.string().optional().describe("Create a target with this name when none exists, and use it."),
135
+ bootstrap_token: z.string().optional().describe("One-time enrollment token. Only when the CLI cannot mint one."),
136
+ ca_cert_path: z.string().optional().describe("Path to a private CA to trust. Defaults to the CLI session's CA."),
137
+ callback_insecure: z.boolean().optional().describe("Dial the callback endpoint without TLS. Local daemons only."),
138
+ },
139
+ }, async (args) => {
140
+ const base = await loadSettings(env);
141
+ const s = {
142
+ ...base,
143
+ platformURL: args.platform_url ?? base.platformURL,
144
+ targetId: args.target_id ?? base.targetId,
145
+ caCertPath: args.ca_cert_path ?? base.caCertPath,
146
+ callbackInsecure: args.callback_insecure ?? base.callbackInsecure,
147
+ bootstrapToken: args.bootstrap_token ?? base.bootstrapToken,
148
+ };
149
+ if (!s.platformURL) {
150
+ return failure("no platform", "Pass platform_url, or call gibson_login first so the CLI session names the platform.");
151
+ }
152
+ const hostKeyExists = (deps.open?.hostKeyExists ?? existsSync)(s.hostKeyPath);
153
+ try {
154
+ if (!hostKeyExists && !s.bootstrapToken) {
155
+ s.bootstrapToken = await enrollIdentity(s.agentName, cli(s));
156
+ }
157
+ if (!s.targetId) {
158
+ if (args.create_target) {
159
+ s.targetId = await createTarget(args.create_target, cli(s));
160
+ }
161
+ else {
162
+ const targets = await listTargets(cli(s));
163
+ if (targets.length === 1)
164
+ s.targetId = targets[0].id;
165
+ else if (targets.length === 0) {
166
+ return failure("no target", "The tenant has no target. Call gibson_connect again with create_target set to a name for this workspace.");
167
+ }
168
+ else {
169
+ return failure("choose a target", `The tenant has ${targets.length} targets. Call gibson_connect again with target_id:\n${targets.map((t) => `- ${t.id} ${t.name} (${t.type}, ${t.status})`).join("\n")}`);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ catch (e) {
175
+ return failure("gibson_connect failed", e.message);
176
+ }
177
+ await writeConfig(env, {
178
+ platformURL: s.platformURL,
179
+ targetId: s.targetId,
180
+ ...(s.caCertPath ? { caCertPath: s.caCertPath } : {}),
181
+ ...(s.daemonURL ? { daemonURL: s.daemonURL } : {}),
182
+ callbackInsecure: s.callbackInsecure,
183
+ });
184
+ const previous = surface.current();
185
+ await previous.close().catch(() => { });
186
+ const next = await openGibson({ settings: s, log, ...deps.open });
187
+ await surface.upgrade(next);
188
+ const body = describeGibson(next);
189
+ return next.mode === "live" ? text("connected", body) : failure("connected, but not live", body);
190
+ });
191
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ /**
3
+ * The `gibson` CLI as the session's user. It holds the login session
4
+ * (`gibson login`, a browser device flow), so it is the one process on the
5
+ * host that can mint an enrollment token or create a target. The plugin
6
+ * drives it instead of asking a person to copy tokens between terminals.
7
+ */
8
+ export type Spawn = (cmd: string, args: string[], opts: {
9
+ detached?: boolean;
10
+ env?: NodeJS.ProcessEnv;
11
+ }) => ChildProcess;
12
+ export interface CliOptions {
13
+ bin?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ spawn?: Spawn;
16
+ timeoutMs?: number;
17
+ }
18
+ export interface CliResult {
19
+ code: number;
20
+ stdout: string;
21
+ stderr: string;
22
+ }
23
+ export declare function runGibson(args: string[], opts?: CliOptions): Promise<CliResult>;
24
+ /** True when the CLI's failure reads as "no login session". */
25
+ export declare function needsLogin(r: CliResult): boolean;
26
+ /**
27
+ * `gibson agent enroll`: mint a one-time bootstrap token for a new machine
28
+ * identity, authenticated as the person who ran `gibson login`.
29
+ */
30
+ export declare function enrollIdentity(name: string, opts?: CliOptions & {
31
+ gibsonURL?: string;
32
+ tenant?: string;
33
+ }): Promise<string>;
34
+ export interface Target {
35
+ id: string;
36
+ name: string;
37
+ type: string;
38
+ status: string;
39
+ }
40
+ /** `gibson target list`, a tabwriter table: UUID NAME TYPE STATUS. */
41
+ export declare function parseTargetList(stdout: string): Target[];
42
+ export declare function listTargets(opts?: CliOptions & {
43
+ gibsonURL?: string;
44
+ tenant?: string;
45
+ }): Promise<Target[]>;
46
+ /** `gibson target create`: prints the new target id. */
47
+ export declare function createTarget(name: string, opts?: CliOptions & {
48
+ gibsonURL?: string;
49
+ tenant?: string;
50
+ type?: string;
51
+ }): Promise<string>;
52
+ export interface LoginPrompt {
53
+ url: string;
54
+ code: string;
55
+ }
56
+ /**
57
+ * `gibson login`: a browser device flow. The CLI prints a URL and a code, then
58
+ * waits for the approval. This starts it detached, returns the URL and code
59
+ * as soon as they appear, and leaves the CLI waiting so the session lands in
60
+ * `~/.gibson/auth/credentials` when the person approves.
61
+ */
62
+ export declare function startLogin(opts?: CliOptions & {
63
+ gibsonURL?: string;
64
+ }): Promise<LoginPrompt>;
package/dist/cli.js ADDED
@@ -0,0 +1,130 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ export async function runGibson(args, opts = {}) {
3
+ const spawn = opts.spawn ?? nodeSpawn;
4
+ const child = spawn(opts.bin ?? opts.env?.GIBSON_CLI ?? "gibson", args, { env: opts.env ?? process.env });
5
+ return await new Promise((resolve, reject) => {
6
+ const out = [];
7
+ const err = [];
8
+ const timer = setTimeout(() => {
9
+ child.kill();
10
+ reject(new Error(`gibson ${args[0] ?? ""} ${args[1] ?? ""} timed out after ${opts.timeoutMs ?? 60_000}ms`));
11
+ }, opts.timeoutMs ?? 60_000);
12
+ child.stdout?.on("data", (d) => out.push(d.toString()));
13
+ child.stderr?.on("data", (d) => err.push(d.toString()));
14
+ child.on("error", (e) => {
15
+ clearTimeout(timer);
16
+ reject(new Error(`cannot run the gibson CLI (${e.message}). Install it: see https://github.com/zeroroot-ai/adk`));
17
+ });
18
+ child.on("exit", (code) => {
19
+ clearTimeout(timer);
20
+ resolve({ code: code ?? -1, stdout: out.join(""), stderr: err.join("") });
21
+ });
22
+ });
23
+ }
24
+ /** True when the CLI's failure reads as "no login session". */
25
+ export function needsLogin(r) {
26
+ return /login|credentials|unauthenticated|token expired|not signed in/i.test(r.stderr + r.stdout);
27
+ }
28
+ /**
29
+ * `gibson agent enroll`: mint a one-time bootstrap token for a new machine
30
+ * identity, authenticated as the person who ran `gibson login`.
31
+ */
32
+ export async function enrollIdentity(name, opts = {}) {
33
+ const args = ["agent", "enroll", "--name", name, "--kind", "agent"];
34
+ if (opts.gibsonURL)
35
+ args.push("--gibson-url", opts.gibsonURL);
36
+ if (opts.tenant)
37
+ args.push("--tenant", opts.tenant);
38
+ const r = await runGibson(args, opts);
39
+ const m = /^bootstrap_token:\s*(\S+)/m.exec(r.stdout);
40
+ if (r.code !== 0 || !m) {
41
+ const why = (r.stderr || r.stdout).trim();
42
+ throw new Error(needsLogin(r)
43
+ ? `the gibson CLI has no login session (${why}). Call gibson_login first.`
44
+ : `gibson agent enroll failed (exit ${r.code}): ${why || "no bootstrap_token in the output"}`);
45
+ }
46
+ return m[1];
47
+ }
48
+ /** `gibson target list`, a tabwriter table: UUID NAME TYPE STATUS. */
49
+ export function parseTargetList(stdout) {
50
+ return stdout
51
+ .split("\n")
52
+ .map((l) => l.trim())
53
+ .filter((l) => l && !/^UUID\s+NAME/.test(l))
54
+ .map((l) => l.split(/\s{2,}|\t/))
55
+ .filter((cols) => cols.length >= 2)
56
+ .map(([id, name, type, status]) => ({ id: id, name: name ?? "", type: type ?? "", status: status ?? "" }));
57
+ }
58
+ export async function listTargets(opts = {}) {
59
+ const args = ["target", "list"];
60
+ if (opts.gibsonURL)
61
+ args.push("--gibson-url", opts.gibsonURL);
62
+ if (opts.tenant)
63
+ args.push("--tenant", opts.tenant);
64
+ const r = await runGibson(args, opts);
65
+ if (r.code !== 0) {
66
+ const why = (r.stderr || r.stdout).trim();
67
+ throw new Error(needsLogin(r) ? `the gibson CLI has no login session (${why}). Call gibson_login first.` : `gibson target list failed: ${why}`);
68
+ }
69
+ return parseTargetList(r.stdout);
70
+ }
71
+ /** `gibson target create`: prints the new target id. */
72
+ export async function createTarget(name, opts = {}) {
73
+ const args = ["target", "create", "--name", name, "--type", opts.type ?? "custom"];
74
+ if (opts.gibsonURL)
75
+ args.push("--gibson-url", opts.gibsonURL);
76
+ if (opts.tenant)
77
+ args.push("--tenant", opts.tenant);
78
+ const r = await runGibson(args, opts);
79
+ const id = r.stdout.trim().split("\n").pop()?.trim() ?? "";
80
+ if (r.code !== 0 || !id) {
81
+ const why = (r.stderr || r.stdout).trim();
82
+ throw new Error(needsLogin(r) ? `the gibson CLI has no login session (${why}). Call gibson_login first.` : `gibson target create failed: ${why}`);
83
+ }
84
+ return id;
85
+ }
86
+ /**
87
+ * `gibson login`: a browser device flow. The CLI prints a URL and a code, then
88
+ * waits for the approval. This starts it detached, returns the URL and code
89
+ * as soon as they appear, and leaves the CLI waiting so the session lands in
90
+ * `~/.gibson/auth/credentials` when the person approves.
91
+ */
92
+ export async function startLogin(opts = {}) {
93
+ const spawn = opts.spawn ?? nodeSpawn;
94
+ const args = ["login"];
95
+ if (opts.gibsonURL)
96
+ args.push("--gibson-url", opts.gibsonURL);
97
+ const child = spawn(opts.bin ?? opts.env?.GIBSON_CLI ?? "gibson", args, { detached: true, env: opts.env ?? process.env });
98
+ return await new Promise((resolve, reject) => {
99
+ let buf = "";
100
+ const timer = setTimeout(() => {
101
+ child.kill();
102
+ reject(new Error(`gibson login printed no device code within ${opts.timeoutMs ?? 30_000}ms: ${buf.trim()}`));
103
+ }, opts.timeoutMs ?? 30_000);
104
+ const check = () => {
105
+ const url = /open:\s*\n?\s*(https?:\/\/\S+)/.exec(buf);
106
+ const code = /confirm this code:\s*(\S+)/.exec(buf);
107
+ if (url && code) {
108
+ clearTimeout(timer);
109
+ child.unref();
110
+ resolve({ url: url[1], code: code[1] });
111
+ }
112
+ };
113
+ child.stdout?.on("data", (d) => {
114
+ buf += d.toString();
115
+ check();
116
+ });
117
+ child.stderr?.on("data", (d) => {
118
+ buf += d.toString();
119
+ check();
120
+ });
121
+ child.on("error", (e) => {
122
+ clearTimeout(timer);
123
+ reject(new Error(`cannot run the gibson CLI (${e.message}). Install it: see https://github.com/zeroroot-ai/adk`));
124
+ });
125
+ child.on("exit", (code) => {
126
+ clearTimeout(timer);
127
+ reject(new Error(`gibson login exited with ${code} before printing a device code: ${buf.trim()}`));
128
+ });
129
+ });
130
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Where a session learns how to reach Gibson, in precedence order:
3
+ *
4
+ * 1. the environment (`GIBSON_*`), for people who script it,
5
+ * 2. the plugin config this plugin wrote after a `gibson_connect`,
6
+ * 3. the `gibson` CLI login session (`~/.gibson/auth/credentials`), which
7
+ * already knows the platform URL, the active tenant and the private CA.
8
+ *
9
+ * So a person who ran `gibson login` once needs nothing else: the plugin
10
+ * finds the platform through the same session the CLI uses.
11
+ */
12
+ export interface PluginConfig {
13
+ platformURL?: string;
14
+ daemonURL?: string;
15
+ targetId?: string;
16
+ caCertPath?: string;
17
+ callbackInsecure?: boolean;
18
+ }
19
+ export interface CliCredentials {
20
+ gibsonURL?: string;
21
+ tenant?: string;
22
+ caCertPath?: string;
23
+ }
24
+ export interface Settings {
25
+ platformURL?: string;
26
+ daemonURL?: string;
27
+ targetId?: string;
28
+ caCertPath?: string;
29
+ callbackInsecure: boolean;
30
+ bootstrapToken?: string;
31
+ hostKeyPath: string;
32
+ agentName: string;
33
+ tenant?: string;
34
+ }
35
+ export declare const DEFAULT_AGENT_NAME = "zerocool-claude";
36
+ export declare function configPath(env: NodeJS.ProcessEnv): string;
37
+ export declare function readConfig(env: NodeJS.ProcessEnv): Promise<PluginConfig>;
38
+ /** Merge and persist. Only the keys given change. */
39
+ export declare function writeConfig(env: NodeJS.ProcessEnv, patch: PluginConfig): Promise<PluginConfig>;
40
+ export declare function cliCredentialsPath(env: NodeJS.ProcessEnv): string;
41
+ /** The `gibson` CLI login session, addressing fields only. Tokens are never read. */
42
+ export declare function cliCredentials(env: NodeJS.ProcessEnv): Promise<CliCredentials | undefined>;
43
+ export declare function resolveSettings(env: NodeJS.ProcessEnv, cfg: PluginConfig, creds: CliCredentials | undefined): Settings;
44
+ export declare function loadSettings(env: NodeJS.ProcessEnv): Promise<Settings>;
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { stateDir } from "./state.js";
5
+ export const DEFAULT_AGENT_NAME = "zerocool-claude";
6
+ export function configPath(env) {
7
+ return join(stateDir(env), "config.json");
8
+ }
9
+ export async function readConfig(env) {
10
+ try {
11
+ return JSON.parse(await readFile(configPath(env), "utf8"));
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ /** Merge and persist. Only the keys given change. */
18
+ export async function writeConfig(env, patch) {
19
+ const merged = { ...(await readConfig(env)), ...patch };
20
+ await mkdir(stateDir(env), { recursive: true, mode: 0o700 });
21
+ await writeFile(configPath(env), `${JSON.stringify(merged, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
22
+ return merged;
23
+ }
24
+ export function cliCredentialsPath(env) {
25
+ return env.GIBSON_CLI_CREDENTIALS ?? join(homedir(), ".gibson", "auth", "credentials");
26
+ }
27
+ /** The `gibson` CLI login session, addressing fields only. Tokens are never read. */
28
+ export async function cliCredentials(env) {
29
+ try {
30
+ const raw = JSON.parse(await readFile(cliCredentialsPath(env), "utf8"));
31
+ const str = (k) => (typeof raw[k] === "string" && raw[k] ? raw[k] : undefined);
32
+ return { gibsonURL: str("gibson_url"), tenant: str("active_tenant"), caCertPath: str("ca_cert_path") };
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function resolveSettings(env, cfg, creds) {
39
+ return {
40
+ platformURL: env.GIBSON_PLATFORM_URL ?? cfg.platformURL ?? creds?.gibsonURL,
41
+ daemonURL: env.GIBSON_DAEMON_URL ?? cfg.daemonURL,
42
+ targetId: env.GIBSON_TARGET_ID ?? cfg.targetId,
43
+ caCertPath: env.GIBSON_CA_CERT ?? cfg.caCertPath ?? creds?.caCertPath,
44
+ callbackInsecure: env.GIBSON_CALLBACK_INSECURE === "1" || cfg.callbackInsecure === true,
45
+ bootstrapToken: env.GIBSON_BOOTSTRAP_TOKEN,
46
+ hostKeyPath: env.GIBSON_HOST_KEY_PATH ?? join(homedir(), ".zerocool", "host.key"),
47
+ agentName: env.ZEROCOOL_AGENT_NAME ?? DEFAULT_AGENT_NAME,
48
+ tenant: creds?.tenant,
49
+ };
50
+ }
51
+ export async function loadSettings(env) {
52
+ return resolveSettings(env, await readConfig(env), await cliCredentials(env));
53
+ }
@@ -0,0 +1,21 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * The hook bin. Claude Code runs it as a separate process per event, with the
4
+ * event on stdin. It never talks to the MCP server. It reads what the server
5
+ * handed off through the state directory (state.ts) and fails open: a missing
6
+ * file means no output, and no output blocks nothing.
7
+ *
8
+ * - SessionStart: inject the ambient knowledge block as additionalContext.
9
+ * - SessionEnd: checkpoint the transcript to the daemon session store under
10
+ * the live mission's task grant (zerocool-plugins#13, the session seam).
11
+ */
12
+ export interface HookInput {
13
+ session_id?: string;
14
+ hook_event_name?: string;
15
+ cwd?: string;
16
+ transcript_path?: string;
17
+ reason?: string;
18
+ }
19
+ export declare function runHook(input: HookInput, env: NodeJS.ProcessEnv, deps?: {
20
+ readFile: typeof readFile;
21
+ }): Promise<string>;
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { openTaskHarness } from "@zeroroot-ai/sdk";
3
+ import { readAmbient, readLive, stateDir } from "./state.js";
4
+ /** The session store caps a blob at 8 MiB (gibson callback_session_context.go). */
5
+ const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
6
+ export async function runHook(input, env, deps = { readFile }) {
7
+ const cwd = input.cwd ?? process.cwd();
8
+ const dir = stateDir(env);
9
+ switch (input.hook_event_name) {
10
+ case "SessionStart": {
11
+ const block = await readAmbient(dir, cwd);
12
+ if (!block)
13
+ return "";
14
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: block } });
15
+ }
16
+ case "SessionEnd": {
17
+ const live = await readLive(dir, cwd);
18
+ if (!live || !input.transcript_path || !input.session_id)
19
+ return "";
20
+ let data;
21
+ try {
22
+ data = await deps.readFile(input.transcript_path);
23
+ }
24
+ catch {
25
+ return "";
26
+ }
27
+ if (data.byteLength > MAX_TRANSCRIPT_BYTES)
28
+ data = data.subarray(data.byteLength - MAX_TRANSCRIPT_BYTES);
29
+ const harness = openTaskHarness({ endpoint: live.endpoint, token: live.token, insecure: live.insecure, renew: false });
30
+ try {
31
+ const res = await harness.client.putSessionContext({
32
+ context: harness.context,
33
+ sessionId: input.session_id,
34
+ data: new Uint8Array(data),
35
+ ifMatch: "",
36
+ });
37
+ if (res.error)
38
+ return JSON.stringify({ systemMessage: `zerocool: session checkpoint refused: ${res.error.message}` });
39
+ }
40
+ catch (e) {
41
+ return JSON.stringify({ systemMessage: `zerocool: session checkpoint failed: ${e.message}` });
42
+ }
43
+ finally {
44
+ harness.stop();
45
+ }
46
+ return "";
47
+ }
48
+ default:
49
+ return "";
50
+ }
51
+ }
package/dist/hook.d.ts CHANGED
@@ -1,23 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- /**
4
- * The hook bin. Claude Code runs it as a separate process per event, with the
5
- * event on stdin. It never talks to the MCP server. It reads what the server
6
- * handed off through the state directory (state.ts) and fails open: a missing
7
- * file means no output, and no output blocks nothing.
8
- *
9
- * - SessionStart: inject the ambient knowledge block as additionalContext.
10
- * - SessionEnd: checkpoint the transcript to the daemon session store under
11
- * the live mission's task grant (zerocool-plugins#13, the session seam).
12
- */
13
- interface HookInput {
14
- session_id?: string;
15
- hook_event_name?: string;
16
- cwd?: string;
17
- transcript_path?: string;
18
- reason?: string;
19
- }
20
- export declare function runHook(input: HookInput, env: NodeJS.ProcessEnv, deps?: {
21
- readFile: typeof readFile;
22
- }): Promise<string>;
23
2
  export {};
package/dist/hook.js CHANGED
@@ -1,72 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { readFile } from "node:fs/promises";
3
- import { openTaskHarness } from "@zeroroot-ai/sdk";
4
- import { readAmbient, readLive, stateDir } from "./state.js";
5
- /** The session store caps a blob at 8 MiB (gibson callback_session_context.go). */
6
- const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
7
- export async function runHook(input, env, deps = { readFile }) {
8
- const cwd = input.cwd ?? process.cwd();
9
- const dir = stateDir(env);
10
- switch (input.hook_event_name) {
11
- case "SessionStart": {
12
- const block = await readAmbient(dir, cwd);
13
- if (!block)
14
- return "";
15
- return JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: block } });
16
- }
17
- case "SessionEnd": {
18
- const live = await readLive(dir, cwd);
19
- if (!live || !input.transcript_path || !input.session_id)
20
- return "";
21
- let data;
22
- try {
23
- data = await deps.readFile(input.transcript_path);
24
- }
25
- catch {
26
- return "";
27
- }
28
- if (data.byteLength > MAX_TRANSCRIPT_BYTES)
29
- data = data.subarray(data.byteLength - MAX_TRANSCRIPT_BYTES);
30
- const harness = openTaskHarness({ endpoint: live.endpoint, token: live.token, insecure: live.insecure, renew: false });
31
- try {
32
- const res = await harness.client.putSessionContext({
33
- context: harness.context,
34
- sessionId: input.session_id,
35
- data: new Uint8Array(data),
36
- ifMatch: "",
37
- });
38
- if (res.error)
39
- return JSON.stringify({ systemMessage: `zerocool: session checkpoint refused: ${res.error.message}` });
40
- }
41
- catch (e) {
42
- return JSON.stringify({ systemMessage: `zerocool: session checkpoint failed: ${e.message}` });
43
- }
44
- finally {
45
- harness.stop();
46
- }
47
- return "";
48
- }
49
- default:
50
- return "";
51
- }
52
- }
2
+ import { runHook } from "./hook-run.js";
3
+ /**
4
+ * The hook bin. Runs unconditionally: npm installs it as a symlink named
5
+ * `zerocool-claude-hook`, so an argv[1] guard would never match. Library
6
+ * code lives in hook-run.ts.
7
+ */
53
8
  async function readStdin() {
54
9
  const chunks = [];
55
10
  for await (const c of process.stdin)
56
11
  chunks.push(c);
57
12
  return Buffer.concat(chunks).toString("utf8");
58
13
  }
59
- const invokedDirectly = process.argv[1]?.endsWith("hook.js") ?? false;
60
- if (invokedDirectly) {
61
- readStdin()
62
- .then((raw) => runHook(raw ? JSON.parse(raw) : {}, process.env))
63
- .then((out) => {
64
- if (out)
65
- process.stdout.write(out);
66
- process.exit(0);
67
- })
68
- .catch((e) => {
69
- process.stderr.write(`[zerocool-claude hook] ${e.message}\n`);
70
- process.exit(0); // fail open: a hook error must not block the session
71
- });
72
- }
14
+ readStdin()
15
+ .then((raw) => runHook(raw ? JSON.parse(raw) : {}, process.env))
16
+ .then((out) => {
17
+ if (out)
18
+ process.stdout.write(out);
19
+ process.exit(0);
20
+ })
21
+ .catch((e) => {
22
+ process.stderr.write(`[zerocool-claude hook] ${e.message}\n`);
23
+ process.exit(0); // fail open: a hook error must not block the session
24
+ });
package/dist/index.d.ts CHANGED
@@ -2,4 +2,7 @@ export * from "./mode.js";
2
2
  export * from "./session.js";
3
3
  export * from "./state.js";
4
4
  export * from "./ambient.js";
5
- export { buildServer } from "./server.js";
5
+ export * from "./config.js";
6
+ export * from "./cli.js";
7
+ export { buildServer } from "./build.js";
8
+ export { runHook } from "./hook-run.js";
package/dist/index.js CHANGED
@@ -4,4 +4,7 @@ export * from "./mode.js";
4
4
  export * from "./session.js";
5
5
  export * from "./state.js";
6
6
  export * from "./ambient.js";
7
- export { buildServer } from "./server.js";
7
+ export * from "./config.js";
8
+ export * from "./cli.js";
9
+ export { buildServer } from "./build.js";
10
+ export { runHook } from "./hook-run.js";
package/dist/server.d.ts CHANGED
@@ -1,7 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { type Gibson } from "./session.js";
4
- export declare function buildServer(env: NodeJS.ProcessEnv, cwd: string): Promise<{
5
- server: McpServer;
6
- gibson: Gibson;
7
- }>;
2
+ export {};
package/dist/server.js CHANGED
@@ -1,76 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { ambientBlock } from "./ambient.js";
7
- import { openGibson } from "./session.js";
8
- import { clearLive, stateDir, writeAmbient, writeLive } from "./state.js";
9
- import { registerComponentizeTool, registerEnrollTool } from "./tools/componentize.js";
10
- import { registerDelegationTools } from "./tools/delegate.js";
11
- import { gibsonFindingsBackend, localFindingsBackend, registerFindingTools } from "./tools/findings.js";
12
- import { registerGibsonTools } from "./tools/gibson-tools.js";
13
- import { registerMemoryTools } from "./tools/memory.js";
14
- import { describeGibson, registerStatusTool } from "./tools/status.js";
3
+ import { buildServer, log } from "./build.js";
4
+ import { clearLive, stateDir } from "./state.js";
5
+ import { describeGibson } from "./tools/status.js";
15
6
  /**
16
- * @zeroroot-ai/zerocool-claude: the MCP server Claude Code starts for a session.
17
- *
18
- * The model stays on the user's Claude subscription. This server adds Gibson:
19
- * one live mission per session, memory and knowledge, findings, tools,
20
- * delegation. It never routes LLM traffic and never reads an Anthropic key
21
- * (ADR-0007).
7
+ * The MCP server bin. This file runs main() unconditionally: npm installs
8
+ * bins as symlinks named after the bin (`zerocool-claude-mcp`), so any
9
+ * "am I the entry point" check on argv[1] is false there and the process
10
+ * exits before it serves anything. Library code lives in build.ts.
22
11
  */
23
- const log = (line) => {
24
- process.stderr.write(`${line}\n`);
25
- };
26
- export async function buildServer(env, cwd) {
27
- const gibson = await openGibson({ env, log });
28
- const server = new McpServer({ name: "zerocool", version: "0.0.0" });
29
- registerStatusTool(server, gibson);
30
- registerComponentizeTool(server, cwd);
31
- if (!gibson.session) {
32
- registerFindingTools(server, gibson, localFindingsBackend(env.ZEROCOOL_FINDINGS_LOG ?? join(homedir(), ".zerocool", "findings.jsonl")));
33
- return { server, gibson };
34
- }
35
- registerFindingTools(server, gibson, gibsonFindingsBackend(gibson));
36
- registerMemoryTools(server, gibson);
37
- registerEnrollTool(server, gibson.session);
38
- registerDelegationTools(server, gibson.session);
39
- const discovery = await registerGibsonTools(server, gibson.session);
40
- log(discovery.note ? `[zerocool-claude] Gibson tool discovery: ${discovery.note}` : `[zerocool-claude] ${discovery.discovered} Gibson tool(s) registered`);
41
- // Handoff for the hooks: the ambient block now, the live coordinates while
42
- // the mission runs. Both fail open.
43
- const dir = stateDir(env);
44
- if (gibson.knowledge) {
45
- const block = await ambientBlock(gibson.knowledge, env.ZEROCOOL_AMBIENT_QUERY ?? "memories, prior findings and security facts for this codebase");
46
- if (block)
47
- await writeAmbient(dir, cwd, block).catch((e) => log(`[zerocool-claude] ambient handoff failed: ${e.message}`));
48
- }
49
- if (gibson.live) {
50
- const live = gibson.live;
51
- const write = () => writeLive(dir, cwd, {
52
- missionId: live.missionId,
53
- workId: live.workId,
54
- endpoint: live.harness.endpoint,
55
- token: live.harness.token(),
56
- insecure: env.GIBSON_CALLBACK_INSECURE === "1",
57
- writtenAt: Date.now(),
58
- }).catch((e) => log(`[zerocool-claude] live handoff failed: ${e.message}`));
59
- await write();
60
- }
61
- return { server, gibson };
62
- }
63
12
  async function main() {
64
13
  const cwd = process.cwd();
65
- const { server, gibson } = await buildServer(process.env, cwd);
66
- log(`[zerocool-claude] ${describeGibson(gibson).replaceAll("\n", "; ")}`);
14
+ const surface = await buildServer(process.env, cwd);
15
+ const { server } = surface;
16
+ log(`[zerocool-claude] ${describeGibson(surface.current()).replaceAll("\n", "; ")}`);
67
17
  let closing = false;
68
18
  const close = async () => {
69
19
  if (closing)
70
20
  return;
71
21
  closing = true;
72
22
  await clearLive(stateDir(process.env), cwd).catch(() => { });
73
- await gibson.close().catch((e) => log(`[zerocool-claude] close: ${e.message}`));
23
+ await surface.current().close().catch((e) => log(`[zerocool-claude] close: ${e.message}`));
74
24
  };
75
25
  // Claude Code ends the session by closing stdio. The mission ends with it.
76
26
  process.stdin.on("close", () => void close().finally(() => process.exit(0)));
@@ -78,10 +28,7 @@ async function main() {
78
28
  process.on(sig, () => void close().finally(() => process.exit(0)));
79
29
  await server.connect(new StdioServerTransport());
80
30
  }
81
- const invokedDirectly = process.argv[1]?.endsWith("server.js") ?? false;
82
- if (invokedDirectly) {
83
- main().catch((e) => {
84
- log(`[zerocool-claude] fatal: ${e.message}`);
85
- process.exit(1);
86
- });
87
- }
31
+ main().catch((e) => {
32
+ log(`[zerocool-claude] fatal: ${e.message}`);
33
+ process.exit(1);
34
+ });
package/dist/session.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { connectGibson, startLiveMission, type GibsonSession, type KnowledgeSource, type LiveMission } from "@zeroroot-ai/sdk";
2
+ import type { Settings } from "./config.js";
2
3
  import { type Mode } from "./mode.js";
3
- /** Everything the tools need, decided once at start. */
4
+ export { DEFAULT_AGENT_NAME } from "./config.js";
5
+ /** Everything the tools need, decided once per connect. */
4
6
  export interface Gibson {
5
7
  mode: Mode;
6
8
  reason: string;
7
9
  agentName: string;
8
10
  hostKeyPath: string;
11
+ settings: Settings;
9
12
  session?: GibsonSession;
10
13
  live?: LiveMission;
11
14
  knowledge?: KnowledgeSource;
@@ -13,14 +16,14 @@ export interface Gibson {
13
16
  close(): Promise<void>;
14
17
  }
15
18
  export interface OpenGibsonOptions {
16
- env: NodeJS.ProcessEnv;
19
+ settings: Settings;
17
20
  log: (line: string) => void;
18
21
  /** Test seams. */
19
22
  connect?: typeof connectGibson;
20
23
  start?: typeof startLiveMission;
21
24
  hostKeyExists?: (path: string) => boolean;
25
+ trust?: (path: string) => Promise<void>;
22
26
  }
23
- export declare const DEFAULT_AGENT_NAME = "zerocool-claude";
24
27
  /**
25
28
  * Check in, then make the session a live mission. Fails open at every step:
26
29
  * a platform failure lands one posture down, never in a refusal to start.
package/dist/session.js CHANGED
@@ -1,48 +1,43 @@
1
1
  import { existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
2
  import { componentKnowledge, connectGibson, startLiveMission, taskKnowledge, } from "@zeroroot-ai/sdk";
5
3
  import { decideMode } from "./mode.js";
6
- export const DEFAULT_AGENT_NAME = "zerocool-claude";
4
+ import { trustCA } from "./tls.js";
5
+ export { DEFAULT_AGENT_NAME } from "./config.js";
7
6
  /**
8
7
  * Check in, then make the session a live mission. Fails open at every step:
9
8
  * a platform failure lands one posture down, never in a refusal to start.
10
9
  */
11
10
  export async function openGibson(opts) {
12
- const { env, log } = opts;
13
- const hostKeyPath = env.GIBSON_HOST_KEY_PATH ?? join(homedir(), ".zerocool", "host.key");
11
+ const { settings, log } = opts;
12
+ const { hostKeyPath, agentName } = settings;
14
13
  const hostKeyExists = (opts.hostKeyExists ?? existsSync)(hostKeyPath);
15
- const agentName = env.ZEROCOOL_AGENT_NAME ?? DEFAULT_AGENT_NAME;
16
- const decision = decideMode({
17
- platformURL: env.GIBSON_PLATFORM_URL,
18
- bootstrapToken: env.GIBSON_BOOTSTRAP_TOKEN,
19
- hostKeyExists,
20
- targetId: env.GIBSON_TARGET_ID,
21
- }, hostKeyPath);
22
- const standalone = (reason) => ({
23
- mode: "standalone",
24
- reason,
25
- agentName,
26
- hostKeyPath,
27
- close: async () => { },
28
- });
14
+ const decision = decideMode({ platformURL: settings.platformURL, bootstrapToken: settings.bootstrapToken, hostKeyExists, targetId: settings.targetId }, hostKeyPath);
15
+ const standalone = (reason) => ({ mode: "standalone", reason, agentName, hostKeyPath, settings, close: async () => { } });
29
16
  if (decision.mode === "standalone") {
30
17
  log(`[zerocool-claude] standalone: ${decision.reason}`);
31
18
  return standalone(decision.reason);
32
19
  }
20
+ if (settings.caCertPath) {
21
+ try {
22
+ await (opts.trust ?? trustCA)(settings.caCertPath);
23
+ }
24
+ catch (e) {
25
+ log(`[zerocool-claude] CA at ${settings.caCertPath} not loaded: ${e.message}`);
26
+ }
27
+ }
33
28
  // The bootstrap token is passed ONLY when no host key exists yet. Replaying a
34
29
  // one-time token on every start would be rejected (gibson
35
30
  // capabilitygrant_register.go:134-155).
36
- const bootstrapToken = hostKeyExists ? undefined : env.GIBSON_BOOTSTRAP_TOKEN;
31
+ const bootstrapToken = hostKeyExists ? undefined : settings.bootstrapToken;
37
32
  let session;
38
33
  try {
39
34
  session = await (opts.connect ?? connectGibson)({
40
- platformURL: env.GIBSON_PLATFORM_URL,
41
- daemonURL: env.GIBSON_DAEMON_URL,
35
+ platformURL: settings.platformURL,
36
+ daemonURL: settings.daemonURL,
42
37
  bootstrapToken,
43
38
  hostKeyPath,
44
39
  agentName,
45
- agentMode: env.GIBSON_AGENT_MODE ?? "interactive",
40
+ agentMode: "interactive",
46
41
  agent: { name: agentName, version: "0.0.0", capabilities: ["code"] },
47
42
  });
48
43
  }
@@ -53,14 +48,14 @@ export async function openGibson(opts) {
53
48
  }
54
49
  log(`[zerocool-claude] checked in via ${hostKeyExists ? "the registered host key" : "first check-in"} ` +
55
50
  `as ${agentName} (component_scope=${session.componentScope})`);
56
- if (!hostKeyExists) {
57
- log(`[zerocool-claude] host key written to ${hostKeyPath}; unset GIBSON_BOOTSTRAP_TOKEN, it is spent`);
58
- }
51
+ if (!hostKeyExists)
52
+ log(`[zerocool-claude] host key written to ${hostKeyPath}; the bootstrap token is spent`);
59
53
  const component = {
60
54
  mode: "component",
61
55
  reason: decision.reason,
62
56
  agentName,
63
57
  hostKeyPath,
58
+ settings,
64
59
  session,
65
60
  knowledge: componentKnowledge(session.clients.component),
66
61
  close: async () => session.stop(),
@@ -73,8 +68,8 @@ export async function openGibson(opts) {
73
68
  try {
74
69
  live = await (opts.start ?? startLiveMission)(session, {
75
70
  agentName,
76
- targetId: env.GIBSON_TARGET_ID,
77
- harness: { insecure: env.GIBSON_CALLBACK_INSECURE === "1" },
71
+ targetId: settings.targetId,
72
+ harness: { insecure: settings.callbackInsecure },
78
73
  });
79
74
  }
80
75
  catch (e) {
@@ -89,6 +84,7 @@ export async function openGibson(opts) {
89
84
  reason: "",
90
85
  agentName,
91
86
  hostKeyPath,
87
+ settings,
92
88
  session,
93
89
  live,
94
90
  knowledge: taskKnowledge(live.harness),
package/dist/tls.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function trustCA(path: string): Promise<void>;
package/dist/tls.js ADDED
@@ -0,0 +1,19 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import tls from "node:tls";
3
+ /**
4
+ * Trust a private CA for every TLS client in this process, at runtime.
5
+ *
6
+ * A self-hosted Gibson (kind, on-prem) fronts its edge with its own CA. Node
7
+ * reads NODE_EXTRA_CA_CERTS only at start, and the connect happens mid-session,
8
+ * so the CA is added through `tls.setDefaultCACertificates` (Node 22.15+).
9
+ * Both `fetch` (the Capability Grant register call) and the gRPC transports
10
+ * use the default CA set, so one call covers all of them.
11
+ */
12
+ const trusted = new Set();
13
+ export async function trustCA(path) {
14
+ if (trusted.has(path))
15
+ return;
16
+ const pem = await readFile(path, "utf8");
17
+ tls.setDefaultCACertificates([...tls.getCACertificates("default"), pem]);
18
+ trusted.add(path);
19
+ }
@@ -1,5 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { Gibson } from "../session.js";
3
3
  /** One tool that says what posture this session runs in and why. */
4
- export declare function registerStatusTool(server: McpServer, g: Gibson): void;
4
+ export declare function registerStatusTool(server: McpServer, current: () => Gibson): void;
5
5
  export declare function describeGibson(g: Gibson): string;
@@ -1,7 +1,7 @@
1
1
  import { text } from "./result.js";
2
2
  /** One tool that says what posture this session runs in and why. */
3
- export function registerStatusTool(server, g) {
4
- server.registerTool("gibson_status", { description: "Show how this session is connected to Gibson: standalone, component, or a live mission, and what is missing if anything.", inputSchema: {} }, async () => text("", describeGibson(g)));
3
+ export function registerStatusTool(server, current) {
4
+ server.registerTool("gibson_status", { description: "Show how this session is connected to Gibson: standalone, component, or a live mission, and what is missing if anything.", inputSchema: {} }, async () => text("", describeGibson(current())));
5
5
  }
6
6
  export function describeGibson(g) {
7
7
  const lines = [`posture: ${g.mode}`, `agent: ${g.agentName}`];
@@ -11,6 +11,10 @@ export function describeGibson(g) {
11
11
  const exp = g.live.harness.expiresAt();
12
12
  lines.push(`mission: ${g.live.missionId}`, `work: ${g.live.workId}`, `grant expires: ${exp ? new Date(exp).toISOString() : "unknown"}`);
13
13
  }
14
+ if (g.settings.platformURL)
15
+ lines.push(`platform: ${g.settings.platformURL}`);
16
+ if (g.settings.targetId)
17
+ lines.push(`target: ${g.settings.targetId}`);
14
18
  if (g.reason)
15
19
  lines.push(`note: ${g.reason}`);
16
20
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroroot-ai/zerocool-claude",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Claude Code plugin: Gibson platform integration (zerocool). The session is a live mission. Memory, knowledge, findings, tools, delegation. The model stays on your Claude subscription.",
5
5
  "license": "MIT",
6
6
  "repository": {