opencara 0.103.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -458,6 +458,11 @@ var WsClient = class {
458
458
  // src/runner/spawn.ts
459
459
  import { spawn as spawn3 } from "node:child_process";
460
460
 
461
+ // src/runner/acpRunner.ts
462
+ import { existsSync as existsSync3 } from "node:fs";
463
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
464
+ import { dirname as dirname2, resolve as pathResolve3 } from "node:path";
465
+
461
466
  // src/acp/client.ts
462
467
  import { spawn as spawn2 } from "node:child_process";
463
468
  import { EventEmitter } from "node:events";
@@ -1049,9 +1054,10 @@ function runAcpJob(opts) {
1049
1054
  sendRequest: handlers.sendAgentCall
1050
1055
  });
1051
1056
  const host = new McpHost({ runId, router: bridge.router });
1057
+ const resolved = resolveLocalAcpAdapter(spec.command, spec.args);
1052
1058
  const client = new AcpClient({
1053
- command: spec.command,
1054
- args: spec.args,
1059
+ command: resolved.command,
1060
+ args: resolved.args,
1055
1061
  env: spec.env,
1056
1062
  cwd: spec.cwd
1057
1063
  });
@@ -1164,6 +1170,22 @@ function textOfContent(content) {
1164
1170
  if (content.type !== "text") return "";
1165
1171
  return content.text ?? "";
1166
1172
  }
1173
+ var LOCAL_ACP_ADAPTERS = /* @__PURE__ */ new Set(["claude-acp"]);
1174
+ function resolveLocalAcpAdapter(command, args) {
1175
+ if (!LOCAL_ACP_ADAPTERS.has(command)) {
1176
+ return { command, args: [...args] };
1177
+ }
1178
+ const here = dirname2(fileURLToPath2(import.meta.url));
1179
+ const sourceBin = pathResolve3(here, "..", "bin", `${command}.ts`);
1180
+ if (existsSync3(sourceBin)) {
1181
+ return { command: "tsx", args: [sourceBin, ...args] };
1182
+ }
1183
+ const distBin = pathResolve3(here, `${command}.js`);
1184
+ if (existsSync3(distBin)) {
1185
+ return { command: "node", args: [distBin, ...args] };
1186
+ }
1187
+ return { command, args: [...args] };
1188
+ }
1167
1189
 
1168
1190
  // src/runner/spawn.ts
