opencara 0.105.0 → 0.105.2
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 +100 -15
- package/dist/claude-acp.js +48 -27
- package/dist/opencara-mcp.js +8 -2
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
uptime
|
|
14
14
|
} from "node:os";
|
|
15
15
|
import { statfsSync } from "node:fs";
|
|
16
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
16
17
|
|
|
17
18
|
// src/config/store.ts
|
|
18
19
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
@@ -85,7 +86,8 @@ var AcpSpecSchema = z3.object({
|
|
|
85
86
|
systemPromptMd: z3.string(),
|
|
86
87
|
userPromptMd: z3.string(),
|
|
87
88
|
history: z3.array(AcpHistoryTurnSchema).default([]),
|
|
88
|
-
pageContextJson: z3.string().optional()
|
|
89
|
+
pageContextJson: z3.string().optional(),
|
|
90
|
+
priorSessionId: z3.string().optional()
|
|
89
91
|
});
|
|
90
92
|
var AgentSpecSchema = z3.object({
|
|
91
93
|
kind: z3.string(),
|
|
@@ -184,7 +186,12 @@ var RunDoneSchema = z4.object({
|
|
|
184
186
|
runId: z4.string(),
|
|
185
187
|
status: z4.enum(["succeeded", "failed", "cancelled"]),
|
|
186
188
|
exitCode: z4.number().int().nullable().optional(),
|
|
187
|
-
errorMessage: z4.string().optional()
|
|
189
|
+
errorMessage: z4.string().optional(),
|
|
190
|
+
/** ACP session id the agent ran under (fresh from session/new, or
|
|
191
|
+
* echoed from session/load). The orchestrator persists this per
|
|
192
|
+
* (repo, branch) so the next iteration can resume via session/load.
|
|
193
|
+
* Null/absent for non-ACP runs (worktree-allocate, write-session). */
|
|
194
|
+
acpSessionId: z4.string().nullable().optional()
|
|
188
195
|
});
|
|
189
196
|
var HelloAckSchema = z4.object({
|
|
190
197
|
type: z4.literal("hello-ack"),
|
|
@@ -561,6 +568,9 @@ var AcpConnection = class {
|
|
|
561
568
|
newSession(req) {
|
|
562
569
|
return this.request(ACP_METHODS.session_new, req);
|
|
563
570
|
}
|
|
571
|
+
loadSession(req) {
|
|
572
|
+
return this.request(ACP_METHODS.session_load, req);
|
|
573
|
+
}
|
|
564
574
|
prompt(req) {
|
|
565
575
|
return this.request(ACP_METHODS.session_prompt, req);
|
|
566
576
|
}
|
|
@@ -722,6 +732,9 @@ var AcpClient = class {
|
|
|
722
732
|
newSession(req) {
|
|
723
733
|
return this.must().newSession(req);
|
|
724
734
|
}
|
|
735
|
+
loadSession(req) {
|
|
736
|
+
return this.must().loadSession(req);
|
|
737
|
+
}
|
|
725
738
|
prompt(req) {
|
|
726
739
|
return this.must().prompt(req);
|
|
727
740
|
}
|
|
@@ -1065,26 +1078,35 @@ function runAcpJob(opts) {
|
|
|
1065
1078
|
}
|
|
1066
1079
|
};
|
|
1067
1080
|
const promise = (async () => {
|
|
1068
|
-
let result = { exitCode: 1, stopReason: "uninitialized" };
|
|
1081
|
+
let result = { exitCode: 1, stopReason: "uninitialized", sessionId: "" };
|
|
1069
1082
|
try {
|
|
1070
1083
|
await host.start();
|
|
1071
1084
|
client.start();
|
|
1072
|
-
await client.initialize({
|
|
1085
|
+
const initResult = await client.initialize({
|
|
1073
1086
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
1074
1087
|
clientCapabilities: {}
|
|
1075
1088
|
});
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1089
|
+
const cwd = spec.cwd ?? process.cwd();
|
|
1090
|
+
const mcpServers = [host.acpServerEntry()];
|
|
1091
|
+
const shimSupportsLoad = initResult.agentCapabilities?.loadSession === true;
|
|
1092
|
+
let sessionId;
|
|
1093
|
+
if (acpSpec.priorSessionId && shimSupportsLoad) {
|
|
1094
|
+
await client.loadSession({
|
|
1095
|
+
sessionId: acpSpec.priorSessionId,
|
|
1096
|
+
cwd,
|
|
1097
|
+
mcpServers
|
|
1098
|
+
});
|
|
1099
|
+
sessionId = acpSpec.priorSessionId;
|
|
1100
|
+
} else {
|
|
1101
|
+
const session = await client.newSession({ cwd, mcpServers });
|
|
1102
|
+
sessionId = session.sessionId;
|
|
1103
|
+
}
|
|
1080
1104
|
const prompt = buildPromptContent(acpSpec);
|
|
1081
|
-
const promptResult = await client.prompt({
|
|
1082
|
-
sessionId: session.sessionId,
|
|
1083
|
-
prompt
|
|
1084
|
-
});
|
|
1105
|
+
const promptResult = await client.prompt({ sessionId, prompt });
|
|
1085
1106
|
result = {
|
|
1086
1107
|
exitCode: promptResult.stopReason === "end_turn" ? 0 : 1,
|
|
1087
|
-
stopReason: promptResult.stopReason
|
|
1108
|
+
stopReason: promptResult.stopReason,
|
|
1109
|
+
sessionId
|
|
1088
1110
|
};
|
|
1089
1111
|
return result;
|
|
1090
1112
|
} finally {
|
|
@@ -1184,7 +1206,7 @@ function resolveLocalAcpAdapter(command, args) {
|
|
|
1184
1206
|
}
|
|
1185
1207
|
|
|
1186
1208
|
// src/commands/run.ts
|
|
1187
|
-
var PKG_VERSION = "0.105.
|
|
1209
|
+
var PKG_VERSION = "0.105.2";
|
|
1188
1210
|
var LOG_FLUSH_MS = 800;
|
|
1189
1211
|
var MAX_CHUNK_SIZE = 4 * 1024;
|
|
1190
1212
|
async function run(opts = {}) {
|
|
@@ -1260,6 +1282,11 @@ async function executeJob(job, client) {
|
|
|
1260
1282
|
if (flushTimer) return;
|
|
1261
1283
|
flushTimer = setTimeout(flush, LOG_FLUSH_MS);
|
|
1262
1284
|
};
|
|
1285
|
+
if (isInternalCommand(job.spec)) {
|
|
1286
|
+
flush();
|
|
1287
|
+
await runInternalCommand(job, client);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1263
1290
|
if (!job.spec.acp) {
|
|
1264
1291
|
flush();
|
|
1265
1292
|
const message = `legacy stdin-JSON dispatch removed in v0.30 \u2014 orchestrator must send spec.acp. Got command: ${job.spec.command}.`;
|
|
@@ -1286,7 +1313,8 @@ async function executeJob(job, client) {
|
|
|
1286
1313
|
type: "done",
|
|
1287
1314
|
runId,
|
|
1288
1315
|
status: result.exitCode === 0 ? "succeeded" : "failed",
|
|
1289
|
-
exitCode: result.exitCode
|
|
1316
|
+
exitCode: result.exitCode,
|
|
1317
|
+
acpSessionId: result.sessionId || null
|
|
1290
1318
|
});
|
|
1291
1319
|
console.log(
|
|
1292
1320
|
`[opencara] job ${runId.slice(-8)} (acp) \u2192 ${result.stopReason} exit=${result.exitCode}`
|
|
@@ -1300,6 +1328,63 @@ async function executeJob(job, client) {
|
|
|
1300
1328
|
acpControllers.delete(runId);
|
|
1301
1329
|
}
|
|
1302
1330
|
}
|
|
1331
|
+
function isInternalCommand(spec) {
|
|
1332
|
+
return spec.command === "opencara" && Array.isArray(spec.args) && spec.args[0] === "internal";
|
|
1333
|
+
}
|
|
1334
|
+
async function runInternalCommand(job, client, opts = {}) {
|
|
1335
|
+
const runId = job.run.id;
|
|
1336
|
+
const args = job.spec.args ?? [];
|
|
1337
|
+
const binPath = opts.binPath ?? process.argv[1];
|
|
1338
|
+
const nodePath = opts.nodePath ?? process.execPath;
|
|
1339
|
+
if (!binPath) {
|
|
1340
|
+
client.send({
|
|
1341
|
+
type: "done",
|
|
1342
|
+
runId,
|
|
1343
|
+
status: "failed",
|
|
1344
|
+
errorMessage: "internal: process.argv[1] missing \u2014 cannot re-invoke bin"
|
|
1345
|
+
});
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
let seq = 0;
|
|
1349
|
+
const emit = (stream, chunk) => {
|
|
1350
|
+
let remaining = chunk;
|
|
1351
|
+
while (remaining.length > 0) {
|
|
1352
|
+
const take = remaining.slice(0, MAX_CHUNK_SIZE);
|
|
1353
|
+
client.send({ type: "log", runId, seq: seq++, stream, chunk: take });
|
|
1354
|
+
remaining = remaining.slice(MAX_CHUNK_SIZE);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const child = spawn3(nodePath, [binPath, ...args], {
|
|
1358
|
+
cwd: job.spec.cwd,
|
|
1359
|
+
env: job.spec.env ? { ...process.env, ...job.spec.env } : process.env,
|
|
1360
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1361
|
+
});
|
|
1362
|
+
child.stdout.setEncoding("utf8");
|
|
1363
|
+
child.stdout.on("data", (c) => emit("stdout", c));
|
|
1364
|
+
child.stderr.setEncoding("utf8");
|
|
1365
|
+
child.stderr.on("data", (c) => emit("stderr", c));
|
|
1366
|
+
const { exitCode, errorMessage } = await new Promise((resolve) => {
|
|
1367
|
+
let done = false;
|
|
1368
|
+
child.on("error", (err) => {
|
|
1369
|
+
if (done) return;
|
|
1370
|
+
done = true;
|
|
1371
|
+
resolve({ exitCode: 1, errorMessage: `internal spawn error: ${err.message}` });
|
|
1372
|
+
});
|
|
1373
|
+
child.on("close", (code, signal) => {
|
|
1374
|
+
if (done) return;
|
|
1375
|
+
done = true;
|
|
1376
|
+
resolve({ exitCode: code ?? (signal ? 1 : 0) });
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
client.send({
|
|
1380
|
+
type: "done",
|
|
1381
|
+
runId,
|
|
1382
|
+
status: exitCode === 0 ? "succeeded" : "failed",
|
|
1383
|
+
exitCode,
|
|
1384
|
+
...errorMessage ? { errorMessage } : {}
|
|
1385
|
+
});
|
|
1386
|
+
console.log(`[opencara] job ${runId.slice(-8)} (internal) \u2192 exit=${exitCode}`);
|
|
1387
|
+
}
|
|
1303
1388
|
function collectSystemInfo() {
|
|
1304
1389
|
try {
|
|
1305
1390
|
const cpuList = cpus();
|
package/dist/claude-acp.js
CHANGED
|
@@ -74,23 +74,25 @@ async function runClaudeTurn(sessionId, state, promptText) {
|
|
|
74
74
|
"--include-partial-messages",
|
|
75
75
|
"--verbose",
|
|
76
76
|
"--session-id",
|
|
77
|
-
|
|
77
|
+
sessionId,
|
|
78
78
|
// Headless: no human in the loop to approve tool use. Matches the
|
|
79
79
|
// legacy `claudeAdapter` posture in agents/kinds.ts.
|
|
80
|
-
"--dangerously-skip-permissions"
|
|
81
|
-
promptText
|
|
80
|
+
"--dangerously-skip-permissions"
|
|
82
81
|
];
|
|
83
82
|
const child = spawn("claude", args, {
|
|
84
83
|
cwd: state.cwd,
|
|
85
84
|
env: process.env,
|
|
86
|
-
stdio: ["
|
|
85
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
86
|
+
});
|
|
87
|
+
child.stdin.on("error", () => {
|
|
87
88
|
});
|
|
88
|
-
|
|
89
|
+
child.stdin.end(promptText);
|
|
90
|
+
const decoder = new FrameDecoder();
|
|
89
91
|
let resolved = false;
|
|
90
92
|
let stopReason = "end_turn";
|
|
91
93
|
child.stdout.setEncoding("utf8");
|
|
92
94
|
child.stdout.on("data", (chunk) => {
|
|
93
|
-
const { messages, malformed } =
|
|
95
|
+
const { messages, malformed } = decoder.feed(chunk);
|
|
94
96
|
for (const line of malformed) {
|
|
95
97
|
stderr.write(`[claude-acp] malformed: ${line}
|
|
96
98
|
`);
|
|
@@ -189,10 +191,12 @@ function handleInitialize(_params) {
|
|
|
189
191
|
version: "0.0.1"
|
|
190
192
|
},
|
|
191
193
|
agentCapabilities: {
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
194
|
+
// Session resume works by passing the ACP sessionId back as
|
|
195
|
+
// `claude --session-id <uuid>` on the next prompt — Claude CLI
|
|
196
|
+
// replays its own JSONL internally. MCP via stdio is not yet
|
|
197
|
+
// propagated from ACP's mcpServers config (the `claude` CLI uses
|
|
198
|
+
// settings.json for that today; bridging is a separate change).
|
|
199
|
+
loadSession: true,
|
|
196
200
|
mcpCapabilities: {},
|
|
197
201
|
promptCapabilities: { embeddedContext: false, image: false, audio: false }
|
|
198
202
|
},
|
|
@@ -201,12 +205,16 @@ function handleInitialize(_params) {
|
|
|
201
205
|
}
|
|
202
206
|
function handleNewSession(params) {
|
|
203
207
|
const sessionId = randomUUID();
|
|
204
|
-
sessions.set(sessionId, {
|
|
205
|
-
claudeSessionId: randomUUID(),
|
|
206
|
-
cwd: params.cwd ?? process.cwd()
|
|
207
|
-
});
|
|
208
|
+
sessions.set(sessionId, { cwd: params.cwd ?? process.cwd() });
|
|
208
209
|
return { sessionId };
|
|
209
210
|
}
|
|
211
|
+
function handleLoadSession(params) {
|
|
212
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
213
|
+
throw new Error("session/load: sessionId required");
|
|
214
|
+
}
|
|
215
|
+
sessions.set(params.sessionId, { cwd: params.cwd ?? process.cwd() });
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
210
218
|
async function handlePrompt(params) {
|
|
211
219
|
const state = sessions.get(params.sessionId);
|
|
212
220
|
if (!state) {
|
|
@@ -219,19 +227,22 @@ async function handlePrompt(params) {
|
|
|
219
227
|
const result = await runClaudeTurn(params.sessionId, state, promptText);
|
|
220
228
|
return { stopReason: result.stopReason };
|
|
221
229
|
}
|
|
222
|
-
var
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
var isMainModule = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("claude-acp.ts") === true || process.argv[1]?.endsWith("claude-acp.js") === true;
|
|
231
|
+
if (isMainModule) {
|
|
232
|
+
const decoder = new FrameDecoder();
|
|
233
|
+
stdin.setEncoding("utf8");
|
|
234
|
+
stdin.on("data", (chunk) => {
|
|
235
|
+
const { messages, malformed } = decoder.feed(chunk);
|
|
236
|
+
for (const line of malformed) {
|
|
237
|
+
stderr.write(`[claude-acp] malformed inbound: ${line}
|
|
228
238
|
`);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
232
|
-
stdin.on("end", () => {
|
|
233
|
-
|
|
234
|
-
});
|
|
239
|
+
}
|
|
240
|
+
for (const msg of messages) void dispatch(msg);
|
|
241
|
+
});
|
|
242
|
+
stdin.on("end", () => {
|
|
243
|
+
exit(0);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
235
246
|
async function dispatch(msg) {
|
|
236
247
|
if (!("id" in msg) || msg.id == null) return;
|
|
237
248
|
if ("result" in msg || "error" in msg) return;
|
|
@@ -244,6 +255,9 @@ async function dispatch(msg) {
|
|
|
244
255
|
case "session/new":
|
|
245
256
|
reply(req.id, handleNewSession(req.params));
|
|
246
257
|
return;
|
|
258
|
+
case "session/load":
|
|
259
|
+
reply(req.id, handleLoadSession(req.params));
|
|
260
|
+
return;
|
|
247
261
|
case "session/prompt": {
|
|
248
262
|
const result = await handlePrompt(req.params);
|
|
249
263
|
reply(req.id, result);
|
|
@@ -258,10 +272,17 @@ async function dispatch(msg) {
|
|
|
258
272
|
}
|
|
259
273
|
} catch (err) {
|
|
260
274
|
const message = err instanceof Error ? err.message : String(err);
|
|
275
|
+
const isParamsError = err instanceof Error && (message.startsWith("session/prompt:") || message.startsWith("session/load:"));
|
|
261
276
|
replyError(
|
|
262
277
|
req.id,
|
|
263
|
-
|
|
278
|
+
isParamsError ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
|
|
264
279
|
message
|
|
265
280
|
);
|
|
266
281
|
}
|
|
267
282
|
}
|
|
283
|
+
export {
|
|
284
|
+
handleInitialize,
|
|
285
|
+
handleLoadSession,
|
|
286
|
+
handleNewSession,
|
|
287
|
+
sessions
|
|
288
|
+
};
|
package/dist/opencara-mcp.js
CHANGED
|
@@ -127,7 +127,8 @@ var AcpSpecSchema = z2.object({
|
|
|
127
127
|
systemPromptMd: z2.string(),
|
|
128
128
|
userPromptMd: z2.string(),
|
|
129
129
|
history: z2.array(AcpHistoryTurnSchema).default([]),
|
|
130
|
-
pageContextJson: z2.string().optional()
|
|
130
|
+
pageContextJson: z2.string().optional(),
|
|
131
|
+
priorSessionId: z2.string().optional()
|
|
131
132
|
});
|
|
132
133
|
var AgentSpecSchema = z2.object({
|
|
133
134
|
kind: z2.string(),
|
|
@@ -226,7 +227,12 @@ var RunDoneSchema = z3.object({
|
|
|
226
227
|
runId: z3.string(),
|
|
227
228
|
status: z3.enum(["succeeded", "failed", "cancelled"]),
|
|
228
229
|
exitCode: z3.number().int().nullable().optional(),
|
|
229
|
-
errorMessage: z3.string().optional()
|
|
230
|
+
errorMessage: z3.string().optional(),
|
|
231
|
+
/** ACP session id the agent ran under (fresh from session/new, or
|
|
232
|
+
* echoed from session/load). The orchestrator persists this per
|
|
233
|
+
* (repo, branch) so the next iteration can resume via session/load.
|
|
234
|
+
* Null/absent for non-ACP runs (worktree-allocate, write-session). */
|
|
235
|
+
acpSessionId: z3.string().nullable().optional()
|
|
230
236
|
});
|
|
231
237
|
var HelloAckSchema = z3.object({
|
|
232
238
|
type: z3.literal("hello-ack"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencara",
|
|
3
|
-
"version": "0.105.
|
|
3
|
+
"version": "0.105.2",
|
|
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": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dev": "tsx watch src/bin.ts",
|
|
39
39
|
"start": "node dist/bin.js",
|
|
40
40
|
"typecheck": "tsc -b",
|
|
41
|
-
"test": "node --import tsx --test --test-reporter=spec src/acp/__tests__/*.test.ts src/mcp/__tests__/*.test.ts src/runner/__tests__/*.test.ts",
|
|
41
|
+
"test": "node --import tsx --test --test-reporter=spec src/acp/__tests__/*.test.ts src/mcp/__tests__/*.test.ts src/runner/__tests__/*.test.ts src/bin/__tests__/*.test.ts src/commands/__tests__/*.test.ts",
|
|
42
42
|
"acp:spike": "tsx src/acp/spike.ts",
|
|
43
43
|
"mcp:smoke": "tsx src/mcp/smoke.ts",
|
|
44
44
|
"clean": "rm -rf dist *.tsbuildinfo"
|