opencara 0.103.0 → 0.104.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.
- package/dist/bin.js +29 -7
- package/dist/claude-acp.js +267 -0
- package/package.json +5 -3
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:
|
|
1054
|
-
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.
|
|
1283
|
+
var PKG_VERSION = "0.104.1";
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
1668
|
+
if (!existsSync4(target)) continue;
|
|
1647
1669
|
let resolved;
|
|
1648
1670
|
try {
|
|
1649
1671
|
resolved = realpathSync(target);
|
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (type === "stream_event") {
|
|
138
|
+
const event = msg["event"];
|
|
139
|
+
if (event?.type !== "content_block_delta") return;
|
|
140
|
+
if (event.delta?.type !== "text_delta") return;
|
|
141
|
+
const text = typeof event.delta.text === "string" ? event.delta.text : "";
|
|
142
|
+
if (text.length === 0) return;
|
|
143
|
+
notify("session/update", {
|
|
144
|
+
sessionId,
|
|
145
|
+
update: {
|
|
146
|
+
sessionUpdate: "agent_message_chunk",
|
|
147
|
+
content: { type: "text", text }
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (type === "result") {
|
|
153
|
+
const subtype = typeof msg["subtype"] === "string" ? msg["subtype"] : "";
|
|
154
|
+
const isError = msg["is_error"] === true;
|
|
155
|
+
if (isError) {
|
|
156
|
+
const resultText = typeof msg["result"] === "string" ? msg["result"] : "";
|
|
157
|
+
if (resultText.length > 0) {
|
|
158
|
+
notify("session/update", {
|
|
159
|
+
sessionId,
|
|
160
|
+
update: {
|
|
161
|
+
sessionUpdate: "agent_message_chunk",
|
|
162
|
+
content: { type: "text", text: `
|
|
163
|
+
|
|
164
|
+
[claude error: ${resultText}]` }
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
done("refusal");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (subtype === "error_max_turns") {
|
|
172
|
+
done("max_turn_requests");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (subtype === "error_max_tokens") {
|
|
176
|
+
done("max_tokens");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
done("end_turn");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function handleInitialize(_params) {
|
|
184
|
+
return {
|
|
185
|
+
protocolVersion: 1,
|
|
186
|
+
agentInfo: {
|
|
187
|
+
name: "opencara-claude-acp",
|
|
188
|
+
title: "opencara Claude shim",
|
|
189
|
+
version: "0.0.1"
|
|
190
|
+
},
|
|
191
|
+
agentCapabilities: {
|
|
192
|
+
// No session resume yet (follow-up). MCP via stdio works because
|
|
193
|
+
// the `claude` CLI itself supports `mcpServers` in settings.json
|
|
194
|
+
// but we don't propagate ACP's mcpServers config in this MVP.
|
|
195
|
+
loadSession: false,
|
|
196
|
+
mcpCapabilities: {},
|
|
197
|
+
promptCapabilities: { embeddedContext: false, image: false, audio: false }
|
|
198
|
+
},
|
|
199
|
+
authMethods: []
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function handleNewSession(params) {
|
|
203
|
+
const sessionId = randomUUID();
|
|
204
|
+
sessions.set(sessionId, {
|
|
205
|
+
claudeSessionId: randomUUID(),
|
|
206
|
+
cwd: params.cwd ?? process.cwd()
|
|
207
|
+
});
|
|
208
|
+
return { sessionId };
|
|
209
|
+
}
|
|
210
|
+
async function handlePrompt(params) {
|
|
211
|
+
const state = sessions.get(params.sessionId);
|
|
212
|
+
if (!state) {
|
|
213
|
+
throw new Error(`unknown sessionId: ${params.sessionId}`);
|
|
214
|
+
}
|
|
215
|
+
const promptText = params.prompt.filter((b) => b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").join("\n\n");
|
|
216
|
+
if (promptText.length === 0) {
|
|
217
|
+
throw new Error("session/prompt: no text content blocks");
|
|
218
|
+
}
|
|
219
|
+
const result = await runClaudeTurn(params.sessionId, state, promptText);
|
|
220
|
+
return { stopReason: result.stopReason };
|
|
221
|
+
}
|
|
222
|
+
var decoder = new FrameDecoder();
|
|
223
|
+
stdin.setEncoding("utf8");
|
|
224
|
+
stdin.on("data", (chunk) => {
|
|
225
|
+
const { messages, malformed } = decoder.feed(chunk);
|
|
226
|
+
for (const line of malformed) {
|
|
227
|
+
stderr.write(`[claude-acp] malformed inbound: ${line}
|
|
228
|
+
`);
|
|
229
|
+
}
|
|
230
|
+
for (const msg of messages) void dispatch(msg);
|
|
231
|
+
});
|
|
232
|
+
stdin.on("end", () => {
|
|
233
|
+
exit(0);
|
|
234
|
+
});
|
|
235
|
+
async function dispatch(msg) {
|
|
236
|
+
if (!("id" in msg) || msg.id == null) return;
|
|
237
|
+
if ("result" in msg || "error" in msg) return;
|
|
238
|
+
const req = msg;
|
|
239
|
+
try {
|
|
240
|
+
switch (req.method) {
|
|
241
|
+
case "initialize":
|
|
242
|
+
reply(req.id, handleInitialize(req.params));
|
|
243
|
+
return;
|
|
244
|
+
case "session/new":
|
|
245
|
+
reply(req.id, handleNewSession(req.params));
|
|
246
|
+
return;
|
|
247
|
+
case "session/prompt": {
|
|
248
|
+
const result = await handlePrompt(req.params);
|
|
249
|
+
reply(req.id, result);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
replyError(
|
|
254
|
+
req.id,
|
|
255
|
+
JSON_RPC_ERROR_METHOD_NOT_FOUND,
|
|
256
|
+
`method not implemented: ${req.method}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
replyError(
|
|
262
|
+
req.id,
|
|
263
|
+
err instanceof Error && message.startsWith("session/prompt:") ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
|
|
264
|
+
message
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencara",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.1",
|
|
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",
|