@zeroroot-ai/zerocool-claude 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +19 -4
- package/dist/build.js +144 -13
- package/dist/cli.d.ts +64 -0
- package/dist/cli.js +130 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/server.js +4 -3
- package/dist/session.d.ts +6 -3
- package/dist/session.js +24 -28
- package/dist/tls.d.ts +1 -0
- package/dist/tls.js +19 -0
- package/dist/tools/status.d.ts +1 -1
- package/dist/tools/status.js +6 -2
- package/package.json +1 -1
package/dist/build.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type CliOptions } from "./cli.js";
|
|
3
|
+
import { type Gibson, type OpenGibsonOptions } from "./session.js";
|
|
3
4
|
/**
|
|
4
5
|
* @zeroroot-ai/zerocool-claude: the MCP server Claude Code starts for a session.
|
|
5
6
|
*
|
|
@@ -7,9 +8,23 @@ import { type Gibson } from "./session.js";
|
|
|
7
8
|
* one live mission per session, memory and knowledge, findings, tools,
|
|
8
9
|
* delegation. It never routes LLM traffic and never reads an Anthropic key
|
|
9
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.
|
|
10
17
|
*/
|
|
11
18
|
export declare const log: (line: string) => void;
|
|
12
|
-
export
|
|
19
|
+
export interface BuildDeps {
|
|
20
|
+
open?: Pick<OpenGibsonOptions, "connect" | "start" | "hostKeyExists" | "trust">;
|
|
21
|
+
cli?: CliOptions;
|
|
22
|
+
}
|
|
23
|
+
export interface Surface {
|
|
13
24
|
server: McpServer;
|
|
14
|
-
|
|
15
|
-
|
|
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
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
4
6
|
import { ambientBlock } from "./ambient.js";
|
|
7
|
+
import { createTarget, enrollIdentity, listTargets, startLogin } from "./cli.js";
|
|
8
|
+
import { loadSettings, writeConfig } from "./config.js";
|
|
5
9
|
import { openGibson } from "./session.js";
|
|
6
10
|
import { stateDir, writeAmbient, writeLive } from "./state.js";
|
|
7
11
|
import { registerComponentizeTool, registerEnrollTool } from "./tools/componentize.js";
|
|
@@ -9,7 +13,8 @@ import { registerDelegationTools } from "./tools/delegate.js";
|
|
|
9
13
|
import { gibsonFindingsBackend, localFindingsBackend, registerFindingTools } from "./tools/findings.js";
|
|
10
14
|
import { registerGibsonTools } from "./tools/gibson-tools.js";
|
|
11
15
|
import { registerMemoryTools } from "./tools/memory.js";
|
|
12
|
-
import {
|
|
16
|
+
import { failure, text } from "./tools/result.js";
|
|
17
|
+
import { describeGibson, registerStatusTool } from "./tools/status.js";
|
|
13
18
|
/**
|
|
14
19
|
* @zeroroot-ai/zerocool-claude: the MCP server Claude Code starts for a session.
|
|
15
20
|
*
|
|
@@ -17,27 +22,69 @@ import { registerStatusTool } from "./tools/status.js";
|
|
|
17
22
|
* one live mission per session, memory and knowledge, findings, tools,
|
|
18
23
|
* delegation. It never routes LLM traffic and never reads an Anthropic key
|
|
19
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.
|
|
20
31
|
*/
|
|
21
32
|
export const log = (line) => {
|
|
22
33
|
process.stderr.write(`${line}\n`);
|
|
23
34
|
};
|
|
24
|
-
|
|
25
|
-
|
|
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 });
|
|
26
51
|
const server = new McpServer({ name: "zerocool", version: "0.0.0" });
|
|
27
|
-
|
|
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);
|
|
28
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) {
|
|
29
71
|
if (!gibson.session) {
|
|
30
72
|
registerFindingTools(server, gibson, localFindingsBackend(env.ZEROCOOL_FINDINGS_LOG ?? join(homedir(), ".zerocool", "findings.jsonl")));
|
|
31
|
-
return
|
|
73
|
+
return;
|
|
32
74
|
}
|
|
33
75
|
registerFindingTools(server, gibson, gibsonFindingsBackend(gibson));
|
|
34
76
|
registerMemoryTools(server, gibson);
|
|
35
77
|
registerEnrollTool(server, gibson.session);
|
|
36
78
|
registerDelegationTools(server, gibson.session);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
88
|
const dir = stateDir(env);
|
|
42
89
|
if (gibson.knowledge) {
|
|
43
90
|
const block = await ambientBlock(gibson.knowledge, env.ZEROCOOL_AMBIENT_QUERY ?? "memories, prior findings and security facts for this codebase");
|
|
@@ -46,15 +93,99 @@ export async function buildServer(env, cwd) {
|
|
|
46
93
|
}
|
|
47
94
|
if (gibson.live) {
|
|
48
95
|
const live = gibson.live;
|
|
49
|
-
|
|
96
|
+
await writeLive(dir, cwd, {
|
|
50
97
|
missionId: live.missionId,
|
|
51
98
|
workId: live.workId,
|
|
52
99
|
endpoint: live.harness.endpoint,
|
|
53
100
|
token: live.harness.token(),
|
|
54
|
-
insecure:
|
|
101
|
+
insecure: gibson.settings.callbackInsecure,
|
|
55
102
|
writtenAt: Date.now(),
|
|
56
103
|
}).catch((e) => log(`[zerocool-claude] live handoff failed: ${e.message}`));
|
|
57
|
-
await write();
|
|
58
104
|
}
|
|
59
|
-
|
|
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
|
+
});
|
|
60
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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -11,15 +11,16 @@ import { describeGibson } from "./tools/status.js";
|
|
|
11
11
|
*/
|
|
12
12
|
async function main() {
|
|
13
13
|
const cwd = process.cwd();
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const surface = await buildServer(process.env, cwd);
|
|
15
|
+
const { server } = surface;
|
|
16
|
+
log(`[zerocool-claude] ${describeGibson(surface.current()).replaceAll("\n", "; ")}`);
|
|
16
17
|
let closing = false;
|
|
17
18
|
const close = async () => {
|
|
18
19
|
if (closing)
|
|
19
20
|
return;
|
|
20
21
|
closing = true;
|
|
21
22
|
await clearLive(stateDir(process.env), cwd).catch(() => { });
|
|
22
|
-
await
|
|
23
|
+
await surface.current().close().catch((e) => log(`[zerocool-claude] close: ${e.message}`));
|
|
23
24
|
};
|
|
24
25
|
// Claude Code ends the session by closing stdio. The mission ends with it.
|
|
25
26
|
process.stdin.on("close", () => void close().finally(() => process.exit(0)));
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
13
|
-
const hostKeyPath
|
|
11
|
+
const { settings, log } = opts;
|
|
12
|
+
const { hostKeyPath, agentName } = settings;
|
|
14
13
|
const hostKeyExists = (opts.hostKeyExists ?? existsSync)(hostKeyPath);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
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 :
|
|
31
|
+
const bootstrapToken = hostKeyExists ? undefined : settings.bootstrapToken;
|
|
37
32
|
let session;
|
|
38
33
|
try {
|
|
39
34
|
session = await (opts.connect ?? connectGibson)({
|
|
40
|
-
platformURL:
|
|
41
|
-
daemonURL:
|
|
35
|
+
platformURL: settings.platformURL,
|
|
36
|
+
daemonURL: settings.daemonURL,
|
|
42
37
|
bootstrapToken,
|
|
43
38
|
hostKeyPath,
|
|
44
39
|
agentName,
|
|
45
|
-
agentMode:
|
|
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};
|
|
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:
|
|
77
|
-
harness: { insecure:
|
|
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
|
+
}
|
package/dist/tools/status.d.ts
CHANGED
|
@@ -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,
|
|
4
|
+
export declare function registerStatusTool(server: McpServer, current: () => Gibson): void;
|
|
5
5
|
export declare function describeGibson(g: Gibson): string;
|
package/dist/tools/status.js
CHANGED
|
@@ -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,
|
|
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(
|
|
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.
|
|
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": {
|