@zeroroot-ai/zerocool 0.2.2 → 0.3.1

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.
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * zerocool-dispatch — one sandboxed dispatched run, then exit.
4
+ *
5
+ * The third dispatched shape, and the one gibson launches (zerocool-plugins#57,
6
+ * gibson#1596, ADR-0016). `zerocool-agent` and `zerocool-serve` are long-lived
7
+ * hosts that enroll once, keep a host key, and POLL the daemon for work. This is
8
+ * the opposite: gibson runs an untrusted agent by launching an ephemeral setec
9
+ * sandbox per mission run and injecting the run's credentials as environment. So
10
+ * there is no enrollment, no host key, and no poll loop — the task and the grant
11
+ * are already in the environment when the process starts, and the process serves
12
+ * exactly one dispatch and exits so the sandbox is torn down.
13
+ *
14
+ * WHAT REPLACES THE BOOTSTRAP TOKEN. The poll-loop shapes authenticate as a
15
+ * long-lived COMPONENT: a human mints a one-time `GIBSON_BOOTSTRAP_TOKEN`, the
16
+ * first run trades it for a persistent host key, and every later run re-registers
17
+ * with that key (ADR-0045). A sandboxed run has neither. It authenticates with
18
+ * the PER-DISPATCH capability grant gibson mints for this run and injects as
19
+ * `GIBSON_CALLBACK_ENDPOINT` + `GIBSON_CG_JWT`. That grant carries exactly the
20
+ * authority of this dispatch, so this process never reads a bootstrap token and
21
+ * never touches a host key.
22
+ *
23
+ * THE CALLBACKS ARE GRANT-AUTHENTICATED. This process drives `opencode run`
24
+ * headless and passes the per-dispatch grant to the child (see
25
+ * {@link dispatchChildEnv}). The child's mission callbacks — findings, tool
26
+ * calls, knowledge reads — then reach the daemon's harness callback listener as
27
+ * the TASK, on the injected grant, not as some enrolled component (see
28
+ * `knowledge-source.ts`).
29
+ *
30
+ * THE OUTPUT IS A LIVE NDJSON STREAM. Every event opencode prints is forwarded
31
+ * to this process's stdout as it arrives, so the daemon can stream the run to the
32
+ * read-only console (a later slice consumes it). The last line is the terminal
33
+ * result. stdout is therefore a clean event stream — the driver's own logs go to
34
+ * stderr — and a clean exit lets the sandbox tear down.
35
+ *
36
+ * Environment (injected by the sandbox launch):
37
+ *
38
+ * These names are gibson's canonical dispatch contract — the constants in gibson
39
+ * `internal/engine/harness/sandboxed/agent.go` (envAgent*). This process conforms
40
+ * to them; it does not define its own.
41
+ *
42
+ * GIBSON_CALLBACK_ENDPOINT per-dispatch harness callback dial target (required)
43
+ * GIBSON_CG_JWT per-dispatch capability grant (CG-JWT) (required)
44
+ * GIBSON_AGENT_TASK_B64 base64 protojson gibson.types.v1.Task (required)
45
+ * GIBSON_MODEL provider/model resolved for the tenant at dispatch
46
+ * GIBSON_CALLBACK_INSECURE "1" to dial the callback listener without TLS (kind)
47
+ * GIBSON_MISSION_ID / GIBSON_MISSION_RUN_ID / GIBSON_AGENT_RUN_ID provenance
48
+ * ZEROCOOL_WORKSPACE working directory (defaults to the cwd)
49
+ * GIBSON_OPENCODE_SESSION_ID continue an earlier opencode session
50
+ * GIBSON_TIMEOUT_MS hard deadline for the run, in ms
51
+ *
52
+ * The child `opencode run` still receives the grant as GIBSON_CALLBACK_TOKEN via
53
+ * {@link dispatchChildEnv} — that is the opencode plugin's own contract, fed from
54
+ * the GIBSON_CG_JWT this process reads.
55
+ */
56
+ import type { AgentOutcome } from "@zeroroot-ai/sdk";
57
+ import { runOpencode } from "./opencode-run.js";
58
+ /**
59
+ * The per-dispatch grant, as the sandbox injects it. The child authenticates its
60
+ * harness callbacks with this; {@link dispatchChildEnv} renders it back to env.
61
+ */
62
+ export interface CallbackGrant {
63
+ /** `ExecuteRequest.callback_endpoint` — a bare `host:port` or an http(s) URL. */
64
+ callbackEndpoint: string;
65
+ /** `ExecuteRequest.callback_token` — the task-scoped capability grant. */
66
+ callbackToken: string;
67
+ /** Dial the callback listener without TLS. Only for a local or kind daemon. */
68
+ insecure?: boolean;
69
+ /** Provenance the platform correlates the run by. Empty when the caller set none. */
70
+ missionRunId?: string;
71
+ agentRunId?: string;
72
+ traceId?: string;
73
+ }
74
+ /** Everything one sandboxed dispatch needs, read from the environment. */
75
+ export interface DispatchContext extends CallbackGrant {
76
+ /** `task.goal` — the natural-language objective the sandbox handed over. */
77
+ goal: string;
78
+ /** opencode edits files here; it is the run's workspace. */
79
+ workspace: string;
80
+ /** `provider/model`, e.g. `gibson/default`. Omitted lets opencode choose. */
81
+ model?: string;
82
+ /** Continue an earlier opencode session instead of starting a new one. */
83
+ sessionId?: string;
84
+ /** Hard deadline for the run, in ms. `0` when none was set. */
85
+ timeoutMs: number;
86
+ }
87
+ /**
88
+ * Render the per-dispatch grant back to environment for the opencode child, so
89
+ * the child's harness callbacks travel on the task grant rather than on any
90
+ * component identity. Empty fields are omitted rather than set blank.
91
+ *
92
+ * Shared with `serve-agent.ts`, which builds the same passthrough from a polled
93
+ * work item — one definition so the two dispatched shapes hand the child the
94
+ * same environment.
95
+ */
96
+ export declare function dispatchChildEnv(g: CallbackGrant): NodeJS.ProcessEnv;
97
+ /**
98
+ * Decode the base64 protojson `gibson.types.v1.Task` the sandbox launch injects
99
+ * as `GIBSON_AGENT_TASK_B64` (gibson agent.go `envAgentTaskB64`) and return its
100
+ * goal. gibson sends the full typed Task, not a plain goal string, so decode it
101
+ * here rather than run opencode against an empty or malformed prompt.
102
+ */
103
+ export declare function decodeTaskGoal(taskB64: string | undefined): string;
104
+ /**
105
+ * Read one sandboxed dispatch's context from the environment.
106
+ *
107
+ * Deliberately never reads `GIBSON_BOOTSTRAP_TOKEN` or a host key: a sandboxed
108
+ * run is authenticated by its per-dispatch grant alone, and reaching for a
109
+ * bootstrap token here would reintroduce the enrollment handshake this shape
110
+ * exists to avoid.
111
+ *
112
+ * The grant and the goal are required. A missing piece throws with the reason,
113
+ * because a sandbox that launched this process without them is a defect in the
114
+ * launch, not something to guess around by running opencode with no task or by
115
+ * making unauthenticated calls.
116
+ */
117
+ export declare function readDispatchContext(env: NodeJS.ProcessEnv, opts?: {
118
+ cwd?: string;
119
+ }): DispatchContext;
120
+ /** Injectable seams, so a test drives a dispatch without spawning opencode. */
121
+ export interface DispatchDeps {
122
+ /** Overridable opencode driver. */
123
+ run?: typeof runOpencode;
124
+ /** Live tap for each NDJSON event opencode prints. */
125
+ onEvent?: (line: string) => void;
126
+ }
127
+ /**
128
+ * Run one dispatched Task headless and map it to a terminal outcome.
129
+ *
130
+ * The opencode child inherits the per-dispatch grant, so its findings, tool
131
+ * calls and knowledge reads authenticate as the task. Events stream live through
132
+ * `deps.onEvent`. A crashed or timed-out run throws — {@link main} turns that
133
+ * into a failed terminal result and a non-zero exit.
134
+ */
135
+ export declare function runDispatch(ctx: DispatchContext, deps?: DispatchDeps): Promise<AgentOutcome>;
136
+ /**
137
+ * Render a terminal outcome as the last NDJSON line on stdout.
138
+ *
139
+ * `type: "result"` marks it apart from the streamed opencode events on the same
140
+ * stream, so the sandbox launcher reads the run's result from the one line that
141
+ * carries it.
142
+ */
143
+ export declare function formatTerminalResult(outcome: AgentOutcome): string;
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ import { runOpencode } from "./opencode-run.js";
3
+ /**
4
+ * Render the per-dispatch grant back to environment for the opencode child, so
5
+ * the child's harness callbacks travel on the task grant rather than on any
6
+ * component identity. Empty fields are omitted rather than set blank.
7
+ *
8
+ * Shared with `serve-agent.ts`, which builds the same passthrough from a polled
9
+ * work item — one definition so the two dispatched shapes hand the child the
10
+ * same environment.
11
+ */
12
+ export function dispatchChildEnv(g) {
13
+ return {
14
+ ...(g.callbackEndpoint ? { GIBSON_CALLBACK_ENDPOINT: g.callbackEndpoint } : {}),
15
+ ...(g.callbackToken ? { GIBSON_CALLBACK_TOKEN: g.callbackToken } : {}),
16
+ ...(g.insecure ? { GIBSON_CALLBACK_INSECURE: "1" } : {}),
17
+ ...(g.missionRunId ? { GIBSON_MISSION_RUN_ID: g.missionRunId } : {}),
18
+ ...(g.agentRunId ? { GIBSON_AGENT_RUN_ID: g.agentRunId } : {}),
19
+ ...(g.traceId ? { GIBSON_TRACE_ID: g.traceId } : {}),
20
+ };
21
+ }
22
+ /**
23
+ * Decode the base64 protojson `gibson.types.v1.Task` the sandbox launch injects
24
+ * as `GIBSON_AGENT_TASK_B64` (gibson agent.go `envAgentTaskB64`) and return its
25
+ * goal. gibson sends the full typed Task, not a plain goal string, so decode it
26
+ * here rather than run opencode against an empty or malformed prompt.
27
+ */
28
+ export function decodeTaskGoal(taskB64) {
29
+ if (!taskB64) {
30
+ throw new Error("GIBSON_AGENT_TASK_B64 is not set. The sandbox launch injects the task to pursue as the " +
31
+ "base64 protojson of a gibson.types.v1.Task; without it there is nothing to run.");
32
+ }
33
+ let task;
34
+ try {
35
+ task = JSON.parse(Buffer.from(taskB64, "base64").toString("utf8"));
36
+ }
37
+ catch (e) {
38
+ throw new Error("GIBSON_AGENT_TASK_B64 is not valid base64 protojson of a gibson.types.v1.Task: " +
39
+ (e instanceof Error ? e.message : String(e)));
40
+ }
41
+ const goal = typeof task.goal === "string" ? task.goal.trim() : "";
42
+ if (!goal) {
43
+ throw new Error("GIBSON_AGENT_TASK_B64 decoded to a Task with no goal. Running opencode against an empty " +
44
+ "prompt would report whatever it says back as a mission result.");
45
+ }
46
+ return goal;
47
+ }
48
+ /**
49
+ * Read one sandboxed dispatch's context from the environment.
50
+ *
51
+ * Deliberately never reads `GIBSON_BOOTSTRAP_TOKEN` or a host key: a sandboxed
52
+ * run is authenticated by its per-dispatch grant alone, and reaching for a
53
+ * bootstrap token here would reintroduce the enrollment handshake this shape
54
+ * exists to avoid.
55
+ *
56
+ * The grant and the goal are required. A missing piece throws with the reason,
57
+ * because a sandbox that launched this process without them is a defect in the
58
+ * launch, not something to guess around by running opencode with no task or by
59
+ * making unauthenticated calls.
60
+ */
61
+ export function readDispatchContext(env, opts = {}) {
62
+ const endpoint = env.GIBSON_CALLBACK_ENDPOINT;
63
+ const token = env.GIBSON_CG_JWT;
64
+ if (!endpoint) {
65
+ throw new Error("GIBSON_CALLBACK_ENDPOINT is not set. A sandboxed dispatched run is authenticated by " +
66
+ "the per-dispatch grant the sandbox launch injects, not by a bootstrap token — the " +
67
+ "launch must supply the callback endpoint and the grant.");
68
+ }
69
+ if (!token) {
70
+ throw new Error("GIBSON_CALLBACK_ENDPOINT is set but GIBSON_CG_JWT is not. The callbacks would have no " +
71
+ "grant to authenticate with; a sandboxed run must not fall back to any other identity, " +
72
+ "so this fails instead.");
73
+ }
74
+ const goal = decodeTaskGoal(env.GIBSON_AGENT_TASK_B64);
75
+ const timeoutRaw = env.GIBSON_TIMEOUT_MS ? Number(env.GIBSON_TIMEOUT_MS) : 0;
76
+ return {
77
+ callbackEndpoint: endpoint,
78
+ callbackToken: token,
79
+ insecure: env.GIBSON_CALLBACK_INSECURE === "1",
80
+ missionRunId: env.GIBSON_MISSION_RUN_ID ?? "",
81
+ agentRunId: env.GIBSON_AGENT_RUN_ID ?? "",
82
+ traceId: env.GIBSON_TRACE_ID ?? "",
83
+ goal,
84
+ workspace: env.ZEROCOOL_WORKSPACE ?? opts.cwd ?? process.cwd(),
85
+ ...(env.GIBSON_MODEL ? { model: env.GIBSON_MODEL } : {}),
86
+ ...(env.GIBSON_OPENCODE_SESSION_ID ? { sessionId: env.GIBSON_OPENCODE_SESSION_ID } : {}),
87
+ timeoutMs: Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 0,
88
+ };
89
+ }
90
+ /**
91
+ * Run one dispatched Task headless and map it to a terminal outcome.
92
+ *
93
+ * The opencode child inherits the per-dispatch grant, so its findings, tool
94
+ * calls and knowledge reads authenticate as the task. Events stream live through
95
+ * `deps.onEvent`. A crashed or timed-out run throws — {@link main} turns that
96
+ * into a failed terminal result and a non-zero exit.
97
+ */
98
+ export async function runDispatch(ctx, deps = {}) {
99
+ const run = deps.run ?? runOpencode;
100
+ const result = await run({
101
+ goal: ctx.goal,
102
+ dir: ctx.workspace,
103
+ ...(ctx.model ? { model: ctx.model } : {}),
104
+ ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
105
+ ...(ctx.timeoutMs > 0 ? { timeoutMs: ctx.timeoutMs } : {}),
106
+ env: dispatchChildEnv(ctx),
107
+ ...(deps.onEvent ? { onEvent: deps.onEvent } : {}),
108
+ });
109
+ return {
110
+ output: result.text,
111
+ success: true,
112
+ metadata: {
113
+ opencode_session_id: result.sessionId,
114
+ finish_reason: result.finishReason,
115
+ tokens_total: String(result.tokens.total),
116
+ tokens_output: String(result.tokens.output),
117
+ cost: String(result.cost),
118
+ },
119
+ };
120
+ }
121
+ /**
122
+ * Render a terminal outcome as the last NDJSON line on stdout.
123
+ *
124
+ * `type: "result"` marks it apart from the streamed opencode events on the same
125
+ * stream, so the sandbox launcher reads the run's result from the one line that
126
+ * carries it.
127
+ */
128
+ export function formatTerminalResult(outcome) {
129
+ return JSON.stringify({
130
+ type: "result",
131
+ success: outcome.success ?? true,
132
+ output: outcome.output ?? "",
133
+ ...(outcome.findingIds ? { finding_ids: outcome.findingIds } : {}),
134
+ metadata: outcome.metadata ?? {},
135
+ });
136
+ }
137
+ async function main() {
138
+ let ctx;
139
+ try {
140
+ ctx = readDispatchContext(process.env);
141
+ }
142
+ catch (e) {
143
+ // A launch defect, not a run failure. Say so on stderr and still emit a
144
+ // terminal result so the launcher never blocks waiting for one.
145
+ const message = e.message;
146
+ console.error(`[zerocool-dispatch] ${message}`);
147
+ process.stdout.write(`${formatTerminalResult({ success: false, output: message, metadata: { error: message } })}\n`);
148
+ process.exit(2);
149
+ return;
150
+ }
151
+ console.error(`[zerocool-dispatch] run mission=${ctx.missionRunId || "-"} agent=${ctx.agentRunId || "-"} ` +
152
+ `workspace=${ctx.workspace}; callbacks use the per-dispatch grant`);
153
+ try {
154
+ const outcome = await runDispatch(ctx, {
155
+ // Forward every opencode event to our stdout, live, for the console stream.
156
+ onEvent: (line) => process.stdout.write(`${line}\n`),
157
+ });
158
+ process.stdout.write(`${formatTerminalResult(outcome)}\n`);
159
+ console.error(`[zerocool-dispatch] done reason=${outcome.metadata?.finish_reason ?? "-"} ` +
160
+ `tokens=${outcome.metadata?.tokens_total ?? "-"}`);
161
+ process.exit(0);
162
+ }
163
+ catch (e) {
164
+ const message = e.message;
165
+ console.error(`[zerocool-dispatch] run failed: ${message}`);
166
+ process.stdout.write(`${formatTerminalResult({ success: false, output: message, metadata: { error: message } })}\n`);
167
+ process.exit(1);
168
+ }
169
+ }
170
+ // Only run when executed as the bin, so the functions above stay importable.
171
+ if (process.argv[1]?.endsWith("dispatch.js")) {
172
+ main().catch((e) => {
173
+ console.error(`[zerocool-dispatch] fatal: ${e.message}`);
174
+ process.exit(1);
175
+ });
176
+ }
@@ -56,7 +56,28 @@ export interface OpencodeRunOptions {
56
56
  env?: NodeJS.ProcessEnv;
57
57
  /** Binary to run. Overridable for tests and for a pinned install. */
58
58
  bin?: string;
59
+ /**
60
+ * Called with each complete NDJSON line as opencode prints it, live, before
61
+ * the run finishes. A dispatched sandbox forwards these to its own stdout so
62
+ * the daemon can stream the run to the read-only console. The same bytes are
63
+ * still buffered and parsed at close, so this is a tap, not a replacement —
64
+ * a slow or throwing sink never changes what the run returns.
65
+ */
66
+ onEvent?: (line: string) => void;
59
67
  }
68
+ /**
69
+ * Split a byte stream into complete lines across chunk boundaries.
70
+ *
71
+ * `spawn`'s `data` events do not align to newlines: one event can carry half a
72
+ * line, or several lines and a fragment. This buffers the fragment and calls
73
+ * `onLine` once per complete, non-empty line, so a live tap sees whole NDJSON
74
+ * events and never half of one. `flush` emits a trailing line with no final
75
+ * newline. Exported so the boundary handling is tested directly.
76
+ */
77
+ export declare function createLineStreamer(onLine: (line: string) => void): {
78
+ push: (chunk: string) => void;
79
+ flush: () => void;
80
+ };
60
81
  /** Build the argv for a headless run. Exported so a test can assert it. */
61
82
  export declare function opencodeArgs(opts: OpencodeRunOptions): string[];
62
83
  /**
@@ -46,6 +46,38 @@ export function parseOpencodeEvents(stdout) {
46
46
  result.text = chunks.join("");
47
47
  return result;
48
48
  }
49
+ /**
50
+ * Split a byte stream into complete lines across chunk boundaries.
51
+ *
52
+ * `spawn`'s `data` events do not align to newlines: one event can carry half a
53
+ * line, or several lines and a fragment. This buffers the fragment and calls
54
+ * `onLine` once per complete, non-empty line, so a live tap sees whole NDJSON
55
+ * events and never half of one. `flush` emits a trailing line with no final
56
+ * newline. Exported so the boundary handling is tested directly.
57
+ */
58
+ export function createLineStreamer(onLine) {
59
+ let pending = "";
60
+ const emit = (raw) => {
61
+ const line = raw.trim();
62
+ if (line)
63
+ onLine(line);
64
+ };
65
+ return {
66
+ push(chunk) {
67
+ pending += chunk;
68
+ let nl;
69
+ while ((nl = pending.indexOf("\n")) >= 0) {
70
+ emit(pending.slice(0, nl));
71
+ pending = pending.slice(nl + 1);
72
+ }
73
+ },
74
+ flush() {
75
+ const rest = pending;
76
+ pending = "";
77
+ emit(rest);
78
+ },
79
+ };
80
+ }
49
81
  /** Build the argv for a headless run. Exported so a test can assert it. */
50
82
  export function opencodeArgs(opts) {
51
83
  const args = ["run", "--format", "json", "--dir", opts.dir];
@@ -81,10 +113,28 @@ export async function runOpencode(opts) {
81
113
  });
82
114
  let stdout = "";
83
115
  let stderr = "";
84
- child.stdout.on("data", (d) => (stdout += d.toString()));
116
+ // A live tap for the daemon console, only when the caller asked for one. The
117
+ // streamer is guarded so a throwing sink cannot fail the run — the buffered
118
+ // parse below is the source of truth for the result.
119
+ const streamer = opts.onEvent
120
+ ? createLineStreamer((line) => {
121
+ try {
122
+ opts.onEvent?.(line);
123
+ }
124
+ catch {
125
+ // A broken console tap must not cost a mission its result.
126
+ }
127
+ })
128
+ : undefined;
129
+ child.stdout.on("data", (d) => {
130
+ const s = d.toString();
131
+ stdout += s;
132
+ streamer?.push(s);
133
+ });
85
134
  child.stderr.on("data", (d) => (stderr += d.toString()));
86
135
  child.on("error", (e) => reject(new Error(`could not run ${bin}: ${e.message}`)));
87
136
  child.on("close", (code, signal) => {
137
+ streamer?.flush();
88
138
  if (signal) {
89
139
  // Killed by the timeout. Say so with the deadline, because "terminated"
90
140
  // alone sends whoever reads the mission looking for a crash.
@@ -28,6 +28,7 @@ import { connectGibson, registerInstance, startAgentWorker, startHeartbeat, } fr
28
28
  import { existsSync } from "node:fs";
29
29
  import { homedir } from "node:os";
30
30
  import { join } from "node:path";
31
+ import { dispatchChildEnv } from "./dispatch.js";
31
32
  import { runOpencode } from "./opencode-run.js";
32
33
  /** Version reported to the daemon on registration. */
33
34
  const VERSION = "0.1.0";
@@ -54,21 +55,18 @@ export async function agentHandler(item, opts) {
54
55
  // pass it back — see the ADR on why this is not yet automatic.
55
56
  sessionId: item.context.opencode_session_id,
56
57
  timeoutMs: item.timeoutMs,
57
- env: {
58
- // The task-scoped callback seam. gibson mints these per dispatch
59
- // (`internal/engine/harness/implementation.go:1205-1210`) so a dispatched
60
- // run reaches the harness as the TASK rather than as this component.
61
- // Passed through to the child now; the plugin consuming them in
62
- // preference to the host-key grant is tracked in the ADR as the next
63
- // slice — today the child still authenticates as the component, which is
64
- // broader authority than the dispatch intends.
65
- ...(item.callbackEndpoint ? { GIBSON_CALLBACK_ENDPOINT: item.callbackEndpoint } : {}),
66
- ...(item.callbackToken ? { GIBSON_CALLBACK_TOKEN: item.callbackToken } : {}),
67
- // Provenance, so anything the child emits can be correlated to this run.
68
- ...(item.missionRunId ? { GIBSON_MISSION_RUN_ID: item.missionRunId } : {}),
69
- ...(item.agentRunId ? { GIBSON_AGENT_RUN_ID: item.agentRunId } : {}),
70
- ...(item.traceId ? { GIBSON_TRACE_ID: item.traceId } : {}),
71
- },
58
+ // The task-scoped callback seam. gibson mints the grant per dispatch
59
+ // (`internal/engine/harness/implementation.go:1205-1210`) so a dispatched run
60
+ // reaches the harness as the TASK rather than as this component. The same
61
+ // passthrough the sandboxed shape uses (`dispatch.ts`), so the child sees one
62
+ // environment however it was launched.
63
+ env: dispatchChildEnv({
64
+ callbackEndpoint: item.callbackEndpoint,
65
+ callbackToken: item.callbackToken,
66
+ missionRunId: item.missionRunId,
67
+ agentRunId: item.agentRunId,
68
+ traceId: item.traceId,
69
+ }),
72
70
  });
73
71
  return {
74
72
  output: run.text,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroroot-ai/zerocool",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "opencode plugin: Gibson platform integration (zerocool). Zero-config LLM, tools, findings, knowledge, delegate/missions.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  },
33
33
  "bin": {
34
34
  "zerocool-serve": "dist/serve.js",
35
- "zerocool-agent": "dist/serve-agent.js"
35
+ "zerocool-agent": "dist/serve-agent.js",
36
+ "zerocool-dispatch": "dist/dispatch.js"
36
37
  },
37
38
  "publishConfig": {
38
39
  "access": "public"