1169
1191
  function runJob(spec, stdinJson, handlers) {
@@ -1258,7 +1280,7 @@ var AgentCallParser = class {
1258
1280
  };
1259
1281
 
1260
1282
  // src/commands/run.ts
1261
- var PKG_VERSION = "0.103.0";
1283
+ var PKG_VERSION = "0.104.0";
1262
1284
  var LOG_FLUSH_MS = 800;
1263
1285
  var MAX_CHUNK_SIZE = 4 * 1024;
1264
1286
  async function run(opts = {}) {
@@ -1501,7 +1523,7 @@ import {
1501
1523
  mkdirSync as mkdirSync2,
1502
1524
  readFileSync as readFileSync2,
1503
1525
  rmSync,
1504
- existsSync as existsSync3,
1526
+ existsSync as existsSync4,
1505
1527
  realpathSync,
1506
1528
  writeFileSync as writeFileSync2,
1507
1529
  renameSync
@@ -1556,7 +1578,7 @@ function worktreeCreate(args) {
1556
1578
  const HELPER_SNIPPET = '!f() { echo username=x-access-token; echo "password=$GH_TOKEN"; }; f';
1557
1579
  const cleanUrl = `https://github.com/${repo}.git`;
1558
1580
  mkdirSync2(sessionDir, { recursive: true });
1559
- if (existsSync3(join2(checkoutDir, ".git"))) {
1581
+ if (existsSync4(join2(checkoutDir, ".git"))) {
1560
1582
  git(checkoutDir, ["fetch", "origin"]);
1561
1583
  git(checkoutDir, ["checkout", "-B", branch, `origin/${branch}`]);
1562
1584
  } else {
@@ -1584,7 +1606,7 @@ function worktreeCreate(args) {
1584
1606
  }
1585
1607
  let priorSession = null;
1586
1608
  const sessionFile = join2(sessionDir, "agent-session.json");
1587
- if (existsSync3(sessionFile)) {
1609
+ if (existsSync4(sessionFile)) {
1588
1610
  try {
1589
1611
  const parsed = JSON.parse(readFileSync2(sessionFile, "utf8"));
1590
1612
  if (typeof parsed.kind === "string" && typeof parsed.id === "string") {
@@ -1643,7 +1665,7 @@ function worktreeRemove(args) {
1643
1665
  const opencaraRoot = realpathSync(OPENCARA_ROOT);
1644
1666
  for (const subtreeRoot of [WORK_ROOT, SESSION_ROOT]) {
1645
1667
  const target = join2(subtreeRoot, key);
1646
- if (!existsSync3(target)) continue;
1668
+ if (!existsSync4(target)) continue;
1647
1669
  let resolved;
1648
1670
  try {
1649
1671
  resolved = realpathSync(target);
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/claude-acp.ts
4
+ import { spawn } from "node:child_process";
5
+ import { randomUUID } from "node:crypto";
6
+ import { stdin, stdout, stderr, exit } from "node:process";
7
+
8
+ // src/acp/framing.ts
9
+ function encodeFrame(msg) {
10
+ return JSON.stringify(msg) + "\n";
11
+ }
12
+ var FrameDecoder = class {
13
+ buffer = "";
14
+ /** Feed a chunk. Returns parsed messages and any malformed lines. */
15
+ feed(chunk) {
16
+ this.buffer += chunk;
17
+ const messages = [];
18
+ const malformed = [];
19
+ const lines = this.buffer.split("\n");
20
+ this.buffer = lines.pop() ?? "";
21
+ for (const line of lines) {
22
+ const trimmed = line.trim();
23
+ if (trimmed.length === 0) continue;
24
+ try {
25
+ const parsed = JSON.parse(trimmed);
26
+ if (isPlainObject(parsed)) {
27
+ messages.push(parsed);
28
+ } else {
29
+ malformed.push(trimmed);
30
+ }
31
+ } catch {
32
+ malformed.push(trimmed);
33
+ }
34
+ }
35
+ return { messages, malformed };
36
+ }
37
+ /** Whatever's still buffered (a partial line awaiting more data). */
38
+ remainder() {
39
+ return this.buffer;
40
+ }
41
+ };
42
+ function isPlainObject(v) {
43
+ return typeof v === "object" && v !== null && !Array.isArray(v);
44
+ }
45
+
46
+ // src/acp/jsonrpc.ts
47
+ var JSON_RPC_ERROR_METHOD_NOT_FOUND = -32601;
48
+ var JSON_RPC_ERROR_INVALID_PARAMS = -32602;
49
+ var JSON_RPC_ERROR_INTERNAL = -32603;
50
+
51
+ // src/bin/claude-acp.ts
52
+ function send(msg) {
53
+ stdout.write(encodeFrame(msg));
54
+ }
55
+ function reply(id, result) {
56
+ send({ jsonrpc: "2.0", id, result });
57
+ }
58
+ function replyError(id, code, message) {
59
+ send({ jsonrpc: "2.0", id, error: { code, message } });
60
+ }
61
+ function notify(method, params) {
62
+ send({ jsonrpc: "2.0", method, params });
63
+ }
64
+ var sessions = /* @__PURE__ */ new Map();
65
+ async function runClaudeTurn(sessionId, state, promptText) {
66
+ return new Promise((resolve, reject) => {
67
+ const args = [
68
+ "-p",
69
+ "--output-format",
70
+ "stream-json",
71
+ // Required for stream-json output per Claude's CLI contract —
72
+ // without it Claude refuses on the grounds that streaming makes
73
+ // sense only when stdin can be partial too.
74
+ "--include-partial-messages",
75
+ "--verbose",
76
+ "--session-id",
77
+ state.claudeSessionId,
78
+ // Headless: no human in the loop to approve tool use. Matches the
79
+ // legacy `claudeAdapter` posture in agents/kinds.ts.
80
+ "--dangerously-skip-permissions",
81
+ promptText
82
+ ];
83
+ const child = spawn("claude", args, {
84
+ cwd: state.cwd,
85
+ env: process.env,
86
+ stdio: ["ignore", "pipe", "pipe"]
87
+ });
88
+ const decoder2 = new FrameDecoder();
89
+ let resolved = false;
90
+ let stopReason = "end_turn";
91
+ child.stdout.setEncoding("utf8");
92
+ child.stdout.on("data", (chunk) => {
93
+ const { messages, malformed } = decoder2.feed(chunk);
94
+ for (const line of malformed) {
95
+ stderr.write(`[claude-acp] malformed: ${line}
96
+ `);
97
+ }
98
+ for (const msg of messages) {
99
+ handleClaudeEvent(sessionId, msg, (sr) => {
100
+ if (resolved) return;
101
+ resolved = true;
102
+ stopReason = sr;
103
+ });
104
+ }
105
+ });
106
+ child.stderr.setEncoding("utf8");
107
+ child.stderr.on("data", (chunk) => {
108
+ stderr.write(chunk);
109
+ });
110
+ child.on("error", (err) => {
111
+ if (!resolved) {
112
+ resolved = true;
113
+ reject(err);
114
+ }
115
+ });
116
+ child.on("close", (code) => {
117
+ if (!resolved) {
118
+ resolved = true;
119
+ if (code !== 0) {
120
+ stderr.write(`[claude-acp] claude exited code=${code} without result event
121
+ `);
122
+ resolve({ stopReason: "refusal" });
123
+ return;
124
+ }
125
+ }
126
+ resolve({ stopReason });
127
+ });
128
+ });
129
+ }
130
+ function handleClaudeEvent(sessionId, raw, done) {
131
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return;
132
+ const msg = raw;
133
+ const type = typeof msg["type"] === "string" ? msg["type"] : "";
134
+ if (type === "assistant") {
135
+ const message = msg["message"];
136
+ const blocks = message?.content ?? [];
137
+ for (const block of blocks) {
138
+ if (block?.type !== "text") continue;
139
+ const text = typeof block.text === "string" ? block.text : "";
140
+ if (text.length === 0) continue;
141
+ notify("session/update", {
142
+ sessionId,
143
+ update: {
144
+ sessionUpdate: "agent_message_chunk",
145
+ content: { type: "text", text }
146
+ }
147
+ });
148
+ }
149
+ return;
150
+ }
151
+ if (type === "stream_event") {
152
+ const event = msg["event"];
153
+ if (event?.type !== "content_block_delta") return;
154
+ if (event.delta?.type !== "text_delta") return;
155
+ const text = typeof event.delta.text === "string" ? event.delta.text : "";
156
+ if (text.length === 0) return;
157
+ notify("session/update", {
158
+ sessionId,
159
+ update: {
160
+ sessionUpdate: "agent_message_chunk",
161
+ content: { type: "text", text }
162
+ }
163
+ });
164
+ return;
165
+ }
166
+ if (type === "result") {
167
+ const subtype = typeof msg["subtype"] === "string" ? msg["subtype"] : "";
168
+ const isError = msg["is_error"] === true;
169
+ if (isError) {
170
+ const resultText = typeof msg["result"] === "string" ? msg["result"] : "";
171
+ if (resultText.length > 0) {
172
+ notify("session/update", {
173
+ sessionId,
174
+ update: {
175
+ sessionUpdate: "agent_message_chunk",
176
+ content: { type: "text", text: `
177
+
178
+ [claude error: ${resultText}]` }
179
+ }
180
+ });
181
+ }
182
+ done("refusal");
183
+ return;
184
+ }
185
+ if (subtype === "error_max_turns") {
186
+ done("max_turn_requests");
187
+ return;
188
+ }
189
+ if (subtype === "error_max_tokens") {
190
+ done("max_tokens");
191
+ return;
192
+ }
193
+ done("end_turn");
194
+ return;
195
+ }
196
+ }
197
+ function handleInitialize(_params) {
198
+ return {
199
+ protocolVersion: 1,
200
+ agentInfo: {
201
+ name: "opencara-claude-acp",
202
+ title: "opencara Claude shim",
203
+ version: "0.0.1"
204
+ },
205
+ agentCapabilities: {
206
+ // No session resume yet (follow-up). MCP via stdio works because
207
+ // the `claude` CLI itself supports `mcpServers` in settings.json
208
+ // but we don't propagate ACP's mcpServers config in this MVP.
209
+ loadSession: false,
210
+ mcpCapabilities: {},
211
+ promptCapabilities: { embeddedContext: false, image: false, audio: false }
212
+ },
213
+ authMethods: []
214
+ };
215
+ }
216
+ function handleNewSession(params) {
217
+ const sessionId = randomUUID();
218
+ sessions.set(sessionId, {
219
+ claudeSessionId: randomUUID(),
220
+ cwd: params.cwd ?? process.cwd()
221
+ });
222
+ return { sessionId };
223
+ }
224
+ async function handlePrompt(params) {
225
+ const state = sessions.get(params.sessionId);
226
+ if (!state) {
227
+ throw new Error(`unknown sessionId: ${params.sessionId}`);
228
+ }
229
+ const promptText = params.prompt.filter((b) => b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").join("\n\n");
230
+ if (promptText.length === 0) {
231
+ throw new Error("session/prompt: no text content blocks");
232
+ }
233
+ const result = await runClaudeTurn(params.sessionId, state, promptText);
234
+ return { stopReason: result.stopReason };
235
+ }
236
+ var decoder = new FrameDecoder();
237
+ stdin.setEncoding("utf8");
238
+ stdin.on("data", (chunk) => {
239
+ const { messages, malformed } = decoder.feed(chunk);
240
+ for (const line of malformed) {
241
+ stderr.write(`[claude-acp] malformed inbound: ${line}
242
+ `);
243
+ }
244
+ for (const msg of messages) void dispatch(msg);
245
+ });
246
+ stdin.on("end", () => {
247
+ exit(0);
248
+ });
249
+ async function dispatch(msg) {
250
+ if (!("id" in msg) || msg.id == null) return;
251
+ if ("result" in msg || "error" in msg) return;
252
+ const req = msg;
253
+ try {
254
+ switch (req.method) {
255
+ case "initialize":
256
+ reply(req.id, handleInitialize(req.params));
257
+ return;
258
+ case "session/new":
259
+ reply(req.id, handleNewSession(req.params));
260
+ return;
261
+ case "session/prompt": {
262
+ const result = await handlePrompt(req.params);
263
+ reply(req.id, result);
264
+ return;
265
+ }
266
+ default:
267
+ replyError(
268
+ req.id,
269
+ JSON_RPC_ERROR_METHOD_NOT_FOUND,
270
+ `method not implemented: ${req.method}`
271
+ );
272
+ }
273
+ } catch (err) {
274
+ const message = err instanceof Error ? err.message : String(err);
275
+ replyError(
276
+ req.id,
277
+ err instanceof Error && message.startsWith("session/prompt:") ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
278
+ message
279
+ );
280
+ }
281
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.103.0",
3
+ "version": "0.104.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": {
@@ -12,12 +12,14 @@
12
12
  "type": "module",
13
13
  "bin": {
14
14
  "opencara": "./dist/bin.js",
15
- "opencara-mcp": "./dist/opencara-mcp.js"
15
+ "opencara-mcp": "./dist/opencara-mcp.js",
16
+ "claude-acp": "./dist/claude-acp.js"
16
17
  },
17
18
  "main": "./dist/bin.js",
18
19
  "files": [
19
20
  "dist/bin.js",
20
- "dist/opencara-mcp.js"
21
+ "dist/opencara-mcp.js",
22
+ "dist/claude-acp.js"
21
23
  ],
22
24
  "dependencies": {
23
25
  "@modelcontextprotocol/sdk": "^1.29.0",