opencara 0.101.0 → 0.103.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.
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/opencara-mcp.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { env, exit, stderr } from "node:process";
7
+
8
+ // src/mcp/ipc.ts
9
+ import { createServer, createConnection } from "node:net";
10
+ var IPC_SOCKET_ENV = "OPENCARA_MCP_IPC_SOCKET";
11
+ function encode(frame) {
12
+ return JSON.stringify(frame) + "\n";
13
+ }
14
+ function decode(buffered, chunk) {
15
+ const buf = buffered + chunk;
16
+ const lines = buf.split("\n");
17
+ const remainder = lines.pop() ?? "";
18
+ const frames = [];
19
+ const malformed = [];
20
+ for (const line of lines) {
21
+ const t = line.trim();
22
+ if (t.length === 0) continue;
23
+ try {
24
+ const parsed = JSON.parse(t);
25
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && "type" in parsed && (parsed.type === "tool-call" || parsed.type === "tool-result")) {
26
+ frames.push(parsed);
27
+ } else {
28
+ malformed.push(t);
29
+ }
30
+ } catch {
31
+ malformed.push(t);
32
+ }
33
+ }
34
+ return { frames, malformed, buffered: remainder };
35
+ }
36
+ var IpcClient = class {
37
+ constructor(socketPath) {
38
+ this.socketPath = socketPath;
39
+ }
40
+ socketPath;
41
+ socket = null;
42
+ buffered = "";
43
+ nextCallId = 1;
44
+ pending = /* @__PURE__ */ new Map();
45
+ async connect() {
46
+ await new Promise((resolve, reject) => {
47
+ const socket = createConnection({ path: this.socketPath }, () => {
48
+ this.socket = socket;
49
+ socket.setEncoding("utf8");
50
+ socket.on("data", (chunk) => this.onData(chunk));
51
+ socket.on("close", () => this.onClose("socket closed"));
52
+ socket.on("error", (err) => this.onClose(`socket error: ${err.message}`));
53
+ resolve();
54
+ });
55
+ socket.once("error", (err) => {
56
+ if (!this.socket) reject(err);
57
+ });
58
+ });
59
+ }
60
+ /** Mint a callId and round-trip a tool-call. */
61
+ async call(kind, args) {
62
+ if (!this.socket) throw new Error("ipc client not connected");
63
+ const callId = `c${this.nextCallId++}`;
64
+ const frame = { type: "tool-call", callId, kind, args };
65
+ return new Promise((resolve, reject) => {
66
+ this.pending.set(callId, { resolve, reject });
67
+ this.socket.write(encode(frame));
68
+ });
69
+ }
70
+ close() {
71
+ this.onClose("close()");
72
+ if (this.socket) {
73
+ this.socket.end();
74
+ this.socket = null;
75
+ }
76
+ }
77
+ onData(chunk) {
78
+ const d = decode(this.buffered, chunk);
79
+ this.buffered = d.buffered;
80
+ for (const frame of d.frames) {
81
+ if (frame.type === "tool-result") {
82
+ const p = this.pending.get(frame.callId);
83
+ if (p) {
84
+ this.pending.delete(frame.callId);
85
+ p.resolve(frame.result);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ onClose(reason) {
91
+ if (this.pending.size === 0) return;
92
+ const err = new Error(`ipc client disconnected: ${reason}`);
93
+ for (const p of this.pending.values()) p.reject(err);
94
+ this.pending.clear();
95
+ }
96
+ };
97
+
98
+ // src/mcp/tools.ts
99
+ import "zod";
100
+
101
+ // ../shared/dist/events.js
102
+ import { z } from "zod";
103
+ var PlatformSchema = z.enum(["github"]);
104
+ var PlatformEventSchema = z.object({
105
+ id: z.string(),
106
+ platform: PlatformSchema,
107
+ type: z.string(),
108
+ receivedAt: z.string().datetime(),
109
+ payload: z.unknown()
110
+ });
111
+
112
+ // ../shared/dist/agent.js
113
+ import { z as z2 } from "zod";
114
+ var AgentRunStatusSchema = z2.enum([
115
+ "queued",
116
+ "assigned",
117
+ "running",
118
+ "succeeded",
119
+ "failed",
120
+ "cancelled"
121
+ ]);
122
+ var AcpHistoryTurnSchema = z2.object({
123
+ role: z2.enum(["user", "assistant"]),
124
+ text: z2.string()
125
+ });
126
+ var AcpSpecSchema = z2.object({
127
+ systemPromptMd: z2.string(),
128
+ userPromptMd: z2.string(),
129
+ history: z2.array(AcpHistoryTurnSchema).default([]),
130
+ pageContextJson: z2.string().optional()
131
+ });
132
+ var AgentSpecSchema = z2.object({
133
+ kind: z2.string(),
134
+ command: z2.string(),
135
+ args: z2.array(z2.string()).default([]),
136
+ env: z2.record(z2.string()).default({}),
137
+ cwd: z2.string().optional(),
138
+ /**
139
+ * Present iff this run goes through the ACP+MCP path. Mutually exclusive
140
+ * with the legacy stdin-JSON envelope — when set, `JobAssignment.stdinJson`
141
+ * is ignored by the device runner.
142
+ */
143
+ acp: AcpSpecSchema.optional()
144
+ });
145
+ var AgentRunSchema = z2.object({
146
+ id: z2.string(),
147
+ spec: AgentSpecSchema,
148
+ triggerEventId: z2.string().optional(),
149
+ status: AgentRunStatusSchema,
150
+ hostId: z2.string().nullable(),
151
+ createdAt: z2.string().datetime(),
152
+ startedAt: z2.string().datetime().nullable(),
153
+ finishedAt: z2.string().datetime().nullable(),
154
+ exitCode: z2.number().int().nullable()
155
+ });
156
+
157
+ // ../shared/dist/host-protocol.js
158
+ import { z as z3 } from "zod";
159
+ var PairingCreateRequestSchema = z3.object({
160
+ device_secret_hash: z3.string()
161
+ });
162
+ var PairingCreateResponseSchema = z3.object({
163
+ code: z3.string(),
164
+ expires_at: z3.string().datetime()
165
+ });
166
+ var PairingStatusResponseSchema = z3.union([
167
+ z3.object({ status: z3.literal("pending") }),
168
+ z3.object({
169
+ status: z3.literal("confirmed"),
170
+ token: z3.string(),
171
+ agent_host_id: z3.string(),
172
+ device_name: z3.string()
173
+ }),
174
+ z3.object({ status: z3.literal("expired") })
175
+ ]);
176
+ var PairingConfirmRequestSchema = z3.object({
177
+ device_name: z3.string().min(1)
178
+ });
179
+ var SystemInfoSchema = z3.object({
180
+ os: z3.string(),
181
+ // os.platform()
182
+ release: z3.string(),
183
+ // os.release()
184
+ arch: z3.string(),
185
+ // os.arch()
186
+ hostname: z3.string(),
187
+ cpu: z3.object({
188
+ model: z3.string(),
189
+ cores: z3.number().int().nonnegative(),
190
+ speedMhz: z3.number().int().nonnegative()
191
+ }),
192
+ memory: z3.object({
193
+ totalBytes: z3.number().nonnegative(),
194
+ freeBytes: z3.number().nonnegative()
195
+ }),
196
+ disk: z3.object({
197
+ path: z3.string(),
198
+ totalBytes: z3.number().nonnegative(),
199
+ freeBytes: z3.number().nonnegative()
200
+ }).optional(),
201
+ ipAddrs: z3.array(z3.string()).default([]),
202
+ uptimeSec: z3.number().nonnegative()
203
+ });
204
+ var HelloMessageSchema = z3.object({
205
+ type: z3.literal("hello"),
206
+ platform: z3.string(),
207
+ version: z3.string(),
208
+ capabilities: z3.array(z3.string()).default([]),
209
+ systemInfo: SystemInfoSchema.optional()
210
+ });
211
+ var JobAssignmentSchema = z3.object({
212
+ type: z3.literal("job"),
213
+ run: AgentRunSchema,
214
+ spec: AgentSpecSchema,
215
+ stdinJson: z3.unknown().optional()
216
+ });
217
+ var LogFrameSchema = z3.object({
218
+ type: z3.literal("log"),
219
+ runId: z3.string(),
220
+ seq: z3.number().int().min(0),
221
+ stream: z3.enum(["stdout", "stderr"]),
222
+ chunk: z3.string()
223
+ });
224
+ var RunDoneSchema = z3.object({
225
+ type: z3.literal("done"),
226
+ runId: z3.string(),
227
+ status: z3.enum(["succeeded", "failed", "cancelled"]),
228
+ exitCode: z3.number().int().nullable().optional(),
229
+ errorMessage: z3.string().optional()
230
+ });
231
+ var HelloAckSchema = z3.object({
232
+ type: z3.literal("hello-ack"),
233
+ agentHostId: z3.string(),
234
+ deviceName: z3.string()
235
+ });
236
+ var PingSchema = z3.object({ type: z3.literal("ping") });
237
+ var PongSchema = z3.object({ type: z3.literal("pong") });
238
+ var AgentCallEnvelope = {
239
+ type: z3.literal("agent-call"),
240
+ runId: z3.string(),
241
+ callId: z3.string()
242
+ };
243
+ var IssueBodySetCallSchema = z3.object({
244
+ ...AgentCallEnvelope,
245
+ kind: z3.literal("issue.body.set"),
246
+ issueNumber: z3.number().int(),
247
+ bodyMd: z3.string()
248
+ });
249
+ var FlowNodeConfigSetCallSchema = z3.object({
250
+ ...AgentCallEnvelope,
251
+ kind: z3.literal("flow.node.config.set"),
252
+ flowSlug: z3.string().min(1),
253
+ nodeId: z3.string().min(1),
254
+ config: z3.record(z3.string(), z3.unknown())
255
+ });
256
+ var TemplateNodeConfigSetCallSchema = z3.object({
257
+ ...AgentCallEnvelope,
258
+ kind: z3.literal("template.node.config.set"),
259
+ templateSlug: z3.string().min(1),
260
+ nodeId: z3.string().min(1),
261
+ config: z3.record(z3.string(), z3.unknown())
262
+ });
263
+ var AgentCallSchema = z3.discriminatedUnion("kind", [
264
+ IssueBodySetCallSchema,
265
+ FlowNodeConfigSetCallSchema,
266
+ TemplateNodeConfigSetCallSchema
267
+ ]);
268
+ var AgentCallRequestEnvelope = {
269
+ type: z3.literal("agent-call-request"),
270
+ runId: z3.string(),
271
+ callId: z3.string()
272
+ };
273
+ var IssueBodySetCallRequestSchema = z3.object({
274
+ ...AgentCallRequestEnvelope,
275
+ kind: z3.literal("issue.body.set"),
276
+ issueNumber: z3.number().int(),
277
+ bodyMd: z3.string()
278
+ });
279
+ var FlowNodeConfigSetCallRequestSchema = z3.object({
280
+ ...AgentCallRequestEnvelope,
281
+ kind: z3.literal("flow.node.config.set"),
282
+ flowSlug: z3.string().min(1),
283
+ nodeId: z3.string().min(1),
284
+ config: z3.record(z3.string(), z3.unknown())
285
+ });
286
+ var TemplateNodeConfigSetCallRequestSchema = z3.object({
287
+ ...AgentCallRequestEnvelope,
288
+ kind: z3.literal("template.node.config.set"),
289
+ templateSlug: z3.string().min(1),
290
+ nodeId: z3.string().min(1),
291
+ config: z3.record(z3.string(), z3.unknown())
292
+ });
293
+ var AgentCallRequestSchema = z3.discriminatedUnion("kind", [
294
+ IssueBodySetCallRequestSchema,
295
+ FlowNodeConfigSetCallRequestSchema,
296
+ TemplateNodeConfigSetCallRequestSchema
297
+ ]);
298
+ var AgentCallResultSchema = z3.object({
299
+ type: z3.literal("agent-call-result"),
300
+ runId: z3.string(),
301
+ callId: z3.string(),
302
+ result: z3.union([
303
+ z3.object({ ok: z3.literal(true) }),
304
+ z3.object({ ok: z3.literal(false), reason: z3.string() })
305
+ ])
306
+ });
307
+ var ServerToDeviceMessageSchema = z3.discriminatedUnion("type", [
308
+ JobAssignmentSchema,
309
+ HelloAckSchema,
310
+ PingSchema,
311
+ AgentCallResultSchema
312
+ ]);
313
+ var DeviceToServerMessageSchema = z3.union([
314
+ HelloMessageSchema,
315
+ LogFrameSchema,
316
+ RunDoneSchema,
317
+ PongSchema,
318
+ AgentCallSchema,
319
+ AgentCallRequestSchema
320
+ ]);
321
+ var HostRegisterRequestSchema = z3.object({
322
+ hostId: z3.string(),
323
+ hostName: z3.string(),
324
+ capabilities: z3.array(z3.string()).default([]),
325
+ token: z3.string()
326
+ });
327
+ var HostRegisterResponseSchema = z3.object({
328
+ ok: z3.literal(true),
329
+ pollIntervalMs: z3.number().int().positive()
330
+ });
331
+
332
+ // ../shared/dist/issues.js
333
+ import { z as z4 } from "zod";
334
+ var IssueLabelSchema = z4.object({
335
+ name: z4.string(),
336
+ color: z4.string()
337
+ });
338
+ var IssueAssigneeSchema = z4.object({
339
+ login: z4.string(),
340
+ id: z4.number()
341
+ });
342
+ var IssueSummarySchema = z4.object({
343
+ id: z4.string(),
344
+ number: z4.number().int(),
345
+ title: z4.string(),
346
+ state: z4.string(),
347
+ stateReason: z4.string().nullable(),
348
+ labels: z4.array(IssueLabelSchema),
349
+ assignees: z4.array(IssueAssigneeSchema),
350
+ authorLogin: z4.string().nullable(),
351
+ htmlUrl: z4.string(),
352
+ createdAt: z4.string().datetime(),
353
+ updatedAt: z4.string().datetime(),
354
+ closedAt: z4.string().datetime().nullable()
355
+ });
356
+ var IssueDetailSchema = IssueSummarySchema.extend({
357
+ bodyMd: z4.string().nullable(),
358
+ draftBodyMd: z4.string().nullable(),
359
+ draftUpdatedAt: z4.string().datetime().nullable()
360
+ });
361
+
362
+ // src/mcp/tools.ts
363
+ var issueBodySetShape = IssueBodySetCallSchema.omit({
364
+ type: true,
365
+ runId: true,
366
+ callId: true,
367
+ kind: true
368
+ }).shape;
369
+ var flowNodeConfigSetShape = FlowNodeConfigSetCallSchema.omit({
370
+ type: true,
371
+ runId: true,
372
+ callId: true,
373
+ kind: true
374
+ }).shape;
375
+ var templateNodeConfigSetShape = TemplateNodeConfigSetCallSchema.omit({
376
+ type: true,
377
+ runId: true,
378
+ callId: true,
379
+ kind: true
380
+ }).shape;
381
+ var TOOLS = [
382
+ {
383
+ name: "opencara_issue_body_set",
384
+ kind: "issue.body.set",
385
+ title: "Update an issue body draft",
386
+ description: "Replace the draft Markdown body of an issue in the run's project. The change goes to the draft store \u2014 the user must publish to GitHub from the canvas. Reject with reason if the issue isn't in the run's project scope.",
387
+ inputShape: issueBodySetShape
388
+ },
389
+ {
390
+ name: "opencara_flow_node_config_set",
391
+ kind: "flow.node.config.set",
392
+ title: "Update a flow node's config",
393
+ description: "Replace the config blob of a node in the named flow within the run's project. Reject with reason if the flow or node doesn't exist or is out of scope.",
394
+ inputShape: flowNodeConfigSetShape
395
+ },
396
+ {
397
+ name: "opencara_template_node_config_set",
398
+ kind: "template.node.config.set",
399
+ title: "Update a flow-template draft node's config",
400
+ description: "Replace the config blob of a node in the user's draft of the named flow template. Per-user scope, not per-project. Reject with reason if the template draft isn't owned by the run's user.",
401
+ inputShape: templateNodeConfigSetShape
402
+ }
403
+ ];
404
+ function registerOpencaraTools(server, router) {
405
+ for (const tool of TOOLS) {
406
+ server.registerTool(
407
+ tool.name,
408
+ {
409
+ title: tool.title,
410
+ description: tool.description,
411
+ inputSchema: tool.inputShape
412
+ },
413
+ async (args) => {
414
+ const result = await router.call(tool.kind, args);
415
+ if (result.ok) {
416
+ return {
417
+ content: [{ type: "text", text: "ok" }]
418
+ };
419
+ }
420
+ return {
421
+ content: [{ type: "text", text: `rejected: ${result.reason}` }],
422
+ isError: true
423
+ };
424
+ }
425
+ );
426
+ }
427
+ }
428
+ var TOOL_NAMES = TOOLS.map((t) => ({ name: t.name, kind: t.kind }));
429
+
430
+ // src/bin/opencara-mcp.ts
431
+ async function main() {
432
+ const socketPath = env[IPC_SOCKET_ENV];
433
+ if (!socketPath) {
434
+ stderr.write(
435
+ `[opencara-mcp] missing ${IPC_SOCKET_ENV} env \u2014 this binary is meant to be spawned by the opencara CLI device, not run directly.
436
+ `
437
+ );
438
+ exit(2);
439
+ }
440
+ const ipc = new IpcClient(socketPath);
441
+ try {
442
+ await ipc.connect();
443
+ } catch (err) {
444
+ const e = err instanceof Error ? err.message : String(err);
445
+ stderr.write(`[opencara-mcp] ipc connect failed: ${e}
446
+ `);
447
+ exit(3);
448
+ }
449
+ const router = {
450
+ async call(kind, args) {
451
+ return ipc.call(kind, args);
452
+ }
453
+ };
454
+ const server = new McpServer(
455
+ { name: "opencara", version: "0.0.0" },
456
+ { capabilities: { tools: {} } }
457
+ );
458
+ registerOpencaraTools(server, router);
459
+ const transport = new StdioServerTransport();
460
+ await server.connect(transport);
461
+ }
462
+ main().catch((err) => {
463
+ const e = err instanceof Error ? err.stack ?? err.message : String(err);
464
+ stderr.write(`[opencara-mcp] fatal: ${e}
465
+ `);
466
+ exit(1);
467
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.101.0",
3
+ "version": "0.103.0",
4
4
  "description": "OpenCara agent-host CLI: register a machine as an agent host and run dispatched agents.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,15 +11,19 @@
11
11
  "homepage": "https://opencara.com",
12
12
  "type": "module",
13
13
  "bin": {
14
- "opencara": "./dist/bin.js"
14
+ "opencara": "./dist/bin.js",
15
+ "opencara-mcp": "./dist/opencara-mcp.js"
15
16
  },
16
17
  "main": "./dist/bin.js",
17
18
  "files": [
18
- "dist/bin.js"
19
+ "dist/bin.js",
20
+ "dist/opencara-mcp.js"
19
21
  ],
20
22
  "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.29.0",
24
+ "@zed-industries/codex-acp": "^0.13.0",
21
25
  "ws": "^8.18.0",
22
- "zod": "^3.24.1"
26
+ "zod": "^3.25.0"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@types/ws": "^8.5.13",
@@ -32,6 +36,9 @@
32
36
  "dev": "tsx watch src/bin.ts",
33
37
  "start": "node dist/bin.js",
34
38
  "typecheck": "tsc -b",
39
+ "test": "node --import tsx --test --test-reporter=spec src/acp/__tests__/*.test.ts src/mcp/__tests__/*.test.ts src/runner/__tests__/*.test.ts",
40
+ "acp:spike": "tsx src/acp/spike.ts",
41
+ "mcp:smoke": "tsx src/mcp/smoke.ts",
35
42
  "clean": "rm -rf dist *.tsbuildinfo"
36
43
  }
37
44
  }