opencara 0.108.2 → 0.110.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 +119 -11
- package/dist/claude-acp.js +278 -33
- package/dist/opencara-mcp.js +13 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -82,6 +82,12 @@ var AcpHistoryTurnSchema = z3.object({
|
|
|
82
82
|
role: z3.enum(["user", "assistant"]),
|
|
83
83
|
text: z3.string()
|
|
84
84
|
});
|
|
85
|
+
var AcpImageInputSchema = z3.object({
|
|
86
|
+
/** Base64-encoded image bytes, without the `data:<mime>;base64,` prefix. */
|
|
87
|
+
data: z3.string(),
|
|
88
|
+
/** IANA image MIME type, e.g. `image/png`, `image/jpeg`, `image/webp`. */
|
|
89
|
+
mimeType: z3.string()
|
|
90
|
+
});
|
|
85
91
|
var AcpPermissionModeSchema = z3.enum([
|
|
86
92
|
"default",
|
|
87
93
|
"acceptEdits",
|
|
@@ -94,7 +100,10 @@ var AcpSpecSchema = z3.object({
|
|
|
94
100
|
history: z3.array(AcpHistoryTurnSchema).default([]),
|
|
95
101
|
pageContextJson: z3.string().optional(),
|
|
96
102
|
priorSessionId: z3.string().optional(),
|
|
97
|
-
permissionMode: AcpPermissionModeSchema.optional()
|
|
103
|
+
permissionMode: AcpPermissionModeSchema.optional(),
|
|
104
|
+
instructionsFile: z3.string().optional(),
|
|
105
|
+
images: z3.array(AcpImageInputSchema).default([]),
|
|
106
|
+
model: z3.string().optional()
|
|
98
107
|
});
|
|
99
108
|
var AgentSpecSchema = z3.object({
|
|
100
109
|
kind: z3.string(),
|
|
@@ -350,6 +359,9 @@ var IssueDetailSchema = IssueSummarySchema.extend({
|
|
|
350
359
|
draftUpdatedAt: z5.string().datetime().nullable()
|
|
351
360
|
});
|
|
352
361
|
|
|
362
|
+
// ../shared/dist/cron.js
|
|
363
|
+
var MAX_STEP_MINUTES = 366 * 24 * 60 + 24 * 60;
|
|
364
|
+
|
|
353
365
|
// src/commands/register.ts
|
|
354
366
|
var POLL_INTERVAL_MS = 2e3;
|
|
355
367
|
async function register(opts = {}) {
|
|
@@ -546,6 +558,7 @@ var ACP_METHODS = {
|
|
|
546
558
|
session_load: "session/load",
|
|
547
559
|
session_prompt: "session/prompt",
|
|
548
560
|
session_cancel: "session/cancel",
|
|
561
|
+
session_set_config_option: "session/set_config_option",
|
|
549
562
|
// Client methods (agent → client) — handled minimally in this spike.
|
|
550
563
|
session_update: "session/update",
|
|
551
564
|
session_request_permission: "session/request_permission",
|
|
@@ -601,6 +614,15 @@ var AcpConnection = class {
|
|
|
601
614
|
prompt(req) {
|
|
602
615
|
return this.request(ACP_METHODS.session_prompt, req);
|
|
603
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Select a session config option (e.g. the model) advertised by the agent in
|
|
619
|
+
* the session/new response. pi-acp maps `configId: "model"` onto its model
|
|
620
|
+
* selector. Resolves on success; rejects with the agent's JSON-RPC error
|
|
621
|
+
* (e.g. "Model not found") — callers log and continue rather than fail the run.
|
|
622
|
+
*/
|
|
623
|
+
setConfigOption(req) {
|
|
624
|
+
return this.request(ACP_METHODS.session_set_config_option, req);
|
|
625
|
+
}
|
|
604
626
|
/** Notification (no reply). Used to cancel a turn mid-flight. */
|
|
605
627
|
cancel(sessionId) {
|
|
606
628
|
this.notify(ACP_METHODS.session_cancel, { sessionId });
|
|
@@ -765,6 +787,9 @@ var AcpClient = class {
|
|
|
765
787
|
prompt(req) {
|
|
766
788
|
return this.must().prompt(req);
|
|
767
789
|
}
|
|
790
|
+
setConfigOption(req) {
|
|
791
|
+
return this.must().setConfigOption(req);
|
|
792
|
+
}
|
|
768
793
|
cancel(sessionId) {
|
|
769
794
|
this.must().cancel(sessionId);
|
|
770
795
|
}
|
|
@@ -1152,21 +1177,35 @@ function runAcpJob(opts) {
|
|
|
1152
1177
|
const cwd = spec.cwd ?? process.cwd();
|
|
1153
1178
|
const mcpServers = [host.acpServerEntry()];
|
|
1154
1179
|
const shimSupportsLoad = initResult.agentCapabilities?.loadSession === true;
|
|
1180
|
+
const instructionsExtra = acpSpec.instructionsFile ? { instructionsFile: acpSpec.instructionsFile } : {};
|
|
1155
1181
|
let sessionId;
|
|
1156
1182
|
let resumed = false;
|
|
1183
|
+
let configOptions;
|
|
1157
1184
|
if (acpSpec.priorSessionId && shimSupportsLoad) {
|
|
1158
|
-
await client.loadSession({
|
|
1185
|
+
const loaded = await client.loadSession({
|
|
1159
1186
|
sessionId: acpSpec.priorSessionId,
|
|
1160
1187
|
cwd,
|
|
1161
|
-
mcpServers
|
|
1188
|
+
mcpServers,
|
|
1189
|
+
...instructionsExtra
|
|
1162
1190
|
});
|
|
1163
1191
|
sessionId = acpSpec.priorSessionId;
|
|
1164
1192
|
resumed = true;
|
|
1193
|
+
configOptions = loaded.configOptions;
|
|
1165
1194
|
} else {
|
|
1166
|
-
const session = await client.newSession({ cwd, mcpServers });
|
|
1195
|
+
const session = await client.newSession({ cwd, mcpServers, ...instructionsExtra });
|
|
1167
1196
|
sessionId = session.sessionId;
|
|
1197
|
+
configOptions = session.configOptions;
|
|
1168
1198
|
}
|
|
1169
1199
|
activeSessionId = sessionId;
|
|
1200
|
+
if (acpSpec.model) {
|
|
1201
|
+
await selectAcpModel(
|
|
1202
|
+
client,
|
|
1203
|
+
sessionId,
|
|
1204
|
+
acpSpec.model,
|
|
1205
|
+
configOptions,
|
|
1206
|
+
handlers.onLog
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1170
1209
|
if (cancelRequested) {
|
|
1171
1210
|
try {
|
|
1172
1211
|
client.cancel(sessionId);
|
|
@@ -1232,7 +1271,58 @@ ${turns}`);
|
|
|
1232
1271
|
parts.push(`# Current message
|
|
1233
1272
|
|
|
1234
1273
|
${acp.userPromptMd}`);
|
|
1235
|
-
|
|
1274
|
+
const blocks = [{ type: "text", text: parts.join("\n\n---\n\n") }];
|
|
1275
|
+
for (const img of acp.images ?? []) {
|
|
1276
|
+
blocks.push({ type: "image", data: img.data, mimeType: img.mimeType });
|
|
1277
|
+
}
|
|
1278
|
+
return blocks;
|
|
1279
|
+
}
|
|
1280
|
+
function matchModelValue(requested, values) {
|
|
1281
|
+
const want = requested.trim();
|
|
1282
|
+
if (!want) return void 0;
|
|
1283
|
+
const exact = values.find((v) => v === want);
|
|
1284
|
+
if (exact) return exact;
|
|
1285
|
+
const ci = values.find((v) => v.toLowerCase() === want.toLowerCase());
|
|
1286
|
+
if (ci) return ci;
|
|
1287
|
+
const suffix = `/${want.toLowerCase()}`;
|
|
1288
|
+
return values.find((v) => v.toLowerCase().endsWith(suffix));
|
|
1289
|
+
}
|
|
1290
|
+
async function selectAcpModel(client, sessionId, requested, configOptions, onLog) {
|
|
1291
|
+
const modelOption = configOptions?.find(
|
|
1292
|
+
(o) => o.id === "model" || o.category === "model"
|
|
1293
|
+
);
|
|
1294
|
+
if (!modelOption) {
|
|
1295
|
+
onLog(
|
|
1296
|
+
"stderr",
|
|
1297
|
+
`[acp] model "${requested}" requested but the agent advertised no model option; using its default
|
|
1298
|
+
`
|
|
1299
|
+
);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const values = (modelOption.options ?? []).map((o) => o.value);
|
|
1303
|
+
const target = matchModelValue(requested, values);
|
|
1304
|
+
if (!target) {
|
|
1305
|
+
onLog(
|
|
1306
|
+
"stderr",
|
|
1307
|
+
`[acp] model "${requested}" not among available models [${values.join(", ")}]; using the default
|
|
1308
|
+
`
|
|
1309
|
+
);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (modelOption.currentValue === target) return;
|
|
1313
|
+
try {
|
|
1314
|
+
await client.setConfigOption({
|
|
1315
|
+
sessionId,
|
|
1316
|
+
configId: modelOption.id,
|
|
1317
|
+
value: target
|
|
1318
|
+
});
|
|
1319
|
+
onLog("stderr", `[acp] selected model ${target}
|
|
1320
|
+
`);
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1323
|
+
onLog("stderr", `[acp] model selection failed (${msg}); using the default
|
|
1324
|
+
`);
|
|
1325
|
+
}
|
|
1236
1326
|
}
|
|
1237
1327
|
function createUpdateTranslator(onLog) {
|
|
1238
1328
|
let inThought = false;
|
|
@@ -1312,7 +1402,7 @@ function resolveLocalAcpAdapter(command, args) {
|
|
|
1312
1402
|
}
|
|
1313
1403
|
|
|
1314
1404
|
// src/commands/run.ts
|
|
1315
|
-
var PKG_VERSION = "0.
|
|
1405
|
+
var PKG_VERSION = "0.110.0";
|
|
1316
1406
|
var LOG_FLUSH_MS = 800;
|
|
1317
1407
|
var MAX_CHUNK_SIZE = 4 * 1024;
|
|
1318
1408
|
async function run(opts = {}) {
|
|
@@ -1599,7 +1689,7 @@ import {
|
|
|
1599
1689
|
symlinkSync
|
|
1600
1690
|
} from "node:fs";
|
|
1601
1691
|
import { homedir as homedir2 } from "node:os";
|
|
1602
|
-
import { join as join2, sep } from "node:path";
|
|
1692
|
+
import { dirname as dirname3, join as join2, sep } from "node:path";
|
|
1603
1693
|
var OPENCARA_ROOT = join2(homedir2(), ".opencara");
|
|
1604
1694
|
var WORK_ROOT = join2(OPENCARA_ROOT, "work");
|
|
1605
1695
|
var SESSION_ROOT = join2(OPENCARA_ROOT, "sessions");
|
|
@@ -1662,17 +1752,24 @@ function worktreeCreate(args) {
|
|
|
1662
1752
|
const cleanUrl = `https://github.com/${repo}.git`;
|
|
1663
1753
|
mkdirSync2(sessionDir, { recursive: true });
|
|
1664
1754
|
if (cacheDir) {
|
|
1755
|
+
const cacheLockPath = `${cacheDir}.lock`;
|
|
1756
|
+
mkdirSync2(dirname3(cacheLockPath), { recursive: true });
|
|
1665
1757
|
if (existsSync4(join2(cacheDir, ".git"))) {
|
|
1666
|
-
|
|
1758
|
+
gitLocked(cacheDir, ["fetch", "--all", "--prune"], cacheLockPath, gitEnv);
|
|
1667
1759
|
} else {
|
|
1668
1760
|
mkdirSync2(cacheDir, { recursive: true });
|
|
1669
1761
|
try {
|
|
1670
|
-
|
|
1762
|
+
gitLocked(
|
|
1671
1763
|
cacheDir,
|
|
1672
1764
|
["-c", `credential.helper=${HELPER_SNIPPET}`, "clone", cleanUrl, "."],
|
|
1765
|
+
cacheLockPath,
|
|
1673
1766
|
gitEnv
|
|
1674
1767
|
);
|
|
1675
|
-
|
|
1768
|
+
gitLocked(
|
|
1769
|
+
cacheDir,
|
|
1770
|
+
["config", "credential.helper", HELPER_SNIPPET],
|
|
1771
|
+
cacheLockPath
|
|
1772
|
+
);
|
|
1676
1773
|
} catch (err) {
|
|
1677
1774
|
try {
|
|
1678
1775
|
if (!existsSync4(join2(cacheDir, ".git", "HEAD"))) {
|
|
@@ -1684,7 +1781,7 @@ function worktreeCreate(args) {
|
|
|
1684
1781
|
}
|
|
1685
1782
|
}
|
|
1686
1783
|
if (useLfs) {
|
|
1687
|
-
|
|
1784
|
+
gitLocked(cacheDir, ["lfs", "fetch", "--all"], cacheLockPath, gitEnv);
|
|
1688
1785
|
mkdirSync2(join2(cacheDir, ".git", "lfs", "objects"), { recursive: true });
|
|
1689
1786
|
}
|
|
1690
1787
|
}
|
|
@@ -1831,6 +1928,17 @@ function git(cwd, args, env) {
|
|
|
1831
1928
|
env: env ?? process.env
|
|
1832
1929
|
});
|
|
1833
1930
|
}
|
|
1931
|
+
function gitLocked(cwd, args, lockPath, env) {
|
|
1932
|
+
execFileSync(
|
|
1933
|
+
"flock",
|
|
1934
|
+
["--exclusive", lockPath, "git", ...args],
|
|
1935
|
+
{
|
|
1936
|
+
cwd,
|
|
1937
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
1938
|
+
env: env ?? process.env
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1834
1942
|
function refExists(cwd, ref) {
|
|
1835
1943
|
try {
|
|
1836
1944
|
execFileSync("git", ["rev-parse", "--verify", "--quiet", ref], {
|
package/dist/claude-acp.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// src/bin/claude-acp.ts
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
7
|
+
import { isAbsolute, normalize, resolve as pathResolve, sep } from "node:path";
|
|
6
8
|
import { stdin, stdout, stderr, exit } from "node:process";
|
|
7
9
|
|
|
8
10
|
// src/acp/framing.ts
|
|
@@ -99,7 +101,125 @@ function buildClaudeMcpConfig(servers) {
|
|
|
99
101
|
return JSON.stringify({ mcpServers });
|
|
100
102
|
}
|
|
101
103
|
var sessions = /* @__PURE__ */ new Map();
|
|
102
|
-
|
|
104
|
+
var extraClaudeArgs = [];
|
|
105
|
+
var INSTRUCTIONS_FILE_MAX_BYTES = 64 * 1024;
|
|
106
|
+
function resolveInstructionsFile(cwd, relative) {
|
|
107
|
+
if (!relative || typeof relative !== "string" || relative.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (!isAbsolute(cwd)) {
|
|
111
|
+
stderr.write(
|
|
112
|
+
`[claude-acp] instructionsFile skipped: cwd '${cwd}' is not absolute
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (isAbsolute(relative) || /^[A-Za-z]:[\\/]/.test(relative)) {
|
|
118
|
+
stderr.write(
|
|
119
|
+
`[claude-acp] instructionsFile skipped: '${relative}' must be repo-relative
|
|
120
|
+
`
|
|
121
|
+
);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (relative.split(/[\\/]/).includes("..")) {
|
|
125
|
+
stderr.write(
|
|
126
|
+
`[claude-acp] instructionsFile skipped: '${relative}' contains '..'
|
|
127
|
+
`
|
|
128
|
+
);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const normalizedCwd = normalize(cwd);
|
|
132
|
+
const candidate = pathResolve(normalizedCwd, relative);
|
|
133
|
+
const cwdWithSep = normalizedCwd.endsWith(sep) ? normalizedCwd : `${normalizedCwd}${sep}`;
|
|
134
|
+
if (!candidate.startsWith(cwdWithSep) && candidate !== normalizedCwd) {
|
|
135
|
+
stderr.write(
|
|
136
|
+
`[claude-acp] instructionsFile skipped: '${candidate}' escapes cwd
|
|
137
|
+
`
|
|
138
|
+
);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let realCwd;
|
|
142
|
+
let realCandidate;
|
|
143
|
+
try {
|
|
144
|
+
realCwd = realpathSync(normalizedCwd);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
stderr.write(
|
|
147
|
+
`[claude-acp] instructionsFile skipped: cwd '${normalizedCwd}' realpath failed: ${err instanceof Error ? err.message : String(err)}
|
|
148
|
+
`
|
|
149
|
+
);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
realCandidate = realpathSync(candidate);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
stderr.write(
|
|
156
|
+
`[claude-acp] instructionsFile skipped: '${relative}' not found in cwd (${err instanceof Error ? err.message : String(err)})
|
|
157
|
+
`
|
|
158
|
+
);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const realCwdWithSep = realCwd.endsWith(sep) ? realCwd : `${realCwd}${sep}`;
|
|
162
|
+
if (!realCandidate.startsWith(realCwdWithSep) && realCandidate !== realCwd) {
|
|
163
|
+
stderr.write(
|
|
164
|
+
`[claude-acp] instructionsFile skipped: '${relative}' resolves to '${realCandidate}' which is outside cwd (symlink escape?)
|
|
165
|
+
`
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
let stat;
|
|
170
|
+
try {
|
|
171
|
+
stat = statSync(realCandidate);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
stderr.write(
|
|
174
|
+
`[claude-acp] instructionsFile skipped: '${relative}' stat failed: ${err instanceof Error ? err.message : String(err)}
|
|
175
|
+
`
|
|
176
|
+
);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!stat.isFile()) {
|
|
180
|
+
stderr.write(
|
|
181
|
+
`[claude-acp] instructionsFile skipped: '${relative}' is not a regular file
|
|
182
|
+
`
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (stat.size > INSTRUCTIONS_FILE_MAX_BYTES) {
|
|
187
|
+
stderr.write(
|
|
188
|
+
`[claude-acp] instructionsFile skipped: '${relative}' is ${stat.size} bytes (> ${INSTRUCTIONS_FILE_MAX_BYTES} cap)
|
|
189
|
+
`
|
|
190
|
+
);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
let content;
|
|
194
|
+
try {
|
|
195
|
+
content = readFileSync(realCandidate, "utf8");
|
|
196
|
+
} catch (err) {
|
|
197
|
+
stderr.write(
|
|
198
|
+
`[claude-acp] instructionsFile skipped: read failed for '${relative}': ${err instanceof Error ? err.message : String(err)}
|
|
199
|
+
`
|
|
200
|
+
);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return { path: realCandidate, content };
|
|
204
|
+
}
|
|
205
|
+
function buildStreamJsonInput(promptText, images) {
|
|
206
|
+
const content = [];
|
|
207
|
+
if (promptText.length > 0) {
|
|
208
|
+
content.push({ type: "text", text: promptText });
|
|
209
|
+
}
|
|
210
|
+
for (const img of images) {
|
|
211
|
+
content.push({
|
|
212
|
+
type: "image",
|
|
213
|
+
source: { type: "base64", media_type: img.mimeType, data: img.data }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return `${JSON.stringify({
|
|
217
|
+
type: "user",
|
|
218
|
+
message: { role: "user", content }
|
|
219
|
+
})}
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
async function runClaudeTurn(sessionId, state, promptText, images, permissionMode) {
|
|
103
223
|
return new Promise((resolve, reject) => {
|
|
104
224
|
const idFlag = state.resume ? "--resume" : "--session-id";
|
|
105
225
|
const args = [
|
|
@@ -114,6 +234,10 @@ async function runClaudeTurn(sessionId, state, promptText, permissionMode) {
|
|
|
114
234
|
idFlag,
|
|
115
235
|
sessionId
|
|
116
236
|
];
|
|
237
|
+
const useStreamJsonInput = images.length > 0;
|
|
238
|
+
if (useStreamJsonInput) {
|
|
239
|
+
args.push("--input-format", "stream-json");
|
|
240
|
+
}
|
|
117
241
|
if (permissionMode && permissionMode !== "default") {
|
|
118
242
|
args.push("--permission-mode", permissionMode);
|
|
119
243
|
} else {
|
|
@@ -126,6 +250,16 @@ async function runClaudeTurn(sessionId, state, promptText, permissionMode) {
|
|
|
126
250
|
"--strict-mcp-config"
|
|
127
251
|
);
|
|
128
252
|
}
|
|
253
|
+
const resolvedInstructions = resolveInstructionsFile(
|
|
254
|
+
state.cwd,
|
|
255
|
+
state.instructionsFile
|
|
256
|
+
);
|
|
257
|
+
if (resolvedInstructions) {
|
|
258
|
+
args.push("--bare", "--append-system-prompt", resolvedInstructions.content);
|
|
259
|
+
}
|
|
260
|
+
if (extraClaudeArgs.length > 0) {
|
|
261
|
+
args.push(...extraClaudeArgs);
|
|
262
|
+
}
|
|
129
263
|
const child = spawn("claude", args, {
|
|
130
264
|
cwd: state.cwd,
|
|
131
265
|
env: process.env,
|
|
@@ -134,7 +268,9 @@ async function runClaudeTurn(sessionId, state, promptText, permissionMode) {
|
|
|
134
268
|
state.activeChild = child;
|
|
135
269
|
child.stdin.on("error", () => {
|
|
136
270
|
});
|
|
137
|
-
child.stdin.end(
|
|
271
|
+
child.stdin.end(
|
|
272
|
+
useStreamJsonInput ? buildStreamJsonInput(promptText, images) : promptText
|
|
273
|
+
);
|
|
138
274
|
const decoder = new FrameDecoder();
|
|
139
275
|
let resolved = false;
|
|
140
276
|
let stopReason = "end_turn";
|
|
@@ -182,27 +318,43 @@ async function runClaudeTurn(sessionId, state, promptText, permissionMode) {
|
|
|
182
318
|
});
|
|
183
319
|
});
|
|
184
320
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
321
|
+
function translateClaudeEvent(sessionId, raw) {
|
|
322
|
+
const out = [];
|
|
323
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
324
|
+
return { notifications: out };
|
|
325
|
+
}
|
|
187
326
|
const msg = raw;
|
|
188
327
|
const type = typeof msg["type"] === "string" ? msg["type"] : "";
|
|
189
328
|
if (type === "assistant") {
|
|
190
|
-
|
|
329
|
+
const message = msg["message"];
|
|
330
|
+
if (message && typeof message === "object" && !Array.isArray(message)) {
|
|
331
|
+
const content = message.content;
|
|
332
|
+
if (Array.isArray(content)) {
|
|
333
|
+
for (const block of content) {
|
|
334
|
+
const translated = translateAssistantBlock(sessionId, block);
|
|
335
|
+
if (translated) out.push(...translated);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { notifications: out };
|
|
191
340
|
}
|
|
192
341
|
if (type === "stream_event") {
|
|
193
342
|
const event = msg["event"];
|
|
194
|
-
if (event?.type !== "content_block_delta") return;
|
|
195
|
-
if (event.delta?.type !== "text_delta") return;
|
|
343
|
+
if (event?.type !== "content_block_delta") return { notifications: out };
|
|
344
|
+
if (event.delta?.type !== "text_delta") return { notifications: out };
|
|
196
345
|
const text = typeof event.delta.text === "string" ? event.delta.text : "";
|
|
197
|
-
if (text.length === 0) return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
346
|
+
if (text.length === 0) return { notifications: out };
|
|
347
|
+
out.push({
|
|
348
|
+
method: "session/update",
|
|
349
|
+
params: {
|
|
350
|
+
sessionId,
|
|
351
|
+
update: {
|
|
352
|
+
sessionUpdate: "agent_message_chunk",
|
|
353
|
+
content: { type: "text", text }
|
|
354
|
+
}
|
|
203
355
|
}
|
|
204
356
|
});
|
|
205
|
-
return;
|
|
357
|
+
return { notifications: out };
|
|
206
358
|
}
|
|
207
359
|
if (type === "result") {
|
|
208
360
|
const subtype = typeof msg["subtype"] === "string" ? msg["subtype"] : "";
|
|
@@ -210,30 +362,102 @@ function handleClaudeEvent(sessionId, raw, done) {
|
|
|
210
362
|
if (isError) {
|
|
211
363
|
const resultText = typeof msg["result"] === "string" ? msg["result"] : "";
|
|
212
364
|
if (resultText.length > 0) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
365
|
+
out.push({
|
|
366
|
+
method: "session/update",
|
|
367
|
+
params: {
|
|
368
|
+
sessionId,
|
|
369
|
+
update: {
|
|
370
|
+
sessionUpdate: "agent_message_chunk",
|
|
371
|
+
content: { type: "text", text: `
|
|
218
372
|
|
|
219
373
|
[claude error: ${resultText}]` }
|
|
374
|
+
}
|
|
220
375
|
}
|
|
221
376
|
});
|
|
222
377
|
}
|
|
223
|
-
|
|
224
|
-
return;
|
|
378
|
+
return { notifications: out, stopReason: "refusal" };
|
|
225
379
|
}
|
|
226
380
|
if (subtype === "error_max_turns") {
|
|
227
|
-
|
|
228
|
-
return;
|
|
381
|
+
return { notifications: out, stopReason: "max_turn_requests" };
|
|
229
382
|
}
|
|
230
383
|
if (subtype === "error_max_tokens") {
|
|
231
|
-
|
|
232
|
-
return;
|
|
384
|
+
return { notifications: out, stopReason: "max_tokens" };
|
|
233
385
|
}
|
|
234
|
-
|
|
235
|
-
|
|
386
|
+
return { notifications: out, stopReason: "end_turn" };
|
|
387
|
+
}
|
|
388
|
+
return { notifications: out };
|
|
389
|
+
}
|
|
390
|
+
function translateAssistantBlock(sessionId, block) {
|
|
391
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) return null;
|
|
392
|
+
const b = block;
|
|
393
|
+
if (b["type"] !== "tool_use") return null;
|
|
394
|
+
if (b["name"] !== "AskUserQuestion") return null;
|
|
395
|
+
const input = b["input"];
|
|
396
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
397
|
+
const questions = input.questions;
|
|
398
|
+
if (!Array.isArray(questions)) return null;
|
|
399
|
+
const chunks = [];
|
|
400
|
+
for (const q of questions) {
|
|
401
|
+
const rendered = renderAskUserQuestionItem(q);
|
|
402
|
+
if (rendered) chunks.push(rendered);
|
|
403
|
+
}
|
|
404
|
+
if (chunks.length === 0) return null;
|
|
405
|
+
const text = `
|
|
406
|
+
|
|
407
|
+
${chunks.join("\n\n")}
|
|
408
|
+
`;
|
|
409
|
+
return [
|
|
410
|
+
{
|
|
411
|
+
method: "session/update",
|
|
412
|
+
params: {
|
|
413
|
+
sessionId,
|
|
414
|
+
update: {
|
|
415
|
+
sessionUpdate: "agent_message_chunk",
|
|
416
|
+
content: { type: "text", text }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
function renderAskUserQuestionItem(raw) {
|
|
423
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
424
|
+
const q = raw;
|
|
425
|
+
const question = typeof q["question"] === "string" ? q["question"].trim() : "";
|
|
426
|
+
if (!question) return null;
|
|
427
|
+
const header = typeof q["header"] === "string" ? q["header"].trim() : "";
|
|
428
|
+
const multiSelect = q["multiSelect"] === true;
|
|
429
|
+
const rawOptions = Array.isArray(q["options"]) ? q["options"] : [];
|
|
430
|
+
const options = [];
|
|
431
|
+
for (const opt of rawOptions) {
|
|
432
|
+
if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
|
|
433
|
+
const o = opt;
|
|
434
|
+
const label = typeof o["label"] === "string" ? o["label"].trim() : "";
|
|
435
|
+
if (!label) continue;
|
|
436
|
+
const value = header ? `${header}: ${label}` : label;
|
|
437
|
+
options.push({ label, value });
|
|
236
438
|
}
|
|
439
|
+
if (options.length === 0) return null;
|
|
440
|
+
const promptParts = [`**${question}**`];
|
|
441
|
+
if (multiSelect) {
|
|
442
|
+
promptParts.push(
|
|
443
|
+
"_(Multiple answers expected \u2014 click one, then type any others.)_"
|
|
444
|
+
);
|
|
445
|
+
} else {
|
|
446
|
+
promptParts.push(
|
|
447
|
+
"_(Pick an option, or type your own answer below.)_"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const payload = {
|
|
451
|
+
type: "options",
|
|
452
|
+
text: promptParts.join(" "),
|
|
453
|
+
options
|
|
454
|
+
};
|
|
455
|
+
return "```json\n" + JSON.stringify(payload, null, 2) + "\n```";
|
|
456
|
+
}
|
|
457
|
+
function handleClaudeEvent(sessionId, raw, done) {
|
|
458
|
+
const { notifications, stopReason } = translateClaudeEvent(sessionId, raw);
|
|
459
|
+
for (const n of notifications) notify(n.method, n.params);
|
|
460
|
+
if (stopReason) done(stopReason);
|
|
237
461
|
}
|
|
238
462
|
function handleInitialize(_params) {
|
|
239
463
|
return {
|
|
@@ -251,7 +475,9 @@ function handleInitialize(_params) {
|
|
|
251
475
|
// on each turn (see SessionState.mcpServers / runClaudeTurn).
|
|
252
476
|
loadSession: true,
|
|
253
477
|
mcpCapabilities: {},
|
|
254
|
-
|
|
478
|
+
// image: true since #142 — image blocks are forwarded to claude via
|
|
479
|
+
// `--input-format stream-json`. embeddedContext / audio still unsupported.
|
|
480
|
+
promptCapabilities: { embeddedContext: false, image: true, audio: false }
|
|
255
481
|
},
|
|
256
482
|
authMethods: []
|
|
257
483
|
};
|
|
@@ -259,10 +485,12 @@ function handleInitialize(_params) {
|
|
|
259
485
|
function handleNewSession(params) {
|
|
260
486
|
const sessionId = randomUUID();
|
|
261
487
|
const mcpServers = normalizeMcpServers(params.mcpServers);
|
|
488
|
+
const instructionsFile = normalizeInstructionsFile(params.instructionsFile);
|
|
262
489
|
sessions.set(sessionId, {
|
|
263
490
|
cwd: params.cwd ?? process.cwd(),
|
|
264
491
|
resume: false,
|
|
265
|
-
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
492
|
+
...mcpServers.length > 0 ? { mcpServers } : {},
|
|
493
|
+
...instructionsFile ? { instructionsFile } : {}
|
|
266
494
|
});
|
|
267
495
|
return { sessionId };
|
|
268
496
|
}
|
|
@@ -271,26 +499,37 @@ function handleLoadSession(params) {
|
|
|
271
499
|
throw new Error("session/load: sessionId required");
|
|
272
500
|
}
|
|
273
501
|
const mcpServers = normalizeMcpServers(params.mcpServers);
|
|
502
|
+
const instructionsFile = normalizeInstructionsFile(params.instructionsFile);
|
|
274
503
|
sessions.set(params.sessionId, {
|
|
275
504
|
cwd: params.cwd ?? process.cwd(),
|
|
276
505
|
resume: true,
|
|
277
|
-
...mcpServers.length > 0 ? { mcpServers } : {}
|
|
506
|
+
...mcpServers.length > 0 ? { mcpServers } : {},
|
|
507
|
+
...instructionsFile ? { instructionsFile } : {}
|
|
278
508
|
});
|
|
279
509
|
return {};
|
|
280
510
|
}
|
|
511
|
+
function normalizeInstructionsFile(raw) {
|
|
512
|
+
if (typeof raw !== "string") return void 0;
|
|
513
|
+
const trimmed = raw.trim();
|
|
514
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
515
|
+
}
|
|
281
516
|
async function handlePrompt(params) {
|
|
282
517
|
const state = sessions.get(params.sessionId);
|
|
283
518
|
if (!state) {
|
|
284
519
|
throw new Error(`unknown sessionId: ${params.sessionId}`);
|
|
285
520
|
}
|
|
286
521
|
const promptText = params.prompt.filter((b) => b.type === "text").map((b) => typeof b.text === "string" ? b.text : "").join("\n\n");
|
|
287
|
-
|
|
288
|
-
|
|
522
|
+
const images = params.prompt.filter(
|
|
523
|
+
(b) => b.type === "image" && typeof b.data === "string" && b.data.length > 0 && typeof b.mimeType === "string" && b.mimeType.length > 0
|
|
524
|
+
).map((b) => ({ type: "image", data: b.data, mimeType: b.mimeType }));
|
|
525
|
+
if (promptText.length === 0 && images.length === 0) {
|
|
526
|
+
throw new Error("session/prompt: no text or image content blocks");
|
|
289
527
|
}
|
|
290
528
|
const result = await runClaudeTurn(
|
|
291
529
|
params.sessionId,
|
|
292
530
|
state,
|
|
293
531
|
promptText,
|
|
532
|
+
images,
|
|
294
533
|
params.permissionMode
|
|
295
534
|
);
|
|
296
535
|
state.resume = true;
|
|
@@ -312,6 +551,8 @@ function handleCancel(params) {
|
|
|
312
551
|
}
|
|
313
552
|
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;
|
|
314
553
|
if (isMainModule) {
|
|
554
|
+
extraClaudeArgs = process.argv.slice(2);
|
|
555
|
+
Object.freeze(extraClaudeArgs);
|
|
315
556
|
const decoder = new FrameDecoder();
|
|
316
557
|
stdin.setEncoding("utf8");
|
|
317
558
|
stdin.on("data", (chunk) => {
|
|
@@ -373,8 +614,12 @@ async function dispatch(msg) {
|
|
|
373
614
|
}
|
|
374
615
|
}
|
|
375
616
|
export {
|
|
617
|
+
buildStreamJsonInput,
|
|
618
|
+
extraClaudeArgs,
|
|
376
619
|
handleInitialize,
|
|
377
620
|
handleLoadSession,
|
|
378
621
|
handleNewSession,
|
|
379
|
-
|
|
622
|
+
resolveInstructionsFile,
|
|
623
|
+
sessions,
|
|
624
|
+
translateClaudeEvent
|
|
380
625
|
};
|
package/dist/opencara-mcp.js
CHANGED
|
@@ -123,6 +123,12 @@ var AcpHistoryTurnSchema = z2.object({
|
|
|
123
123
|
role: z2.enum(["user", "assistant"]),
|
|
124
124
|
text: z2.string()
|
|
125
125
|
});
|
|
126
|
+
var AcpImageInputSchema = z2.object({
|
|
127
|
+
/** Base64-encoded image bytes, without the `data:<mime>;base64,` prefix. */
|
|
128
|
+
data: z2.string(),
|
|
129
|
+
/** IANA image MIME type, e.g. `image/png`, `image/jpeg`, `image/webp`. */
|
|
130
|
+
mimeType: z2.string()
|
|
131
|
+
});
|
|
126
132
|
var AcpPermissionModeSchema = z2.enum([
|
|
127
133
|
"default",
|
|
128
134
|
"acceptEdits",
|
|
@@ -135,7 +141,10 @@ var AcpSpecSchema = z2.object({
|
|
|
135
141
|
history: z2.array(AcpHistoryTurnSchema).default([]),
|
|
136
142
|
pageContextJson: z2.string().optional(),
|
|
137
143
|
priorSessionId: z2.string().optional(),
|
|
138
|
-
permissionMode: AcpPermissionModeSchema.optional()
|
|
144
|
+
permissionMode: AcpPermissionModeSchema.optional(),
|
|
145
|
+
instructionsFile: z2.string().optional(),
|
|
146
|
+
images: z2.array(AcpImageInputSchema).default([]),
|
|
147
|
+
model: z2.string().optional()
|
|
139
148
|
});
|
|
140
149
|
var AgentSpecSchema = z2.object({
|
|
141
150
|
kind: z2.string(),
|
|
@@ -391,6 +400,9 @@ var IssueDetailSchema = IssueSummarySchema.extend({
|
|
|
391
400
|
draftUpdatedAt: z4.string().datetime().nullable()
|
|
392
401
|
});
|
|
393
402
|
|
|
403
|
+
// ../shared/dist/cron.js
|
|
404
|
+
var MAX_STEP_MINUTES = 366 * 24 * 60 + 24 * 60;
|
|
405
|
+
|
|
394
406
|
// src/mcp/tools.ts
|
|
395
407
|
var issueBodySetShape = IssueBodySetCallSchema.omit({
|
|
396
408
|
type: true,
|