opencara 0.110.0 → 0.112.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 +91 -13
- package/dist/claude-acp.js +77 -7
- package/dist/opencara-mcp.js +18 -2
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -152,6 +152,8 @@ var PairingStatusResponseSchema = z4.union([
|
|
|
152
152
|
var PairingConfirmRequestSchema = z4.object({
|
|
153
153
|
device_name: z4.string().min(1)
|
|
154
154
|
});
|
|
155
|
+
var HOST_PROTOCOL_VERSION = 1;
|
|
156
|
+
var WS_CLOSE_PROTOCOL_TOO_OLD = 4400;
|
|
155
157
|
var SystemInfoSchema = z4.object({
|
|
156
158
|
os: z4.string(),
|
|
157
159
|
// os.platform()
|
|
@@ -181,6 +183,12 @@ var HelloMessageSchema = z4.object({
|
|
|
181
183
|
type: z4.literal("hello"),
|
|
182
184
|
platform: z4.string(),
|
|
183
185
|
version: z4.string(),
|
|
186
|
+
/**
|
|
187
|
+
* HOST_PROTOCOL_VERSION the device speaks. Absent from CLIs published
|
|
188
|
+
* before versioning existed — the server treats absent as 0 and gates
|
|
189
|
+
* on MIN_HOST_PROTOCOL_VERSION.
|
|
190
|
+
*/
|
|
191
|
+
protocolVersion: z4.number().int().nonnegative().optional(),
|
|
184
192
|
capabilities: z4.array(z4.string()).default([]),
|
|
185
193
|
systemInfo: SystemInfoSchema.optional()
|
|
186
194
|
});
|
|
@@ -212,12 +220,22 @@ var RunDoneSchema = z4.object({
|
|
|
212
220
|
var HelloAckSchema = z4.object({
|
|
213
221
|
type: z4.literal("hello-ack"),
|
|
214
222
|
agentHostId: z4.string(),
|
|
215
|
-
deviceName: z4.string()
|
|
223
|
+
deviceName: z4.string(),
|
|
224
|
+
/**
|
|
225
|
+
* Server's HOST_PROTOCOL_VERSION. Optional so old servers (which don't
|
|
226
|
+
* send it) still satisfy new clients' parse; clients use it only to log
|
|
227
|
+
* a version-skew warning, never to gate.
|
|
228
|
+
*/
|
|
229
|
+
protocolVersion: z4.number().int().nonnegative().optional()
|
|
216
230
|
});
|
|
217
231
|
var CancelJobSchema = z4.object({
|
|
218
232
|
type: z4.literal("cancel"),
|
|
219
233
|
runId: z4.string(),
|
|
220
|
-
|
|
234
|
+
// `.catch()` — not a bare enum — so a future server that grows a new
|
|
235
|
+
// reason (e.g. "timeout") degrades on THIS client to "user_stopped"
|
|
236
|
+
// instead of failing the whole frame's parse and silently dropping the
|
|
237
|
+
// cancel, which left the job unkillable on pre-versioning CLIs.
|
|
238
|
+
reason: z4.enum(["user_stopped", "wave_cancelled"]).catch("user_stopped")
|
|
221
239
|
});
|
|
222
240
|
var PingSchema = z4.object({ type: z4.literal("ping") });
|
|
223
241
|
var PongSchema = z4.object({ type: z4.literal("pong") });
|
|
@@ -318,6 +336,13 @@ var DeviceToServerMessageSchema = z4.union([
|
|
|
318
336
|
PongSchema,
|
|
319
337
|
AgentCallRequestSchema
|
|
320
338
|
]);
|
|
339
|
+
var SERVER_TO_DEVICE_TYPES = /* @__PURE__ */ new Set([
|
|
340
|
+
"job",
|
|
341
|
+
"hello-ack",
|
|
342
|
+
"ping",
|
|
343
|
+
"agent-call-result",
|
|
344
|
+
"cancel"
|
|
345
|
+
]);
|
|
321
346
|
var HostRegisterRequestSchema = z4.object({
|
|
322
347
|
hostId: z4.string(),
|
|
323
348
|
hostName: z4.string(),
|
|
@@ -434,6 +459,23 @@ function sleep(ms) {
|
|
|
434
459
|
|
|
435
460
|
// src/transport/ws-client.ts
|
|
436
461
|
import WebSocket from "ws";
|
|
462
|
+
function classifyServerFrame(raw) {
|
|
463
|
+
let json;
|
|
464
|
+
try {
|
|
465
|
+
json = JSON.parse(raw);
|
|
466
|
+
} catch {
|
|
467
|
+
return { kind: "malformed", error: "not JSON" };
|
|
468
|
+
}
|
|
469
|
+
const type = typeof json === "object" && json !== null && "type" in json ? json.type : void 0;
|
|
470
|
+
if (typeof type === "string" && !SERVER_TO_DEVICE_TYPES.has(type)) {
|
|
471
|
+
return { kind: "unknown-type", type };
|
|
472
|
+
}
|
|
473
|
+
const parsed = ServerToDeviceMessageSchema.safeParse(json);
|
|
474
|
+
if (!parsed.success) {
|
|
475
|
+
return { kind: "malformed", error: parsed.error.message };
|
|
476
|
+
}
|
|
477
|
+
return { kind: "ok", msg: parsed.data };
|
|
478
|
+
}
|
|
437
479
|
var HEARTBEAT_MS = 3e4;
|
|
438
480
|
var WsClient = class {
|
|
439
481
|
constructor(opts) {
|
|
@@ -445,6 +487,8 @@ var WsClient = class {
|
|
|
445
487
|
backoff;
|
|
446
488
|
heartbeat = null;
|
|
447
489
|
stopped = false;
|
|
490
|
+
/** Unknown frame types already warned about (once per type, not per frame). */
|
|
491
|
+
warnedUnknownTypes = /* @__PURE__ */ new Set();
|
|
448
492
|
start() {
|
|
449
493
|
this.connect();
|
|
450
494
|
}
|
|
@@ -473,18 +517,28 @@ var WsClient = class {
|
|
|
473
517
|
this.opts.onOpen?.();
|
|
474
518
|
});
|
|
475
519
|
ws.on("message", (raw) => {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
520
|
+
const frame = classifyServerFrame(raw.toString());
|
|
521
|
+
if (frame.kind === "unknown-type") {
|
|
522
|
+
if (!this.warnedUnknownTypes.has(frame.type)) {
|
|
523
|
+
this.warnedUnknownTypes.add(frame.type);
|
|
524
|
+
console.warn(
|
|
525
|
+
`[ws] ignoring unknown frame type "${frame.type}" \u2014 the server is likely newer than this CLI; consider \`npm i -g opencara\``
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (frame.kind === "malformed") {
|
|
531
|
+
console.error("[ws] invalid frame:", frame.error);
|
|
481
532
|
return;
|
|
482
533
|
}
|
|
483
|
-
this.opts.onMessage(
|
|
534
|
+
this.opts.onMessage(frame.msg);
|
|
484
535
|
});
|
|
485
536
|
ws.on("close", (code, reasonBuf) => {
|
|
486
537
|
const reason = reasonBuf.toString();
|
|
487
538
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
539
|
+
if (code === WS_CLOSE_PROTOCOL_TOO_OLD) {
|
|
540
|
+
this.stopped = true;
|
|
541
|
+
}
|
|
488
542
|
this.opts.onClose?.(code, reason);
|
|
489
543
|
if (!this.stopped) this.scheduleReconnect();
|
|
490
544
|
});
|
|
@@ -1302,11 +1356,21 @@ async function selectAcpModel(client, sessionId, requested, configOptions, onLog
|
|
|
1302
1356
|
const values = (modelOption.options ?? []).map((o) => o.value);
|
|
1303
1357
|
const target = matchModelValue(requested, values);
|
|
1304
1358
|
if (!target) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1359
|
+
try {
|
|
1360
|
+
await client.setConfigOption({
|
|
1361
|
+
sessionId,
|
|
1362
|
+
configId: modelOption.id,
|
|
1363
|
+
value: requested.trim()
|
|
1364
|
+
});
|
|
1365
|
+
onLog("stderr", `[acp] selected model ${requested.trim()} (freeform)
|
|
1366
|
+
`);
|
|
1367
|
+
} catch {
|
|
1368
|
+
onLog(
|
|
1369
|
+
"stderr",
|
|
1370
|
+
`[acp] model "${requested}" not among available models [${values.join(", ")}]; using the default
|
|
1308
1371
|
`
|
|
1309
|
-
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1310
1374
|
return;
|
|
1311
1375
|
}
|
|
1312
1376
|
if (modelOption.currentValue === target) return;
|
|
@@ -1402,7 +1466,7 @@ function resolveLocalAcpAdapter(command, args) {
|
|
|
1402
1466
|
}
|
|
1403
1467
|
|
|
1404
1468
|
// src/commands/run.ts
|
|
1405
|
-
var PKG_VERSION = "0.
|
|
1469
|
+
var PKG_VERSION = "0.112.1";
|
|
1406
1470
|
var LOG_FLUSH_MS = 800;
|
|
1407
1471
|
var MAX_CHUNK_SIZE = 4 * 1024;
|
|
1408
1472
|
async function run(opts = {}) {
|
|
@@ -1422,6 +1486,7 @@ async function run(opts = {}) {
|
|
|
1422
1486
|
type: "hello",
|
|
1423
1487
|
platform: platform(),
|
|
1424
1488
|
version: PKG_VERSION,
|
|
1489
|
+
protocolVersion: HOST_PROTOCOL_VERSION,
|
|
1425
1490
|
// Advertise ACP transport support. Pre-v0.30 devices reported
|
|
1426
1491
|
// "agent-call" (the fenced-stdout-block protocol); since the
|
|
1427
1492
|
// legacy path was removed, this version reports "acp" so the
|
|
@@ -1432,6 +1497,14 @@ async function run(opts = {}) {
|
|
|
1432
1497
|
},
|
|
1433
1498
|
onMessage: (msg) => handleServerMessage(msg, client, cfg),
|
|
1434
1499
|
onClose: (code, reason) => {
|
|
1500
|
+
if (code === WS_CLOSE_PROTOCOL_TOO_OLD) {
|
|
1501
|
+
console.error(
|
|
1502
|
+
`[opencara] this CLI's protocol is too old for the orchestrator: ${reason}`
|
|
1503
|
+
);
|
|
1504
|
+
console.error("[opencara] upgrade with: npm i -g opencara");
|
|
1505
|
+
process.exitCode = 1;
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1435
1508
|
console.log(`[opencara] disconnected (code=${code} reason="${reason}")`);
|
|
1436
1509
|
}
|
|
1437
1510
|
});
|
|
@@ -1442,6 +1515,11 @@ var acpControllers = /* @__PURE__ */ new Map();
|
|
|
1442
1515
|
function handleServerMessage(msg, client, _cfg) {
|
|
1443
1516
|
if (msg.type === "hello-ack") {
|
|
1444
1517
|
console.log(`[opencara] acked as ${msg.deviceName} (${msg.agentHostId})`);
|
|
1518
|
+
if (msg.protocolVersion !== void 0 && msg.protocolVersion !== HOST_PROTOCOL_VERSION) {
|
|
1519
|
+
console.warn(
|
|
1520
|
+
`[opencara] protocol skew: server v${msg.protocolVersion}, CLI v${HOST_PROTOCOL_VERSION}` + (msg.protocolVersion > HOST_PROTOCOL_VERSION ? " \u2014 consider `npm i -g opencara`" : "")
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1445
1523
|
return;
|
|
1446
1524
|
}
|
|
1447
1525
|
if (msg.type === "ping") return;
|
package/dist/claude-acp.js
CHANGED
|
@@ -102,6 +102,57 @@ function buildClaudeMcpConfig(servers) {
|
|
|
102
102
|
}
|
|
103
103
|
var sessions = /* @__PURE__ */ new Map();
|
|
104
104
|
var extraClaudeArgs = [];
|
|
105
|
+
function _setExtraClaudeArgsForTest(args) {
|
|
106
|
+
extraClaudeArgs = args;
|
|
107
|
+
}
|
|
108
|
+
function parseModelFromArgs(args) {
|
|
109
|
+
let model;
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
const a = args[i];
|
|
112
|
+
const eq = /^(?:--model|-m)=(.*)$/.exec(a);
|
|
113
|
+
if (eq && eq[1]) {
|
|
114
|
+
model = eq[1];
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if ((a === "--model" || a === "-m") && args[i + 1] !== void 0) {
|
|
118
|
+
model = args[i + 1];
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return model;
|
|
123
|
+
}
|
|
124
|
+
function modelConfigOptions(state) {
|
|
125
|
+
const effective = state.modelOverride ?? parseModelFromArgs(extraClaudeArgs);
|
|
126
|
+
if (!effective) return void 0;
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
type: "select",
|
|
130
|
+
id: "model",
|
|
131
|
+
category: "model",
|
|
132
|
+
name: "Model",
|
|
133
|
+
currentValue: effective,
|
|
134
|
+
options: [{ value: effective }]
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
function handleSetConfigOption(params) {
|
|
139
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
|
|
140
|
+
const state = sessions.get(sessionId);
|
|
141
|
+
if (!state) {
|
|
142
|
+
throw new Error(`session/set_config_option: unknown session '${sessionId}'`);
|
|
143
|
+
}
|
|
144
|
+
if (params.configId !== "model") {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`session/set_config_option: unknown configId '${String(params.configId)}'`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const value = typeof params.value === "string" ? params.value.trim() : "";
|
|
150
|
+
if (!value) {
|
|
151
|
+
throw new Error("session/set_config_option: value must be a non-empty string");
|
|
152
|
+
}
|
|
153
|
+
state.modelOverride = value;
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
105
156
|
var INSTRUCTIONS_FILE_MAX_BYTES = 64 * 1024;
|
|
106
157
|
function resolveInstructionsFile(cwd, relative) {
|
|
107
158
|
if (!relative || typeof relative !== "string" || relative.length === 0) {
|
|
@@ -260,6 +311,9 @@ async function runClaudeTurn(sessionId, state, promptText, images, permissionMod
|
|
|
260
311
|
if (extraClaudeArgs.length > 0) {
|
|
261
312
|
args.push(...extraClaudeArgs);
|
|
262
313
|
}
|
|
314
|
+
if (state.modelOverride) {
|
|
315
|
+
args.push("--model", state.modelOverride);
|
|
316
|
+
}
|
|
263
317
|
const child = spawn("claude", args, {
|
|
264
318
|
cwd: state.cwd,
|
|
265
319
|
env: process.env,
|
|
@@ -486,13 +540,15 @@ function handleNewSession(params) {
|
|
|
486
540
|
const sessionId = randomUUID();
|
|
487
541
|
const mcpServers = normalizeMcpServers(params.mcpServers);
|
|
488
542
|
const instructionsFile = normalizeInstructionsFile(params.instructionsFile);
|
|
489
|
-
|
|
543
|
+
const state = {
|
|
490
544
|
cwd: params.cwd ?? process.cwd(),
|
|
491
545
|
resume: false,
|
|
492
546
|
...mcpServers.length > 0 ? { mcpServers } : {},
|
|
493
547
|
...instructionsFile ? { instructionsFile } : {}
|
|
494
|
-
}
|
|
495
|
-
|
|
548
|
+
};
|
|
549
|
+
sessions.set(sessionId, state);
|
|
550
|
+
const configOptions = modelConfigOptions(state);
|
|
551
|
+
return { sessionId, ...configOptions ? { configOptions } : {} };
|
|
496
552
|
}
|
|
497
553
|
function handleLoadSession(params) {
|
|
498
554
|
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
@@ -500,13 +556,15 @@ function handleLoadSession(params) {
|
|
|
500
556
|
}
|
|
501
557
|
const mcpServers = normalizeMcpServers(params.mcpServers);
|
|
502
558
|
const instructionsFile = normalizeInstructionsFile(params.instructionsFile);
|
|
503
|
-
|
|
559
|
+
const state = {
|
|
504
560
|
cwd: params.cwd ?? process.cwd(),
|
|
505
561
|
resume: true,
|
|
506
562
|
...mcpServers.length > 0 ? { mcpServers } : {},
|
|
507
563
|
...instructionsFile ? { instructionsFile } : {}
|
|
508
|
-
}
|
|
509
|
-
|
|
564
|
+
};
|
|
565
|
+
sessions.set(params.sessionId, state);
|
|
566
|
+
const configOptions = modelConfigOptions(state);
|
|
567
|
+
return configOptions ? { configOptions } : {};
|
|
510
568
|
}
|
|
511
569
|
function normalizeInstructionsFile(raw) {
|
|
512
570
|
if (typeof raw !== "string") return void 0;
|
|
@@ -591,6 +649,14 @@ async function dispatch(msg) {
|
|
|
591
649
|
case "session/load":
|
|
592
650
|
reply(req.id, handleLoadSession(req.params));
|
|
593
651
|
return;
|
|
652
|
+
case "session/set_config_option":
|
|
653
|
+
reply(
|
|
654
|
+
req.id,
|
|
655
|
+
handleSetConfigOption(
|
|
656
|
+
req.params
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
594
660
|
case "session/prompt": {
|
|
595
661
|
const result = await handlePrompt(req.params);
|
|
596
662
|
reply(req.id, result);
|
|
@@ -605,7 +671,7 @@ async function dispatch(msg) {
|
|
|
605
671
|
}
|
|
606
672
|
} catch (err) {
|
|
607
673
|
const message = err instanceof Error ? err.message : String(err);
|
|
608
|
-
const isParamsError = err instanceof Error && (message.startsWith("session/prompt:") || message.startsWith("session/load:"));
|
|
674
|
+
const isParamsError = err instanceof Error && (message.startsWith("session/prompt:") || message.startsWith("session/load:") || message.startsWith("session/set_config_option:"));
|
|
609
675
|
replyError(
|
|
610
676
|
req.id,
|
|
611
677
|
isParamsError ? JSON_RPC_ERROR_INVALID_PARAMS : JSON_RPC_ERROR_INTERNAL,
|
|
@@ -614,11 +680,15 @@ async function dispatch(msg) {
|
|
|
614
680
|
}
|
|
615
681
|
}
|
|
616
682
|
export {
|
|
683
|
+
_setExtraClaudeArgsForTest,
|
|
617
684
|
buildStreamJsonInput,
|
|
618
685
|
extraClaudeArgs,
|
|
619
686
|
handleInitialize,
|
|
620
687
|
handleLoadSession,
|
|
621
688
|
handleNewSession,
|
|
689
|
+
handleSetConfigOption,
|
|
690
|
+
modelConfigOptions,
|
|
691
|
+
parseModelFromArgs,
|
|
622
692
|
resolveInstructionsFile,
|
|
623
693
|
sessions,
|
|
624
694
|
translateClaudeEvent
|
package/dist/opencara-mcp.js
CHANGED
|
@@ -222,6 +222,12 @@ var HelloMessageSchema = z3.object({
|
|
|
222
222
|
type: z3.literal("hello"),
|
|
223
223
|
platform: z3.string(),
|
|
224
224
|
version: z3.string(),
|
|
225
|
+
/**
|
|
226
|
+
* HOST_PROTOCOL_VERSION the device speaks. Absent from CLIs published
|
|
227
|
+
* before versioning existed — the server treats absent as 0 and gates
|
|
228
|
+
* on MIN_HOST_PROTOCOL_VERSION.
|
|
229
|
+
*/
|
|
230
|
+
protocolVersion: z3.number().int().nonnegative().optional(),
|
|
225
231
|
capabilities: z3.array(z3.string()).default([]),
|
|
226
232
|
systemInfo: SystemInfoSchema.optional()
|
|
227
233
|
});
|
|
@@ -253,12 +259,22 @@ var RunDoneSchema = z3.object({
|
|
|
253
259
|
var HelloAckSchema = z3.object({
|
|
254
260
|
type: z3.literal("hello-ack"),
|
|
255
261
|
agentHostId: z3.string(),
|
|
256
|
-
deviceName: z3.string()
|
|
262
|
+
deviceName: z3.string(),
|
|
263
|
+
/**
|
|
264
|
+
* Server's HOST_PROTOCOL_VERSION. Optional so old servers (which don't
|
|
265
|
+
* send it) still satisfy new clients' parse; clients use it only to log
|
|
266
|
+
* a version-skew warning, never to gate.
|
|
267
|
+
*/
|
|
268
|
+
protocolVersion: z3.number().int().nonnegative().optional()
|
|
257
269
|
});
|
|
258
270
|
var CancelJobSchema = z3.object({
|
|
259
271
|
type: z3.literal("cancel"),
|
|
260
272
|
runId: z3.string(),
|
|
261
|
-
|
|
273
|
+
// `.catch()` — not a bare enum — so a future server that grows a new
|
|
274
|
+
// reason (e.g. "timeout") degrades on THIS client to "user_stopped"
|
|
275
|
+
// instead of failing the whole frame's parse and silently dropping the
|
|
276
|
+
// cancel, which left the job unkillable on pre-versioning CLIs.
|
|
277
|
+
reason: z3.enum(["user_stopped", "wave_cancelled"]).catch("user_stopped")
|
|
262
278
|
});
|
|
263
279
|
var PingSchema = z3.object({ type: z3.literal("ping") });
|
|
264
280
|
var PongSchema = z3.object({ type: z3.literal("pong") });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencara",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.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": {
|
|
@@ -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 src/bin/__tests__/*.test.ts src/commands/__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 src/transport/__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"
|