@zeroroot-ai/zerocool-claude 0.1.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/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +8 -0
- package/dist/ambient.d.ts +7 -0
- package/dist/ambient.js +18 -0
- package/dist/hook.d.ts +23 -0
- package/dist/hook.js +72 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/mode.d.ts +28 -0
- package/dist/mode.js +21 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +87 -0
- package/dist/session.d.ts +28 -0
- package/dist/session.js +107 -0
- package/dist/state.d.ts +26 -0
- package/dist/state.js +37 -0
- package/dist/tools/componentize.d.ts +18 -0
- package/dist/tools/componentize.js +80 -0
- package/dist/tools/delegate.d.ts +4 -0
- package/dist/tools/delegate.js +108 -0
- package/dist/tools/findings.d.ts +18 -0
- package/dist/tools/findings.js +69 -0
- package/dist/tools/gibson-tools.d.ts +20 -0
- package/dist/tools/gibson-tools.js +89 -0
- package/dist/tools/memory.d.ts +14 -0
- package/dist/tools/memory.js +96 -0
- package/dist/tools/result.d.ts +5 -0
- package/dist/tools/result.js +8 -0
- package/dist/tools/status.d.ts +5 -0
- package/dist/tools/status.js +17 -0
- package/hooks/hooks.json +30 -0
- package/package.json +45 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
|
+
"name": "zerocool",
|
|
4
|
+
"displayName": "Zerocool (Gibson)",
|
|
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" },
|
|
7
|
+
"homepage": "https://github.com/zeroroot-ai/zerocool-plugins",
|
|
8
|
+
"repository": "https://github.com/zeroroot-ai/zerocool-plugins",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": ["gibson", "security", "knowledge-graph", "mcp"],
|
|
11
|
+
"mcpServers": "./.mcp.json",
|
|
12
|
+
"hooks": "./hooks/hooks.json"
|
|
13
|
+
}
|
package/.mcp.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type KnowledgeSource } from "@zeroroot-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* One GraphRAG lookup per session, injected by the SessionStart hook. A
|
|
4
|
+
* knowledge failure never breaks a start: the agent works without prior
|
|
5
|
+
* context, it just works less well.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ambientBlock(knowledge: KnowledgeSource, seedQuery: string): Promise<string>;
|
package/dist/ambient.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { formatKnowledgeForPrompt } from "@zeroroot-ai/sdk";
|
|
2
|
+
/** How many hits the ambient block carries. Small: it is prompt overhead on every session. */
|
|
3
|
+
const AMBIENT_LIMIT = 5;
|
|
4
|
+
/**
|
|
5
|
+
* One GraphRAG lookup per session, injected by the SessionStart hook. A
|
|
6
|
+
* knowledge failure never breaks a start: the agent works without prior
|
|
7
|
+
* context, it just works less well.
|
|
8
|
+
*/
|
|
9
|
+
export async function ambientBlock(knowledge, seedQuery) {
|
|
10
|
+
try {
|
|
11
|
+
const hits = await knowledge.query({ text: seedQuery, topK: AMBIENT_LIMIT });
|
|
12
|
+
const body = formatKnowledgeForPrompt(hits);
|
|
13
|
+
return body ? `Prior context from the Gibson knowledge graph (recall for more):\n${body}` : "";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export {};
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
}
|
|
53
|
+
async function readStdin() {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for await (const c of process.stdin)
|
|
56
|
+
chunks.push(c);
|
|
57
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
58
|
+
}
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @zeroroot-ai/zerocool-claude: library surface, for tests and for anyone who
|
|
2
|
+
// embeds the server. The bins are server.ts and hook.ts.
|
|
3
|
+
export * from "./mode.js";
|
|
4
|
+
export * from "./session.js";
|
|
5
|
+
export * from "./state.js";
|
|
6
|
+
export * from "./ambient.js";
|
|
7
|
+
export { buildServer } from "./server.js";
|
package/dist/mode.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of three postures this process runs in. Pure: decided from what the
|
|
3
|
+
* environment says and whether the host key exists, so it is testable and the
|
|
4
|
+
* reasons are printed once at start instead of discovered per tool call.
|
|
5
|
+
*
|
|
6
|
+
* - `standalone`: no platform. Findings go to a local log, componentize works.
|
|
7
|
+
* - `component`: checked in, no live mission. Reads and findings run under the
|
|
8
|
+
* component grant. No memory writes: a memory is a World observation and
|
|
9
|
+
* the World is written under a mission (gibson ADR-0012).
|
|
10
|
+
* - `live`: checked in, and this session is a mission (gibson#1593, decision 9).
|
|
11
|
+
*
|
|
12
|
+
* The postures are one path with fewer tools at each step down, the same
|
|
13
|
+
* fail-open shape as the opencode plugin: a coding agent that cannot reach its
|
|
14
|
+
* platform is still a working coding agent.
|
|
15
|
+
*/
|
|
16
|
+
export type Mode = "standalone" | "component" | "live";
|
|
17
|
+
export interface ModeDecision {
|
|
18
|
+
mode: Mode;
|
|
19
|
+
/** One line a person can act on. Empty when nothing is missing. */
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ModeInputs {
|
|
23
|
+
platformURL?: string;
|
|
24
|
+
bootstrapToken?: string;
|
|
25
|
+
hostKeyExists: boolean;
|
|
26
|
+
targetId?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function decideMode(env: ModeInputs, hostKeyPath: string): ModeDecision;
|
package/dist/mode.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function decideMode(env, hostKeyPath) {
|
|
2
|
+
if (!env.platformURL) {
|
|
3
|
+
return { mode: "standalone", reason: "GIBSON_PLATFORM_URL is not set." };
|
|
4
|
+
}
|
|
5
|
+
if (!env.hostKeyExists && !env.bootstrapToken) {
|
|
6
|
+
return {
|
|
7
|
+
mode: "standalone",
|
|
8
|
+
reason: "GIBSON_PLATFORM_URL is set but this host has not checked in. Run `gibson login` then " +
|
|
9
|
+
"`gibson agent enroll`, and start once with GIBSON_BOOTSTRAP_TOKEN=<one-time token>. " +
|
|
10
|
+
`After that the host key at ${hostKeyPath} is enough.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (!env.targetId) {
|
|
14
|
+
return {
|
|
15
|
+
mode: "component",
|
|
16
|
+
reason: "GIBSON_TARGET_ID is not set, so this session cannot be a live mission and cannot " +
|
|
17
|
+
"write memories. Create a target once (`gibson target create`) and set GIBSON_TARGET_ID.",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { mode: "live", reason: "" };
|
|
21
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
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";
|
|
15
|
+
/**
|
|
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).
|
|
22
|
+
*/
|
|
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
|
+
async function main() {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const { server, gibson } = await buildServer(process.env, cwd);
|
|
66
|
+
log(`[zerocool-claude] ${describeGibson(gibson).replaceAll("\n", "; ")}`);
|
|
67
|
+
let closing = false;
|
|
68
|
+
const close = async () => {
|
|
69
|
+
if (closing)
|
|
70
|
+
return;
|
|
71
|
+
closing = true;
|
|
72
|
+
await clearLive(stateDir(process.env), cwd).catch(() => { });
|
|
73
|
+
await gibson.close().catch((e) => log(`[zerocool-claude] close: ${e.message}`));
|
|
74
|
+
};
|
|
75
|
+
// Claude Code ends the session by closing stdio. The mission ends with it.
|
|
76
|
+
process.stdin.on("close", () => void close().finally(() => process.exit(0)));
|
|
77
|
+
for (const sig of ["SIGINT", "SIGTERM"])
|
|
78
|
+
process.on(sig, () => void close().finally(() => process.exit(0)));
|
|
79
|
+
await server.connect(new StdioServerTransport());
|
|
80
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { connectGibson, startLiveMission, type GibsonSession, type KnowledgeSource, type LiveMission } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { type Mode } from "./mode.js";
|
|
3
|
+
/** Everything the tools need, decided once at start. */
|
|
4
|
+
export interface Gibson {
|
|
5
|
+
mode: Mode;
|
|
6
|
+
reason: string;
|
|
7
|
+
agentName: string;
|
|
8
|
+
hostKeyPath: string;
|
|
9
|
+
session?: GibsonSession;
|
|
10
|
+
live?: LiveMission;
|
|
11
|
+
knowledge?: KnowledgeSource;
|
|
12
|
+
/** Release the mission and the component registration. Idempotent. */
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface OpenGibsonOptions {
|
|
16
|
+
env: NodeJS.ProcessEnv;
|
|
17
|
+
log: (line: string) => void;
|
|
18
|
+
/** Test seams. */
|
|
19
|
+
connect?: typeof connectGibson;
|
|
20
|
+
start?: typeof startLiveMission;
|
|
21
|
+
hostKeyExists?: (path: string) => boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_AGENT_NAME = "zerocool-claude";
|
|
24
|
+
/**
|
|
25
|
+
* Check in, then make the session a live mission. Fails open at every step:
|
|
26
|
+
* a platform failure lands one posture down, never in a refusal to start.
|
|
27
|
+
*/
|
|
28
|
+
export declare function openGibson(opts: OpenGibsonOptions): Promise<Gibson>;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { componentKnowledge, connectGibson, startLiveMission, taskKnowledge, } from "@zeroroot-ai/sdk";
|
|
5
|
+
import { decideMode } from "./mode.js";
|
|
6
|
+
export const DEFAULT_AGENT_NAME = "zerocool-claude";
|
|
7
|
+
/**
|
|
8
|
+
* Check in, then make the session a live mission. Fails open at every step:
|
|
9
|
+
* a platform failure lands one posture down, never in a refusal to start.
|
|
10
|
+
*/
|
|
11
|
+
export async function openGibson(opts) {
|
|
12
|
+
const { env, log } = opts;
|
|
13
|
+
const hostKeyPath = env.GIBSON_HOST_KEY_PATH ?? join(homedir(), ".zerocool", "host.key");
|
|
14
|
+
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
|
+
});
|
|
29
|
+
if (decision.mode === "standalone") {
|
|
30
|
+
log(`[zerocool-claude] standalone: ${decision.reason}`);
|
|
31
|
+
return standalone(decision.reason);
|
|
32
|
+
}
|
|
33
|
+
// The bootstrap token is passed ONLY when no host key exists yet. Replaying a
|
|
34
|
+
// one-time token on every start would be rejected (gibson
|
|
35
|
+
// capabilitygrant_register.go:134-155).
|
|
36
|
+
const bootstrapToken = hostKeyExists ? undefined : env.GIBSON_BOOTSTRAP_TOKEN;
|
|
37
|
+
let session;
|
|
38
|
+
try {
|
|
39
|
+
session = await (opts.connect ?? connectGibson)({
|
|
40
|
+
platformURL: env.GIBSON_PLATFORM_URL,
|
|
41
|
+
daemonURL: env.GIBSON_DAEMON_URL,
|
|
42
|
+
bootstrapToken,
|
|
43
|
+
hostKeyPath,
|
|
44
|
+
agentName,
|
|
45
|
+
agentMode: env.GIBSON_AGENT_MODE ?? "interactive",
|
|
46
|
+
agent: { name: agentName, version: "0.0.0", capabilities: ["code"] },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
const reason = `Gibson connect failed: ${e.message}`;
|
|
51
|
+
log(`[zerocool-claude] ${reason}; continuing standalone`);
|
|
52
|
+
return standalone(reason);
|
|
53
|
+
}
|
|
54
|
+
log(`[zerocool-claude] checked in via ${hostKeyExists ? "the registered host key" : "first check-in"} ` +
|
|
55
|
+
`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
|
+
}
|
|
59
|
+
const component = {
|
|
60
|
+
mode: "component",
|
|
61
|
+
reason: decision.reason,
|
|
62
|
+
agentName,
|
|
63
|
+
hostKeyPath,
|
|
64
|
+
session,
|
|
65
|
+
knowledge: componentKnowledge(session.clients.component),
|
|
66
|
+
close: async () => session.stop(),
|
|
67
|
+
};
|
|
68
|
+
if (decision.mode === "component") {
|
|
69
|
+
log(`[zerocool-claude] component posture: ${decision.reason}`);
|
|
70
|
+
return component;
|
|
71
|
+
}
|
|
72
|
+
let live;
|
|
73
|
+
try {
|
|
74
|
+
live = await (opts.start ?? startLiveMission)(session, {
|
|
75
|
+
agentName,
|
|
76
|
+
targetId: env.GIBSON_TARGET_ID,
|
|
77
|
+
harness: { insecure: env.GIBSON_CALLBACK_INSECURE === "1" },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
const reason = `live mission failed: ${e.message}`;
|
|
82
|
+
log(`[zerocool-claude] ${reason}; continuing as a component`);
|
|
83
|
+
return { ...component, reason };
|
|
84
|
+
}
|
|
85
|
+
log(`[zerocool-claude] live mission ${live.missionId} (work ${live.workId}); reads and writes use the task grant`);
|
|
86
|
+
let closed = false;
|
|
87
|
+
return {
|
|
88
|
+
mode: "live",
|
|
89
|
+
reason: "",
|
|
90
|
+
agentName,
|
|
91
|
+
hostKeyPath,
|
|
92
|
+
session,
|
|
93
|
+
live,
|
|
94
|
+
knowledge: taskKnowledge(live.harness),
|
|
95
|
+
close: async () => {
|
|
96
|
+
if (closed)
|
|
97
|
+
return;
|
|
98
|
+
closed = true;
|
|
99
|
+
try {
|
|
100
|
+
await live.end({ output: { ended: "session closed" } });
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
session.stop();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff between the MCP server and the hook processes.
|
|
3
|
+
*
|
|
4
|
+
* Hooks run as separate processes and cannot reach the MCP server's session.
|
|
5
|
+
* The server writes one small file per working directory: the ambient
|
|
6
|
+
* knowledge block for SessionStart, and the live-mission coordinates for
|
|
7
|
+
* SessionEnd. The file is the server's and lives beside the host key. The task
|
|
8
|
+
* grant in it is bounded (30 minutes, renewed by the server), and a hook that
|
|
9
|
+
* finds a stale one fails open.
|
|
10
|
+
*/
|
|
11
|
+
export interface LiveState {
|
|
12
|
+
missionId: string;
|
|
13
|
+
workId: string;
|
|
14
|
+
endpoint: string;
|
|
15
|
+
token: string;
|
|
16
|
+
insecure: boolean;
|
|
17
|
+
/** Unix ms. */
|
|
18
|
+
writtenAt: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function stateDir(env?: NodeJS.ProcessEnv): string;
|
|
21
|
+
export declare function keyFor(cwd: string): string;
|
|
22
|
+
export declare function writeAmbient(dir: string, cwd: string, block: string): Promise<void>;
|
|
23
|
+
export declare function readAmbient(dir: string, cwd: string): Promise<string>;
|
|
24
|
+
export declare function writeLive(dir: string, cwd: string, state: LiveState): Promise<void>;
|
|
25
|
+
export declare function readLive(dir: string, cwd: string): Promise<LiveState | undefined>;
|
|
26
|
+
export declare function clearLive(dir: string, cwd: string): Promise<void>;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
export function stateDir(env = process.env) {
|
|
6
|
+
return env.ZEROCOOL_STATE_DIR ?? join(homedir(), ".zerocool", "claude");
|
|
7
|
+
}
|
|
8
|
+
export function keyFor(cwd) {
|
|
9
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
export async function writeAmbient(dir, cwd, block) {
|
|
12
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
13
|
+
await writeFile(join(dir, `ambient-${keyFor(cwd)}.md`), block, { encoding: "utf8", mode: 0o600 });
|
|
14
|
+
}
|
|
15
|
+
export async function readAmbient(dir, cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return await readFile(join(dir, `ambient-${keyFor(cwd)}.md`), "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeLive(dir, cwd, state) {
|
|
24
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
await writeFile(join(dir, `live-${keyFor(cwd)}.json`), JSON.stringify(state), { encoding: "utf8", mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
export async function readLive(dir, cwd) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await readFile(join(dir, `live-${keyFor(cwd)}.json`), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function clearLive(dir, cwd) {
|
|
36
|
+
await rm(join(dir, `live-${keyFor(cwd)}.json`), { force: true });
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type ComponentSpec, type GibsonSession } from "@zeroroot-ai/sdk";
|
|
3
|
+
type SpecArgs = {
|
|
4
|
+
kind: string;
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
image?: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
capabilities?: string[];
|
|
10
|
+
methods?: string[];
|
|
11
|
+
input_message_type?: string;
|
|
12
|
+
output_message_type?: string;
|
|
13
|
+
config_schema_json?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function toSpec(args: SpecArgs): ComponentSpec;
|
|
16
|
+
export declare function registerComponentizeTool(server: McpServer, cwd: string): void;
|
|
17
|
+
export declare function registerEnrollTool(server: McpServer, session: GibsonSession): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { buildComponentManifest, enrollComponent, enrollmentSupported, validateComponentSpec, } from "@zeroroot-ai/sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { failure, text } from "./result.js";
|
|
6
|
+
/** Componentize and enroll (zerocool-plugins#11): produced artifacts join the fleet. */
|
|
7
|
+
const KINDS = ["agent", "tool", "plugin"];
|
|
8
|
+
const specSchema = {
|
|
9
|
+
kind: z.enum(KINDS).describe("Component kind."),
|
|
10
|
+
name: z.string().describe("Component name, as it will appear in the tenant registry."),
|
|
11
|
+
version: z.string().describe('Semantic version of the artifact, e.g. "0.1.0".'),
|
|
12
|
+
image: z.string().optional().describe("OCI image reference of the built artifact. Required before it can be dispatched."),
|
|
13
|
+
language: z.string().optional().describe("Language the artifact is written in, for the catalog."),
|
|
14
|
+
capabilities: z.array(z.string()).optional().describe('Agent capabilities. Required when kind is "agent".'),
|
|
15
|
+
methods: z.array(z.string()).optional().describe('Plugin method names. Required when kind is "plugin".'),
|
|
16
|
+
input_message_type: z.string().optional().describe('Fully-qualified proto input type. Required when kind is "tool".'),
|
|
17
|
+
output_message_type: z.string().optional().describe('Fully-qualified proto output type. Required when kind is "tool".'),
|
|
18
|
+
config_schema_json: z.string().optional().describe("JSON Schema for a plugin's configuration."),
|
|
19
|
+
};
|
|
20
|
+
export function toSpec(args) {
|
|
21
|
+
const metadata = {};
|
|
22
|
+
if (args.image)
|
|
23
|
+
metadata.image = args.image;
|
|
24
|
+
if (args.language)
|
|
25
|
+
metadata.language = args.language;
|
|
26
|
+
return {
|
|
27
|
+
kind: args.kind,
|
|
28
|
+
name: args.name,
|
|
29
|
+
version: args.version,
|
|
30
|
+
metadata,
|
|
31
|
+
...(args.capabilities ? { capabilities: args.capabilities } : {}),
|
|
32
|
+
...(args.methods ? { methods: args.methods } : {}),
|
|
33
|
+
...(args.input_message_type ? { inputMessageType: args.input_message_type } : {}),
|
|
34
|
+
...(args.output_message_type ? { outputMessageType: args.output_message_type } : {}),
|
|
35
|
+
...(args.config_schema_json ? { configSchemaJson: args.config_schema_json } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function invalid(spec, problems) {
|
|
39
|
+
return failure("invalid component spec", `This artifact does not satisfy Gibson's ${spec.kind} contract:\n${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
40
|
+
}
|
|
41
|
+
export function registerComponentizeTool(server, cwd) {
|
|
42
|
+
server.registerTool("gibson_componentize", {
|
|
43
|
+
description: "Turn a built artifact into a Gibson component manifest and write it to disk. Use this after " +
|
|
44
|
+
"building a tool or agent you want to add to the fleet. Validates the artifact against " +
|
|
45
|
+
"Gibson's component contract. Does not build or push an image.",
|
|
46
|
+
inputSchema: { ...specSchema, path: z.string().optional().describe('Where to write the manifest. Defaults to "gibson-component.json".') },
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
const spec = toSpec(args);
|
|
49
|
+
const problems = validateComponentSpec(spec);
|
|
50
|
+
if (problems.length > 0)
|
|
51
|
+
return invalid(spec, problems);
|
|
52
|
+
const manifest = buildComponentManifest(spec);
|
|
53
|
+
const target = resolve(cwd, args.path ?? "gibson-component.json");
|
|
54
|
+
await mkdir(dirname(target), { recursive: true });
|
|
55
|
+
await writeFile(target, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
56
|
+
const missingImage = spec.metadata?.image
|
|
57
|
+
? ""
|
|
58
|
+
: "\n\nNo image reference was given. Build and push an image, then re-run with image=<oci-ref> before you enroll. A component without an image cannot be dispatched.";
|
|
59
|
+
return text(`${spec.kind} manifest: ${spec.name}`, `Wrote a valid ${spec.kind} manifest for ${spec.name}@${spec.version} to ${target}.${missingImage}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function registerEnrollTool(server, session) {
|
|
63
|
+
server.registerTool("gibson_enroll_component", {
|
|
64
|
+
description: "Register a produced artifact with Gibson so it joins the tenant fleet. Run gibson_componentize first to check the artifact against the component contract.",
|
|
65
|
+
inputSchema: specSchema,
|
|
66
|
+
}, async (args) => {
|
|
67
|
+
const spec = toSpec(args);
|
|
68
|
+
const problems = validateComponentSpec(spec);
|
|
69
|
+
if (problems.length > 0)
|
|
70
|
+
return invalid(spec, problems);
|
|
71
|
+
try {
|
|
72
|
+
const result = await enrollComponent(session.clients.component, spec);
|
|
73
|
+
const { reason } = enrollmentSupported();
|
|
74
|
+
return text(`enrolled ${spec.name}`, `Registered ${spec.kind} ${spec.name}@${spec.version} as instance ${result.instanceId}.\n\nNote: ${reason}. It is visible in the tenant registry, but it drops out when heartbeats stop (every ${Math.round(result.heartbeatIntervalMs / 1000)}s) unless the artifact itself runs and checks in.`);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
return failure("enroll failed", e.message);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type GibsonSession } from "@zeroroot-ai/sdk";
|
|
3
|
+
/** Delegation and missions (zerocool-plugins#10): the network of agents in the tenant. */
|
|
4
|
+
export declare function registerDelegationTools(server: McpServer, session: GibsonSession): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cancelMission, createMission, delegateToAgent, getMissionResults, getMissionStatus, isSeamUnavailable, listAgents, newTask, runMission, waitMission, } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { failure, text } from "./result.js";
|
|
4
|
+
const UNWIRED = "This daemon has no agent delegation or mission management wired (gibson#1186). Complete the work directly instead.";
|
|
5
|
+
/** Delegation and missions (zerocool-plugins#10): the network of agents in the tenant. */
|
|
6
|
+
export function registerDelegationTools(server, session) {
|
|
7
|
+
const component = session.clients.component;
|
|
8
|
+
const seam = (e, what) => (isSeamUnavailable(e) ? text(`${what} unavailable`, UNWIRED) : failure(`${what} failed`, e.message));
|
|
9
|
+
server.registerTool("gibson_list_agents", {
|
|
10
|
+
description: "List the Gibson agents in this tenant that work can be delegated to, with their capabilities and the target types they handle.",
|
|
11
|
+
inputSchema: {},
|
|
12
|
+
}, async () => {
|
|
13
|
+
const { agents, unavailable } = await listAgents(component);
|
|
14
|
+
if (unavailable)
|
|
15
|
+
return text("delegation unavailable", UNWIRED);
|
|
16
|
+
if (agents.length === 0)
|
|
17
|
+
return text("no agents", "No agents are registered for this tenant.");
|
|
18
|
+
const lines = agents.map((a) => {
|
|
19
|
+
const caps = a.capabilities.length > 0 ? `, capabilities: ${a.capabilities.join(", ")}` : "";
|
|
20
|
+
const targets = a.targetTypes.length > 0 ? `, targets: ${a.targetTypes.join(", ")}` : "";
|
|
21
|
+
return `- ${a.name} (${a.version}) ${a.description}${caps}${targets}`;
|
|
22
|
+
});
|
|
23
|
+
return text(`${agents.length} agent${agents.length === 1 ? "" : "s"}`, lines.join("\n"));
|
|
24
|
+
});
|
|
25
|
+
server.registerTool("gibson_delegate", {
|
|
26
|
+
description: "Delegate a sub-task to another Gibson agent and wait for its result. Use gibson_list_agents " +
|
|
27
|
+
"first to pick an agent whose capabilities match the task.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
agent: z.string().describe("Name of the agent to delegate to."),
|
|
30
|
+
goal: z.string().describe("What the delegate should accomplish."),
|
|
31
|
+
context: z.record(z.string(), z.any()).optional().describe("Target details, prior findings, or other context the delegate needs."),
|
|
32
|
+
max_turns: z.number().optional().describe("Cap on the delegate's LLM turns."),
|
|
33
|
+
allowed_tools: z.array(z.string()).optional().describe("Restrict the delegate to these tools."),
|
|
34
|
+
},
|
|
35
|
+
}, async (args) => {
|
|
36
|
+
try {
|
|
37
|
+
const task = newTask(args.goal, {
|
|
38
|
+
...(args.context ? { Context: args.context } : {}),
|
|
39
|
+
...(args.max_turns || args.allowed_tools
|
|
40
|
+
? { Constraints: { ...(args.max_turns ? { MaxTurns: args.max_turns } : {}), ...(args.allowed_tools ? { AllowedTools: args.allowed_tools } : {}) } }
|
|
41
|
+
: {}),
|
|
42
|
+
});
|
|
43
|
+
const result = await delegateToAgent(component, args.agent, task);
|
|
44
|
+
return text(`${args.agent}: ${result.Status}`, JSON.stringify(result, null, 2));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return seam(e, "delegation");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
server.registerTool("gibson_create_mission", {
|
|
51
|
+
description: "Create a Gibson mission from a mission definition, bound to a target. The mission is not started. Call gibson_run_mission to queue it.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
definition: z.record(z.string(), z.any()).describe("Mission definition object (gibson.mission.v1.MissionDefinition as JSON)."),
|
|
54
|
+
target_id: z.string().describe("Identifier of the target the mission runs against."),
|
|
55
|
+
opts: z.record(z.string(), z.any()).optional().describe("Optional mission creation options."),
|
|
56
|
+
},
|
|
57
|
+
}, async (args) => {
|
|
58
|
+
try {
|
|
59
|
+
const info = await createMission(component, args.definition, args.target_id, args.opts);
|
|
60
|
+
return text("mission created", JSON.stringify(info, null, 2));
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return seam(e, "create mission");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
server.registerTool("gibson_run_mission", { description: "Queue a created Gibson mission for execution. Returns as soon as it is queued.", inputSchema: { mission_id: z.string().describe("Mission to run.") } }, async (args) => {
|
|
67
|
+
try {
|
|
68
|
+
await runMission(component, args.mission_id);
|
|
69
|
+
return text("queued", `Mission ${args.mission_id} is queued.`);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return seam(e, "run mission");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
server.registerTool("gibson_mission_status", {
|
|
76
|
+
description: "Check a Gibson mission's status, or block until it finishes. Use wait=true only when you intend to stop and wait for the result.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
mission_id: z.string().describe("Mission to inspect."),
|
|
79
|
+
wait: z.boolean().optional().describe("Block until the mission reaches a terminal state."),
|
|
80
|
+
timeout_ms: z.number().optional().describe("How long to block when wait is true. Defaults to 5 minutes."),
|
|
81
|
+
},
|
|
82
|
+
}, async (args) => {
|
|
83
|
+
try {
|
|
84
|
+
const status = args.wait ? await waitMission(component, args.mission_id, args.timeout_ms) : await getMissionStatus(component, args.mission_id);
|
|
85
|
+
return text(`mission ${args.mission_id}`, JSON.stringify(status, null, 2));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return seam(e, "mission status");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.registerTool("gibson_mission_results", { description: "Read the final results of a completed Gibson mission.", inputSchema: { mission_id: z.string().describe("Mission whose results to read.") } }, async (args) => {
|
|
92
|
+
try {
|
|
93
|
+
return text(`results ${args.mission_id}`, JSON.stringify(await getMissionResults(component, args.mission_id), null, 2));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return seam(e, "mission results");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("gibson_cancel_mission", { description: "Request cancellation of a running Gibson mission.", inputSchema: { mission_id: z.string().describe("Mission to cancel.") } }, async (args) => {
|
|
100
|
+
try {
|
|
101
|
+
await cancelMission(component, args.mission_id);
|
|
102
|
+
return text("cancel requested", `Cancellation of ${args.mission_id} is requested.`);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return seam(e, "cancel mission");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type Finding } from "@zeroroot-ai/sdk";
|
|
3
|
+
import type { Gibson } from "../session.js";
|
|
4
|
+
export interface FindingsBackend {
|
|
5
|
+
submit(f: Finding): Promise<string>;
|
|
6
|
+
describe(): string;
|
|
7
|
+
}
|
|
8
|
+
/** Platform backend: the finding lands in the tenant knowledge graph. */
|
|
9
|
+
export declare function gibsonFindingsBackend(g: Gibson): FindingsBackend;
|
|
10
|
+
/** Standalone backend: an append-only JSONL log. */
|
|
11
|
+
export declare function localFindingsBackend(path: string): FindingsBackend;
|
|
12
|
+
/**
|
|
13
|
+
* Findings: emit what the agent discovers (zerocool-plugins#7). The model
|
|
14
|
+
* calls this when it finds something. Nothing here emits on its own: a file
|
|
15
|
+
* edit is not a security finding, and inventing one fills the tenant graph
|
|
16
|
+
* with noise a person then has to triage.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registerFindingTools(server: McpServer, g: Gibson, backend: FindingsBackend): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { newFinding, submitFinding } from "@zeroroot-ai/sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { failure, text } from "./result.js";
|
|
6
|
+
const SEVERITIES = ["critical", "high", "medium", "low", "info"];
|
|
7
|
+
/** Platform backend: the finding lands in the tenant knowledge graph. */
|
|
8
|
+
export function gibsonFindingsBackend(g) {
|
|
9
|
+
const component = g.session.clients.component;
|
|
10
|
+
return { submit: (f) => submitFinding(component, f), describe: () => "the tenant Gibson graph" };
|
|
11
|
+
}
|
|
12
|
+
/** Standalone backend: an append-only JSONL log. */
|
|
13
|
+
export function localFindingsBackend(path) {
|
|
14
|
+
return {
|
|
15
|
+
submit: async (f) => {
|
|
16
|
+
await mkdir(dirname(path), { recursive: true });
|
|
17
|
+
await appendFile(path, `${JSON.stringify(f)}\n`, "utf8");
|
|
18
|
+
return f.id;
|
|
19
|
+
},
|
|
20
|
+
describe: () => path,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Findings: emit what the agent discovers (zerocool-plugins#7). The model
|
|
25
|
+
* calls this when it finds something. Nothing here emits on its own: a file
|
|
26
|
+
* edit is not a security finding, and inventing one fills the tenant graph
|
|
27
|
+
* with noise a person then has to triage.
|
|
28
|
+
*/
|
|
29
|
+
export function registerFindingTools(server, g, backend) {
|
|
30
|
+
server.registerTool("submit_finding", {
|
|
31
|
+
description: "Record a security finding in the Gibson knowledge graph. Use this when you discover a " +
|
|
32
|
+
"real vulnerability, misconfiguration or notable security fact about the target or " +
|
|
33
|
+
"codebase. Not for progress updates or general observations.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
title: z.string().describe("One-line summary of the finding."),
|
|
36
|
+
description: z.string().describe("What the issue is, where it is, and why it matters."),
|
|
37
|
+
category: z.string().describe('Type of issue, e.g. "injection", "auth", "secrets-exposure", "misconfiguration".'),
|
|
38
|
+
severity: z.enum(SEVERITIES).describe("Impact level of the finding."),
|
|
39
|
+
confidence: z.number().min(0).max(1).optional().describe("How certain you are, from 0 to 1. Defaults to 1."),
|
|
40
|
+
evidence: z.string().optional().describe("A code excerpt, request/response, or log line."),
|
|
41
|
+
remediation: z.string().optional().describe("How to fix or mitigate the issue."),
|
|
42
|
+
target_id: z.string().optional().describe("Identifier of the affected target or component."),
|
|
43
|
+
tags: z.array(z.string()).optional().describe("Labels for filtering."),
|
|
44
|
+
},
|
|
45
|
+
}, async (args) => {
|
|
46
|
+
const finding = newFinding({
|
|
47
|
+
title: args.title,
|
|
48
|
+
description: args.description,
|
|
49
|
+
category: args.category,
|
|
50
|
+
severity: args.severity,
|
|
51
|
+
confidence: args.confidence,
|
|
52
|
+
missionID: g.live?.missionId ?? "",
|
|
53
|
+
agentName: g.agentName,
|
|
54
|
+
...(args.remediation ? { remediation: args.remediation } : {}),
|
|
55
|
+
...(args.target_id ? { targetID: args.target_id } : {}),
|
|
56
|
+
...(args.tags ? { tags: args.tags } : {}),
|
|
57
|
+
...(args.evidence
|
|
58
|
+
? { evidence: [{ type: "text", title: "evidence", content: args.evidence, timestamp: new Date().toISOString() }] }
|
|
59
|
+
: {}),
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
const id = await backend.submit(finding);
|
|
63
|
+
return text(`${args.severity}: ${args.title}`, `Recorded finding ${id} in ${backend.describe()}.`);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return failure("submit_finding failed", e.message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type GibsonSession, type GibsonTool } from "@zeroroot-ai/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* Gibson tools and plugins as MCP tools (zerocool-plugins#9).
|
|
5
|
+
*
|
|
6
|
+
* Discovered tools register individually, one MCP tool per Gibson tool, named
|
|
7
|
+
* `gibson_<tool>`, so the model sees each with its own description. The
|
|
8
|
+
* catalog carries no JSON Schema, only a proto message type name, so a wrapper
|
|
9
|
+
* takes one free-form `input` object and names the expected type. A generic
|
|
10
|
+
* `gibson_call_tool` is always present: discovery is `Unimplemented` on some
|
|
11
|
+
* daemons (gibson#1186), and the operator may know a tool by name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toolKey(name: string): string;
|
|
14
|
+
export declare function registerDiscoveredTool(server: McpServer, session: GibsonSession, t: GibsonTool): void;
|
|
15
|
+
export interface DiscoveryOutcome {
|
|
16
|
+
discovered: number;
|
|
17
|
+
note?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Register the generic tools, then one tool per discovered Gibson tool. */
|
|
20
|
+
export declare function registerGibsonTools(server: McpServer, session: GibsonSession): Promise<DiscoveryOutcome>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { callGibsonTool, listGibsonPlugins, listGibsonTools, queryGibsonPlugin, } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { failure, text } from "./result.js";
|
|
4
|
+
/**
|
|
5
|
+
* Gibson tools and plugins as MCP tools (zerocool-plugins#9).
|
|
6
|
+
*
|
|
7
|
+
* Discovered tools register individually, one MCP tool per Gibson tool, named
|
|
8
|
+
* `gibson_<tool>`, so the model sees each with its own description. The
|
|
9
|
+
* catalog carries no JSON Schema, only a proto message type name, so a wrapper
|
|
10
|
+
* takes one free-form `input` object and names the expected type. A generic
|
|
11
|
+
* `gibson_call_tool` is always present: discovery is `Unimplemented` on some
|
|
12
|
+
* daemons (gibson#1186), and the operator may know a tool by name.
|
|
13
|
+
*/
|
|
14
|
+
export function toolKey(name) {
|
|
15
|
+
return `gibson_${name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
16
|
+
}
|
|
17
|
+
function formatToolResult(name, result) {
|
|
18
|
+
if (result.error) {
|
|
19
|
+
return failure(`${name} failed`, `Gibson tool ${name} failed [${result.error.code}]: ${result.error.message}${result.error.retryable ? " (retryable)" : ""}`);
|
|
20
|
+
}
|
|
21
|
+
const body = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
|
|
22
|
+
return text(name, body ?? "(no output)");
|
|
23
|
+
}
|
|
24
|
+
export function registerDiscoveredTool(server, session, t) {
|
|
25
|
+
const schemaNote = t.inputMessageType ? ` Input must match the Gibson message type ${t.inputMessageType}.` : "";
|
|
26
|
+
server.registerTool(toolKey(t.name), {
|
|
27
|
+
description: `${t.description || `Gibson tool "${t.name}"`}${schemaNote} Runs through the Gibson harness (authorized and metered).`,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
input: z.record(z.string(), z.any()).describe(`Input object for the ${t.name} tool.`),
|
|
30
|
+
timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
|
|
31
|
+
},
|
|
32
|
+
}, async (args) => formatToolResult(t.name, await callGibsonTool(session.clients.component, {
|
|
33
|
+
name: t.name,
|
|
34
|
+
input: args.input,
|
|
35
|
+
...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
|
|
36
|
+
})));
|
|
37
|
+
}
|
|
38
|
+
/** Register the generic tools, then one tool per discovered Gibson tool. */
|
|
39
|
+
export async function registerGibsonTools(server, session) {
|
|
40
|
+
const component = session.clients.component;
|
|
41
|
+
server.registerTool("gibson_call_tool", {
|
|
42
|
+
description: "Invoke a Gibson tool by name through the harness. Use this when you know a tool's name " +
|
|
43
|
+
"but it is not in your tool list. Gibson tool discovery is not available on every daemon.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
name: z.string().describe("Registered Gibson tool name."),
|
|
46
|
+
input: z.record(z.string(), z.any()).describe("Input object matching the tool's input schema."),
|
|
47
|
+
timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
|
|
48
|
+
},
|
|
49
|
+
}, async (args) => formatToolResult(args.name, await callGibsonTool(component, { name: args.name, input: args.input, ...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}) })));
|
|
50
|
+
server.registerTool("gibson_list_tools", {
|
|
51
|
+
description: "List the Gibson tools and plugins available to this tenant, with the input message type each tool expects.",
|
|
52
|
+
inputSchema: {},
|
|
53
|
+
}, async () => {
|
|
54
|
+
const [discovery, plugins] = await Promise.all([listGibsonTools(component), listGibsonPlugins(component).catch(() => [])]);
|
|
55
|
+
const lines = [];
|
|
56
|
+
if (discovery.unavailable)
|
|
57
|
+
lines.push(`Tool discovery is unavailable: ${discovery.unavailable}.`);
|
|
58
|
+
else if (discovery.tools.length === 0)
|
|
59
|
+
lines.push("No Gibson tools are registered for this tenant.");
|
|
60
|
+
else {
|
|
61
|
+
lines.push("Gibson tools:");
|
|
62
|
+
for (const t of discovery.tools) {
|
|
63
|
+
lines.push(`- ${t.name} (${t.version}): ${t.description || "no description"}`);
|
|
64
|
+
if (t.inputMessageType)
|
|
65
|
+
lines.push(` input: ${t.inputMessageType}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (plugins.length > 0) {
|
|
69
|
+
lines.push("", "Gibson plugins:");
|
|
70
|
+
for (const p of plugins)
|
|
71
|
+
lines.push(`- ${p.name} (${p.version}): ${p.description || "no description"}`);
|
|
72
|
+
}
|
|
73
|
+
return text("", lines.join("\n"));
|
|
74
|
+
});
|
|
75
|
+
server.registerTool("gibson_query_plugin", {
|
|
76
|
+
description: "Call a method on a tenant Gibson plugin (a connector or an integration) through the harness.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
plugin: z.string().describe("Plugin name."),
|
|
79
|
+
method: z.string().describe("Method name."),
|
|
80
|
+
params: z.record(z.string(), z.any()).optional().describe("Method parameters."),
|
|
81
|
+
},
|
|
82
|
+
}, async (args) => formatToolResult(`${args.plugin}.${args.method}`, await queryGibsonPlugin(component, { plugin: args.plugin, method: args.method, params: args.params ?? {} })));
|
|
83
|
+
const discovery = await listGibsonTools(component);
|
|
84
|
+
if (discovery.unavailable)
|
|
85
|
+
return { discovered: 0, note: discovery.unavailable };
|
|
86
|
+
for (const t of discovery.tools)
|
|
87
|
+
registerDiscoveredTool(server, session, t);
|
|
88
|
+
return { discovered: discovery.tools.length };
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Gibson } from "../session.js";
|
|
3
|
+
/**
|
|
4
|
+
* Memory and knowledge (gibson#1593, decisions 9 and 10).
|
|
5
|
+
*
|
|
6
|
+
* `remember` is `Observe(MemoryObservation)`: the memory lands in the tenant
|
|
7
|
+
* World as a Timeline event and the projector writes the graph node. It exists
|
|
8
|
+
* only in the live posture, because the World is written under a mission.
|
|
9
|
+
*
|
|
10
|
+
* `recall` reads the graph. In the live posture it reads as the task, in the
|
|
11
|
+
* component posture as the component. The model decides when a lookup is worth
|
|
12
|
+
* a round trip, and the query it writes beats anything derived from the prompt.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerMemoryTools(server: McpServer, g: Gibson): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { formatKnowledgeForPrompt, isSeamUnavailable, remember, WorldEntityKind } from "@zeroroot-ai/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { failure, text } from "./result.js";
|
|
4
|
+
/**
|
|
5
|
+
* Memory and knowledge (gibson#1593, decisions 9 and 10).
|
|
6
|
+
*
|
|
7
|
+
* `remember` is `Observe(MemoryObservation)`: the memory lands in the tenant
|
|
8
|
+
* World as a Timeline event and the projector writes the graph node. It exists
|
|
9
|
+
* only in the live posture, because the World is written under a mission.
|
|
10
|
+
*
|
|
11
|
+
* `recall` reads the graph. In the live posture it reads as the task, in the
|
|
12
|
+
* component posture as the component. The model decides when a lookup is worth
|
|
13
|
+
* a round trip, and the query it writes beats anything derived from the prompt.
|
|
14
|
+
*/
|
|
15
|
+
export function registerMemoryTools(server, g) {
|
|
16
|
+
if (g.knowledge) {
|
|
17
|
+
const knowledge = g.knowledge;
|
|
18
|
+
server.registerTool("recall", {
|
|
19
|
+
description: "Search this tenant's Gibson knowledge graph for memories, prior findings, targets and " +
|
|
20
|
+
"security facts recorded by earlier sessions and runs. Use it before you start on an " +
|
|
21
|
+
"unfamiliar area, or to check whether something was already recorded.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
query: z.string().describe("What to look for, in natural language."),
|
|
24
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum hits. Defaults to 10."),
|
|
25
|
+
node_types: z
|
|
26
|
+
.array(z.string())
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Restrict to node types, e.g. ["Observation"] for memories or ["Finding"].'),
|
|
29
|
+
},
|
|
30
|
+
}, async (args) => {
|
|
31
|
+
let hits;
|
|
32
|
+
try {
|
|
33
|
+
hits = await knowledge.query({
|
|
34
|
+
text: args.query,
|
|
35
|
+
topK: args.limit ?? 10,
|
|
36
|
+
...(args.node_types ? { nodeTypes: args.node_types } : {}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (isSeamUnavailable(e)) {
|
|
41
|
+
return text("recall unavailable", "The Gibson knowledge graph is not available on this daemon (gibson#1186). Continue without prior context.");
|
|
42
|
+
}
|
|
43
|
+
return failure("recall failed", e.message);
|
|
44
|
+
}
|
|
45
|
+
if (hits.length === 0)
|
|
46
|
+
return text("no matches", `Nothing in the tenant graph matches "${args.query}".`);
|
|
47
|
+
return text(`${hits.length} match${hits.length === 1 ? "" : "es"}`, formatKnowledgeForPrompt(hits));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (g.live) {
|
|
51
|
+
const harness = g.live.harness;
|
|
52
|
+
server.registerTool("remember", {
|
|
53
|
+
description: "Keep a fact for later sessions, in this tenant's Gibson knowledge graph. Use it for a " +
|
|
54
|
+
"convention, a decision, a layout fact or a gotcha you would want back next time. Not for " +
|
|
55
|
+
"progress notes. A security finding goes to submit_finding instead.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
text: z.string().describe("The fact, in one or two sentences."),
|
|
58
|
+
kind: z.string().optional().describe('A short category: "convention", "decision", "layout", "gotcha".'),
|
|
59
|
+
tags: z.array(z.string()).optional().describe("Lower-case labels that help recall."),
|
|
60
|
+
source_ref: z.string().optional().describe("Where it came from: a path, a URL, or a work id."),
|
|
61
|
+
},
|
|
62
|
+
}, async (args) => {
|
|
63
|
+
try {
|
|
64
|
+
await remember(harness, { text: args.text, kind: args.kind, tags: args.tags, sourceRef: args.source_ref });
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return failure("remember failed", e.message);
|
|
68
|
+
}
|
|
69
|
+
return text("remembered", `Recorded in the tenant graph under mission ${g.live.missionId}.`);
|
|
70
|
+
});
|
|
71
|
+
server.registerTool("world_view", {
|
|
72
|
+
description: "Read this mission's slice of the tenant World: the hosts, domains, credentials, accounts " +
|
|
73
|
+
"and findings the platform knows about the target. Pass handles from a previous call in " +
|
|
74
|
+
"focus to zoom into entities.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
focus: z.array(z.string()).optional().describe("Entity handles from an earlier world_view call."),
|
|
77
|
+
},
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
try {
|
|
80
|
+
const res = await harness.client.worldView({ context: harness.context, focus: args.focus ?? [] });
|
|
81
|
+
if (res.error)
|
|
82
|
+
return failure("world_view refused", res.error.message);
|
|
83
|
+
if (res.entities.length === 0)
|
|
84
|
+
return text("empty slice", "The World holds nothing for this target yet.");
|
|
85
|
+
const lines = res.entities.map((e) => {
|
|
86
|
+
const attrs = Object.entries(e.attributes).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
87
|
+
return `- [${e.handle}] ${WorldEntityKind[e.kind] ?? e.kind} ${e.label}${attrs ? ` (${attrs})` : ""}`;
|
|
88
|
+
});
|
|
89
|
+
return text(`${res.entities.length} entit${res.entities.length === 1 ? "y" : "ies"}${res.truncated ? " (truncated)" : ""}`, lines.join("\n"));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return failure("world_view failed", e.message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/** One text block. MCP has no title/metadata split, so the title leads the text. */
|
|
3
|
+
export declare function text(title: string, body: string, isError?: boolean): CallToolResult;
|
|
4
|
+
/** Error text for a failure the model can act on. */
|
|
5
|
+
export declare function failure(title: string, body: string): CallToolResult;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** One text block. MCP has no title/metadata split, so the title leads the text. */
|
|
2
|
+
export function text(title, body, isError = false) {
|
|
3
|
+
return { content: [{ type: "text", text: title ? `${title}\n\n${body}` : body }], ...(isError ? { isError } : {}) };
|
|
4
|
+
}
|
|
5
|
+
/** Error text for a failure the model can act on. */
|
|
6
|
+
export function failure(title, body) {
|
|
7
|
+
return text(title, body, true);
|
|
8
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Gibson } from "../session.js";
|
|
3
|
+
/** One tool that says what posture this session runs in and why. */
|
|
4
|
+
export declare function registerStatusTool(server: McpServer, g: Gibson): void;
|
|
5
|
+
export declare function describeGibson(g: Gibson): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { text } from "./result.js";
|
|
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)));
|
|
5
|
+
}
|
|
6
|
+
export function describeGibson(g) {
|
|
7
|
+
const lines = [`posture: ${g.mode}`, `agent: ${g.agentName}`];
|
|
8
|
+
if (g.session)
|
|
9
|
+
lines.push(`component_scope: ${g.session.componentScope}`, `instance: ${g.session.instance.current()}`);
|
|
10
|
+
if (g.live) {
|
|
11
|
+
const exp = g.live.harness.expiresAt();
|
|
12
|
+
lines.push(`mission: ${g.live.missionId}`, `work: ${g.live.workId}`, `grant expires: ${exp ? new Date(exp).toISOString() : "unknown"}`);
|
|
13
|
+
}
|
|
14
|
+
if (g.reason)
|
|
15
|
+
lines.push(`note: ${g.reason}`);
|
|
16
|
+
return lines.join("\n");
|
|
17
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": ["--yes", "--package", "@zeroroot-ai/zerocool-claude@latest", "zerocool-claude-hook"],
|
|
11
|
+
"timeout": 30,
|
|
12
|
+
"statusMessage": "Loading Gibson context"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"SessionEnd": [
|
|
18
|
+
{
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["--yes", "--package", "@zeroroot-ai/zerocool-claude@latest", "zerocool-claude-hook"],
|
|
24
|
+
"timeout": 60
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeroroot-ai/zerocool-claude",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zeroroot-ai/zerocool-plugins.git",
|
|
9
|
+
"directory": "packages/claude-gibson"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/zeroroot-ai/zerocool-plugins",
|
|
12
|
+
"bugs": "https://github.com/zeroroot-ai/zerocool-plugins/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
".claude-plugin",
|
|
19
|
+
".mcp.json",
|
|
20
|
+
"hooks"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\""
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
29
|
+
"@zeroroot-ai/sdk": "^0.6.1",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"zerocool-claude-mcp": "dist/server.js",
|
|
34
|
+
"zerocool-claude-hook": "dist/hook.js"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|