opencara 0.102.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 +948 -35
- package/dist/claude-acp.js +281 -0
- package/dist/opencara-mcp.js +467 -0
- package/package.json +13 -4
|
@@ -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
|
+
}
|