@khalilgharbaoui/opencode-claude-code-plugin 0.1.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/README.md +278 -0
- package/dist/index.d.ts +303 -0
- package/dist/index.js +2631 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2631 @@
|
|
|
1
|
+
// src/claude-code-language-model.ts
|
|
2
|
+
import { generateId } from "@ai-sdk/provider-utils";
|
|
3
|
+
|
|
4
|
+
// src/logger.ts
|
|
5
|
+
var DEBUG = process.env.DEBUG?.includes("opencode-claude-code") ?? false;
|
|
6
|
+
function fmt(level, msg, data) {
|
|
7
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8
|
+
const base = `[${ts}] [opencode-claude-code] ${level}: ${msg}`;
|
|
9
|
+
if (data && Object.keys(data).length > 0) {
|
|
10
|
+
return `${base} ${JSON.stringify(data)}`;
|
|
11
|
+
}
|
|
12
|
+
return base;
|
|
13
|
+
}
|
|
14
|
+
var log = {
|
|
15
|
+
info(msg, data) {
|
|
16
|
+
if (DEBUG) console.error(fmt("INFO", msg, data));
|
|
17
|
+
},
|
|
18
|
+
warn(msg, data) {
|
|
19
|
+
if (DEBUG) console.error(fmt("WARN", msg, data));
|
|
20
|
+
},
|
|
21
|
+
error(msg, data) {
|
|
22
|
+
console.error(fmt("ERROR", msg, data));
|
|
23
|
+
},
|
|
24
|
+
debug(msg, data) {
|
|
25
|
+
if (DEBUG) console.error(fmt("DEBUG", msg, data));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/tool-mapping.ts
|
|
30
|
+
function mapToolInput(name, input) {
|
|
31
|
+
if (!input) return input;
|
|
32
|
+
switch (name) {
|
|
33
|
+
case "Write":
|
|
34
|
+
return {
|
|
35
|
+
filePath: input.file_path ?? input.filePath,
|
|
36
|
+
content: input.content
|
|
37
|
+
};
|
|
38
|
+
case "Edit":
|
|
39
|
+
return {
|
|
40
|
+
filePath: input.file_path ?? input.filePath,
|
|
41
|
+
oldString: input.old_string ?? input.oldString,
|
|
42
|
+
newString: input.new_string ?? input.newString,
|
|
43
|
+
replaceAll: input.replace_all ?? input.replaceAll
|
|
44
|
+
};
|
|
45
|
+
case "Read":
|
|
46
|
+
return {
|
|
47
|
+
filePath: input.file_path ?? input.filePath,
|
|
48
|
+
offset: input.offset,
|
|
49
|
+
limit: input.limit
|
|
50
|
+
};
|
|
51
|
+
case "Bash":
|
|
52
|
+
return {
|
|
53
|
+
command: input.command,
|
|
54
|
+
description: input.description || `Execute: ${String(input.command || "").slice(0, 50)}${String(input.command || "").length > 50 ? "..." : ""}`,
|
|
55
|
+
timeout: input.timeout
|
|
56
|
+
};
|
|
57
|
+
case "NotebookEdit":
|
|
58
|
+
return {
|
|
59
|
+
notebookPath: input.notebook_path ?? input.notebookPath,
|
|
60
|
+
cellNumber: input.cell_number ?? input.cellNumber,
|
|
61
|
+
newSource: input.new_source ?? input.newSource,
|
|
62
|
+
cellType: input.cell_type ?? input.cellType,
|
|
63
|
+
editMode: input.edit_mode ?? input.editMode
|
|
64
|
+
};
|
|
65
|
+
case "Glob":
|
|
66
|
+
return {
|
|
67
|
+
pattern: input.pattern,
|
|
68
|
+
path: input.path
|
|
69
|
+
};
|
|
70
|
+
case "Grep":
|
|
71
|
+
return {
|
|
72
|
+
pattern: input.pattern,
|
|
73
|
+
path: input.path,
|
|
74
|
+
include: input.include
|
|
75
|
+
};
|
|
76
|
+
case "TodoWrite":
|
|
77
|
+
if (Array.isArray(input.todos)) {
|
|
78
|
+
const mappedTodos = input.todos.map((todo, index) => ({
|
|
79
|
+
content: todo.content,
|
|
80
|
+
status: todo.status || "pending",
|
|
81
|
+
priority: todo.priority || "medium",
|
|
82
|
+
id: todo.id || `todo_${Date.now()}_${index}`
|
|
83
|
+
}));
|
|
84
|
+
return { todos: mappedTodos };
|
|
85
|
+
}
|
|
86
|
+
return input;
|
|
87
|
+
default:
|
|
88
|
+
return input;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
var OPENCODE_HANDLED_TOOLS = /* @__PURE__ */ new Set([
|
|
92
|
+
"Edit",
|
|
93
|
+
"Write",
|
|
94
|
+
"Bash",
|
|
95
|
+
"NotebookEdit",
|
|
96
|
+
"Read",
|
|
97
|
+
"Glob",
|
|
98
|
+
"Grep"
|
|
99
|
+
]);
|
|
100
|
+
var CLAUDE_INTERNAL_TOOLS = /* @__PURE__ */ new Set([
|
|
101
|
+
"ToolSearch",
|
|
102
|
+
"Agent",
|
|
103
|
+
"AskFollowupQuestion"
|
|
104
|
+
]);
|
|
105
|
+
function mapTool(name, input) {
|
|
106
|
+
if (CLAUDE_INTERNAL_TOOLS.has(name)) {
|
|
107
|
+
log.debug("skipping Claude CLI internal tool", { name });
|
|
108
|
+
return { name, input, executed: true, skip: true };
|
|
109
|
+
}
|
|
110
|
+
if (name === "EnterPlanMode") return { name: "plan_enter", input: {}, executed: false };
|
|
111
|
+
if (name === "ExitPlanMode") return { name: "plan_exit", input, executed: false };
|
|
112
|
+
if (name === "TodoWrite") {
|
|
113
|
+
const mappedInput = mapToolInput(name, input);
|
|
114
|
+
return { name: "todowrite", input: mappedInput, executed: false };
|
|
115
|
+
}
|
|
116
|
+
if (name === "WebSearch" || name === "web_search") {
|
|
117
|
+
const mappedInput = input?.query ? { query: input.query } : input;
|
|
118
|
+
log.debug("mapping WebSearch", { originalInput: input, mappedInput });
|
|
119
|
+
return { name: "websearch_web_search_exa", input: mappedInput, executed: false };
|
|
120
|
+
}
|
|
121
|
+
if (name === "TaskOutput") {
|
|
122
|
+
if (!input) return { name: "bash", executed: false };
|
|
123
|
+
const output = input?.content || input?.output || JSON.stringify(input);
|
|
124
|
+
return {
|
|
125
|
+
name: "bash",
|
|
126
|
+
input: {
|
|
127
|
+
command: `echo "TASK OUTPUT: ${String(output).replace(/"/g, '\\"')}"`,
|
|
128
|
+
description: "Displaying task output"
|
|
129
|
+
},
|
|
130
|
+
executed: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (name.startsWith("mcp__")) {
|
|
134
|
+
const parts = name.slice(5).split("__");
|
|
135
|
+
if (parts.length >= 2) {
|
|
136
|
+
const serverName = parts[0];
|
|
137
|
+
const toolName = parts.slice(1).join("_");
|
|
138
|
+
const openCodeName = `${serverName}_${toolName}`;
|
|
139
|
+
log.debug("mapping MCP tool", { original: name, mapped: openCodeName });
|
|
140
|
+
return { name: openCodeName, input, executed: false };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (OPENCODE_HANDLED_TOOLS.has(name)) {
|
|
144
|
+
const mappedInput = mapToolInput(name, input);
|
|
145
|
+
const openCodeName = name.toLowerCase();
|
|
146
|
+
log.debug("mapping CLI-executed tool", { name, openCodeName });
|
|
147
|
+
return { name: openCodeName, input: mappedInput, executed: true };
|
|
148
|
+
}
|
|
149
|
+
return { name, input, executed: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/message-builder.ts
|
|
153
|
+
var THINKING_KEYWORDS = {
|
|
154
|
+
minimal: null,
|
|
155
|
+
low: "think",
|
|
156
|
+
medium: "think hard",
|
|
157
|
+
high: "think harder",
|
|
158
|
+
xhigh: "megathink",
|
|
159
|
+
max: "ultrathink"
|
|
160
|
+
};
|
|
161
|
+
function reasoningKeyword(effort) {
|
|
162
|
+
if (!effort) return null;
|
|
163
|
+
return THINKING_KEYWORDS[effort] ?? null;
|
|
164
|
+
}
|
|
165
|
+
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
166
|
+
"image/jpeg",
|
|
167
|
+
"image/png",
|
|
168
|
+
"image/gif",
|
|
169
|
+
"image/webp"
|
|
170
|
+
]);
|
|
171
|
+
function toImageBlock(part) {
|
|
172
|
+
const raw = part.data ?? part.url ?? part.source?.data;
|
|
173
|
+
if (!raw) {
|
|
174
|
+
log.warn("file part without data, skipping");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
|
|
178
|
+
let base64 = null;
|
|
179
|
+
if (typeof raw === "string") {
|
|
180
|
+
if (raw.startsWith("data:")) {
|
|
181
|
+
const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
|
|
182
|
+
if (!match) {
|
|
183
|
+
log.warn("malformed data URI, skipping file part");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
resolvedMediaType = resolvedMediaType || match[1];
|
|
187
|
+
base64 = match[2];
|
|
188
|
+
} else if (/^https?:\/\//i.test(raw)) {
|
|
189
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
190
|
+
return null;
|
|
191
|
+
} else {
|
|
192
|
+
base64 = raw;
|
|
193
|
+
}
|
|
194
|
+
} else if (raw instanceof URL) {
|
|
195
|
+
log.warn("remote URL images are not supported by Claude CLI, skipping");
|
|
196
|
+
return null;
|
|
197
|
+
} else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
|
|
198
|
+
base64 = Buffer.from(raw).toString("base64");
|
|
199
|
+
} else {
|
|
200
|
+
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
|
|
204
|
+
log.warn("unsupported media type for Claude image block, skipping", {
|
|
205
|
+
mediaType: resolvedMediaType
|
|
206
|
+
});
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
type: "image",
|
|
211
|
+
source: { type: "base64", media_type: resolvedMediaType, data: base64 }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function getToolResultText(part) {
|
|
215
|
+
const value = part.output ?? part.result;
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (!value || typeof value !== "object") {
|
|
220
|
+
return JSON.stringify(value);
|
|
221
|
+
}
|
|
222
|
+
switch (value.type) {
|
|
223
|
+
case "text":
|
|
224
|
+
case "error-text":
|
|
225
|
+
return String(value.value);
|
|
226
|
+
case "json":
|
|
227
|
+
case "error-json":
|
|
228
|
+
return JSON.stringify(value.value);
|
|
229
|
+
case "execution-denied":
|
|
230
|
+
return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
|
|
231
|
+
case "content":
|
|
232
|
+
return Array.isArray(value.value) ? value.value.map((item) => {
|
|
233
|
+
if (item?.type === "text") return item.text;
|
|
234
|
+
return JSON.stringify(item);
|
|
235
|
+
}).join("\n") : JSON.stringify(value.value);
|
|
236
|
+
default:
|
|
237
|
+
return JSON.stringify(value);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function compactConversationHistory(prompt) {
|
|
241
|
+
const conversationMessages = prompt.filter(
|
|
242
|
+
(m) => m.role === "user" || m.role === "assistant"
|
|
243
|
+
);
|
|
244
|
+
if (conversationMessages.length <= 1) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const historyParts = [];
|
|
248
|
+
for (let i = 0; i < conversationMessages.length - 1; i++) {
|
|
249
|
+
const msg = conversationMessages[i];
|
|
250
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
251
|
+
let text = "";
|
|
252
|
+
if (typeof msg.content === "string") {
|
|
253
|
+
text = msg.content;
|
|
254
|
+
} else if (Array.isArray(msg.content)) {
|
|
255
|
+
const textParts = msg.content.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
256
|
+
text = textParts.join("\n");
|
|
257
|
+
const toolCalls = msg.content.filter(
|
|
258
|
+
(p) => p.type === "tool-call"
|
|
259
|
+
);
|
|
260
|
+
const toolResults = msg.content.filter(
|
|
261
|
+
(p) => p.type === "tool-result"
|
|
262
|
+
);
|
|
263
|
+
if (toolCalls.length > 0) {
|
|
264
|
+
text += `
|
|
265
|
+
[Called ${toolCalls.length} tool(s): ${toolCalls.map((t) => t.toolName).join(", ")}]`;
|
|
266
|
+
}
|
|
267
|
+
if (toolResults.length > 0) {
|
|
268
|
+
text += `
|
|
269
|
+
[Received ${toolResults.length} tool result(s)]`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (text.trim()) {
|
|
273
|
+
const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
|
|
274
|
+
historyParts.push(`${role}: ${truncated}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (historyParts.length === 0) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return historyParts.join("\n\n");
|
|
281
|
+
}
|
|
282
|
+
function getClaudeUserMessage(prompt, includeHistoryContext = false, reasoningEffort) {
|
|
283
|
+
const content = [];
|
|
284
|
+
if (includeHistoryContext) {
|
|
285
|
+
const historyContext = compactConversationHistory(prompt);
|
|
286
|
+
if (historyContext) {
|
|
287
|
+
log.info("including conversation history context", {
|
|
288
|
+
historyLength: historyContext.length
|
|
289
|
+
});
|
|
290
|
+
content.push({
|
|
291
|
+
type: "text",
|
|
292
|
+
text: `<conversation_history>
|
|
293
|
+
The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
|
|
294
|
+
|
|
295
|
+
${historyContext}
|
|
296
|
+
|
|
297
|
+
</conversation_history>
|
|
298
|
+
|
|
299
|
+
Now continuing with the current message:
|
|
300
|
+
|
|
301
|
+
`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const messages = [];
|
|
306
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
307
|
+
if (prompt[i].role === "assistant") break;
|
|
308
|
+
messages.unshift(prompt[i]);
|
|
309
|
+
}
|
|
310
|
+
for (const msg of messages) {
|
|
311
|
+
if (msg.role === "user") {
|
|
312
|
+
if (typeof msg.content === "string") {
|
|
313
|
+
const str = msg.content;
|
|
314
|
+
if (str.trim()) {
|
|
315
|
+
content.push({ type: "text", text: str });
|
|
316
|
+
}
|
|
317
|
+
} else if (Array.isArray(msg.content)) {
|
|
318
|
+
for (const part of msg.content) {
|
|
319
|
+
if (part.type === "text") {
|
|
320
|
+
if (part.text && part.text.trim()) {
|
|
321
|
+
content.push({ type: "text", text: part.text });
|
|
322
|
+
}
|
|
323
|
+
} else if (part.type === "file" || part.type === "image") {
|
|
324
|
+
const block = toImageBlock(part);
|
|
325
|
+
if (block) {
|
|
326
|
+
content.push(block);
|
|
327
|
+
} else {
|
|
328
|
+
log.debug("skipped non-image file part", {
|
|
329
|
+
mediaType: part.mediaType
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} else if (part.type === "tool-result") {
|
|
333
|
+
const p = part;
|
|
334
|
+
content.push({
|
|
335
|
+
type: "tool_result",
|
|
336
|
+
tool_use_id: p.toolCallId,
|
|
337
|
+
content: getToolResultText(p)
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (content.length === 0) {
|
|
345
|
+
log.warn("empty user content; sending sentinel to satisfy CLI");
|
|
346
|
+
return JSON.stringify({
|
|
347
|
+
type: "user",
|
|
348
|
+
message: {
|
|
349
|
+
role: "user",
|
|
350
|
+
content: [{ type: "text", text: "(empty)" }]
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const keyword = reasoningKeyword(reasoningEffort);
|
|
355
|
+
if (keyword) {
|
|
356
|
+
const lastTextPart = [...content].reverse().find((p) => p.type === "text");
|
|
357
|
+
if (lastTextPart) {
|
|
358
|
+
lastTextPart.text = lastTextPart.text ? `${lastTextPart.text}
|
|
359
|
+
|
|
360
|
+
(${keyword})` : `(${keyword})`;
|
|
361
|
+
} else {
|
|
362
|
+
content.push({ type: "text", text: `(${keyword})` });
|
|
363
|
+
}
|
|
364
|
+
log.debug("injected reasoning keyword", { effort: reasoningEffort, keyword });
|
|
365
|
+
}
|
|
366
|
+
return JSON.stringify({
|
|
367
|
+
type: "user",
|
|
368
|
+
message: {
|
|
369
|
+
role: "user",
|
|
370
|
+
content
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/mcp-bridge.ts
|
|
376
|
+
import * as fs from "fs";
|
|
377
|
+
import * as path from "path";
|
|
378
|
+
import * as os from "os";
|
|
379
|
+
import * as crypto from "crypto";
|
|
380
|
+
var CONFIG_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
381
|
+
function fileExists(p) {
|
|
382
|
+
try {
|
|
383
|
+
return fs.statSync(p).isFile();
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function findConfigInDir(dir) {
|
|
389
|
+
for (const name of CONFIG_NAMES) {
|
|
390
|
+
const p = path.join(dir, name);
|
|
391
|
+
if (fileExists(p)) return p;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
function walkUpForConfig(startDir) {
|
|
396
|
+
const closestFirst = [];
|
|
397
|
+
let dir = path.resolve(startDir);
|
|
398
|
+
while (true) {
|
|
399
|
+
const hit = findConfigInDir(dir);
|
|
400
|
+
if (hit) closestFirst.push(hit);
|
|
401
|
+
const dotdir = path.join(dir, ".opencode");
|
|
402
|
+
const dothit = findConfigInDir(dotdir);
|
|
403
|
+
if (dothit) closestFirst.push(dothit);
|
|
404
|
+
const parent = path.dirname(dir);
|
|
405
|
+
if (parent === dir) break;
|
|
406
|
+
dir = parent;
|
|
407
|
+
}
|
|
408
|
+
return closestFirst.reverse();
|
|
409
|
+
}
|
|
410
|
+
function globalConfigs() {
|
|
411
|
+
const out = [];
|
|
412
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
413
|
+
const dir = path.join(xdg, "opencode");
|
|
414
|
+
const hit = findConfigInDir(dir);
|
|
415
|
+
if (hit) out.push(hit);
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
function stripJsonComments(text) {
|
|
419
|
+
let out = "";
|
|
420
|
+
let i = 0;
|
|
421
|
+
let inString = null;
|
|
422
|
+
while (i < text.length) {
|
|
423
|
+
const c = text[i];
|
|
424
|
+
if (inString) {
|
|
425
|
+
out += c;
|
|
426
|
+
if (c === "\\" && i + 1 < text.length) {
|
|
427
|
+
out += text[i + 1];
|
|
428
|
+
i += 2;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (c === inString) inString = null;
|
|
432
|
+
i++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (c === '"' || c === "'") {
|
|
436
|
+
inString = c;
|
|
437
|
+
out += c;
|
|
438
|
+
i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
442
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
446
|
+
i += 2;
|
|
447
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
448
|
+
i++;
|
|
449
|
+
i += 2;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
out += c;
|
|
453
|
+
i++;
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
function discoverConfigFiles(cwd) {
|
|
458
|
+
const files = [];
|
|
459
|
+
files.push(...globalConfigs());
|
|
460
|
+
files.push(...walkUpForConfig(cwd));
|
|
461
|
+
const dir = process.env.OPENCODE_CONFIG_DIR;
|
|
462
|
+
if (dir) {
|
|
463
|
+
const hit = findConfigInDir(dir);
|
|
464
|
+
if (hit) files.push(hit);
|
|
465
|
+
}
|
|
466
|
+
const explicit = process.env.OPENCODE_CONFIG;
|
|
467
|
+
if (explicit && fileExists(explicit)) files.push(explicit);
|
|
468
|
+
const resolvedOrder = files.map((f) => path.resolve(f));
|
|
469
|
+
const lastIndex = /* @__PURE__ */ new Map();
|
|
470
|
+
resolvedOrder.forEach((f, i) => lastIndex.set(f, i));
|
|
471
|
+
return resolvedOrder.filter((f, i) => lastIndex.get(f) === i);
|
|
472
|
+
}
|
|
473
|
+
function translateServer(name, spec) {
|
|
474
|
+
if (!spec || typeof spec !== "object") return null;
|
|
475
|
+
if (spec.enabled === false) return null;
|
|
476
|
+
if (spec.type === "local") {
|
|
477
|
+
const cmd = spec.command;
|
|
478
|
+
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
479
|
+
log.warn("skipping local MCP server with no command", { name });
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const out = {
|
|
483
|
+
type: "stdio",
|
|
484
|
+
command: String(cmd[0])
|
|
485
|
+
};
|
|
486
|
+
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
487
|
+
if (spec.environment && typeof spec.environment === "object") {
|
|
488
|
+
out.env = spec.environment;
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
if (spec.type === "remote") {
|
|
493
|
+
if (!spec.url || typeof spec.url !== "string") {
|
|
494
|
+
log.warn("skipping remote MCP server with no url", { name });
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const out = {
|
|
498
|
+
type: "http",
|
|
499
|
+
url: spec.url
|
|
500
|
+
};
|
|
501
|
+
if (spec.headers && typeof spec.headers === "object") {
|
|
502
|
+
out.headers = spec.headers;
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
log.warn("skipping MCP server with unknown type", {
|
|
507
|
+
name,
|
|
508
|
+
type: spec?.type
|
|
509
|
+
});
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
function readAndParse(file) {
|
|
513
|
+
try {
|
|
514
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
515
|
+
return JSON.parse(stripJsonComments(raw));
|
|
516
|
+
} catch (e) {
|
|
517
|
+
log.warn("failed to parse opencode config", {
|
|
518
|
+
file,
|
|
519
|
+
error: e instanceof Error ? e.message : String(e)
|
|
520
|
+
});
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function bridgeOpencodeMcp(cwd) {
|
|
525
|
+
const files = discoverConfigFiles(cwd);
|
|
526
|
+
if (files.length === 0) return null;
|
|
527
|
+
const merged = {};
|
|
528
|
+
for (const file of files) {
|
|
529
|
+
const parsed = readAndParse(file);
|
|
530
|
+
const mcp = parsed?.mcp ?? null;
|
|
531
|
+
if (!mcp || typeof mcp !== "object") continue;
|
|
532
|
+
for (const [name, spec] of Object.entries(mcp)) {
|
|
533
|
+
merged[name] = spec;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const servers = {};
|
|
537
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
538
|
+
const translated = translateServer(name, spec);
|
|
539
|
+
if (translated) servers[name] = translated;
|
|
540
|
+
}
|
|
541
|
+
if (Object.keys(servers).length === 0) return null;
|
|
542
|
+
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
543
|
+
const hash = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
544
|
+
const outPath = path.join(
|
|
545
|
+
os.tmpdir(),
|
|
546
|
+
`opencode-claude-code-mcp-${hash}.json`
|
|
547
|
+
);
|
|
548
|
+
try {
|
|
549
|
+
if (!fileExists(outPath)) {
|
|
550
|
+
fs.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
551
|
+
}
|
|
552
|
+
} catch (e) {
|
|
553
|
+
log.warn("failed to write bridged MCP config", {
|
|
554
|
+
error: e instanceof Error ? e.message : String(e)
|
|
555
|
+
});
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
log.info("bridged opencode MCP config", {
|
|
559
|
+
sources: files,
|
|
560
|
+
target: outPath,
|
|
561
|
+
servers: Object.keys(servers)
|
|
562
|
+
});
|
|
563
|
+
return outPath;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/session-manager.ts
|
|
567
|
+
import { spawn } from "child_process";
|
|
568
|
+
import { createInterface } from "readline";
|
|
569
|
+
import { EventEmitter } from "events";
|
|
570
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
571
|
+
var claudeSessions = /* @__PURE__ */ new Map();
|
|
572
|
+
var MAX_ACTIVE_PROCESSES = 16;
|
|
573
|
+
function touch(key) {
|
|
574
|
+
const existing = activeProcesses.get(key);
|
|
575
|
+
if (existing) {
|
|
576
|
+
activeProcesses.delete(key);
|
|
577
|
+
activeProcesses.set(key, existing);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function evictIfNeeded() {
|
|
581
|
+
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
582
|
+
const oldestKey = activeProcesses.keys().next().value;
|
|
583
|
+
if (!oldestKey) break;
|
|
584
|
+
log.info("evicting LRU claude process", { sessionKey: oldestKey });
|
|
585
|
+
deleteActiveProcess(oldestKey);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function getActiveProcess(key) {
|
|
589
|
+
const ap = activeProcesses.get(key);
|
|
590
|
+
if (ap) touch(key);
|
|
591
|
+
return ap;
|
|
592
|
+
}
|
|
593
|
+
function deleteActiveProcess(key) {
|
|
594
|
+
const ap = activeProcesses.get(key);
|
|
595
|
+
if (ap) {
|
|
596
|
+
void ap.proxyServer?.close();
|
|
597
|
+
ap.proc.kill();
|
|
598
|
+
activeProcesses.delete(key);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function getClaudeSessionId(key) {
|
|
602
|
+
return claudeSessions.get(key);
|
|
603
|
+
}
|
|
604
|
+
function setClaudeSessionId(key, sessionId) {
|
|
605
|
+
claudeSessions.set(key, sessionId);
|
|
606
|
+
}
|
|
607
|
+
function deleteClaudeSessionId(key) {
|
|
608
|
+
claudeSessions.delete(key);
|
|
609
|
+
}
|
|
610
|
+
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer) {
|
|
611
|
+
evictIfNeeded();
|
|
612
|
+
log.info("spawning new claude process", { cliPath, cliArgs, cwd, sessionKey: sessionKey2 });
|
|
613
|
+
const proc = spawn(cliPath, cliArgs, {
|
|
614
|
+
cwd,
|
|
615
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
616
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
617
|
+
shell: process.platform === "win32"
|
|
618
|
+
});
|
|
619
|
+
const lineEmitter = new EventEmitter();
|
|
620
|
+
const rl = createInterface({ input: proc.stdout });
|
|
621
|
+
rl.on("line", (line) => {
|
|
622
|
+
lineEmitter.emit("line", line);
|
|
623
|
+
});
|
|
624
|
+
rl.on("close", () => {
|
|
625
|
+
lineEmitter.emit("close");
|
|
626
|
+
});
|
|
627
|
+
const ap = { proc, lineEmitter, proxyServer: proxyServer ?? null };
|
|
628
|
+
activeProcesses.set(sessionKey2, ap);
|
|
629
|
+
proc.on("exit", (code, signal) => {
|
|
630
|
+
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
631
|
+
void proxyServer?.close();
|
|
632
|
+
activeProcesses.delete(sessionKey2);
|
|
633
|
+
if (code !== 0 && code !== null) {
|
|
634
|
+
log.info("process exited with error, clearing session", {
|
|
635
|
+
code,
|
|
636
|
+
sessionKey: sessionKey2
|
|
637
|
+
});
|
|
638
|
+
claudeSessions.delete(sessionKey2);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
proc.stderr?.on("data", (data) => {
|
|
642
|
+
const stderr = data.toString();
|
|
643
|
+
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
644
|
+
if (stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
645
|
+
log.warn("claude session ID error, clearing session", {
|
|
646
|
+
sessionKey: sessionKey2,
|
|
647
|
+
error: stderr.slice(0, 200)
|
|
648
|
+
});
|
|
649
|
+
claudeSessions.delete(sessionKey2);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
return ap;
|
|
653
|
+
}
|
|
654
|
+
function buildCliArgs(opts) {
|
|
655
|
+
const {
|
|
656
|
+
sessionKey: sessionKey2,
|
|
657
|
+
skipPermissions,
|
|
658
|
+
includeSessionId = true,
|
|
659
|
+
model,
|
|
660
|
+
permissionMode,
|
|
661
|
+
mcpConfig,
|
|
662
|
+
strictMcpConfig,
|
|
663
|
+
disallowedTools
|
|
664
|
+
} = opts;
|
|
665
|
+
const args = [
|
|
666
|
+
"--output-format",
|
|
667
|
+
"stream-json",
|
|
668
|
+
"--input-format",
|
|
669
|
+
"stream-json",
|
|
670
|
+
"--verbose"
|
|
671
|
+
];
|
|
672
|
+
if (model) {
|
|
673
|
+
args.push("--model", model);
|
|
674
|
+
}
|
|
675
|
+
if (permissionMode) {
|
|
676
|
+
args.push("--permission-mode", permissionMode);
|
|
677
|
+
}
|
|
678
|
+
if (includeSessionId) {
|
|
679
|
+
const sessionId = claudeSessions.get(sessionKey2);
|
|
680
|
+
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
681
|
+
args.push("--session-id", sessionId);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (mcpConfig) {
|
|
685
|
+
const configs = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
|
|
686
|
+
const filtered = configs.filter((c) => typeof c === "string" && c.length > 0);
|
|
687
|
+
if (filtered.length > 0) {
|
|
688
|
+
args.push("--mcp-config", ...filtered);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (strictMcpConfig) {
|
|
692
|
+
args.push("--strict-mcp-config");
|
|
693
|
+
}
|
|
694
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
695
|
+
args.push("--disallowedTools", ...disallowedTools);
|
|
696
|
+
}
|
|
697
|
+
if (skipPermissions) {
|
|
698
|
+
args.push("--dangerously-skip-permissions");
|
|
699
|
+
}
|
|
700
|
+
return args;
|
|
701
|
+
}
|
|
702
|
+
function sessionKey(cwd, modelId) {
|
|
703
|
+
return `${cwd}::${modelId}`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/proxy-mcp.ts
|
|
707
|
+
import { createServer } from "http";
|
|
708
|
+
import * as fs2 from "fs";
|
|
709
|
+
import * as path2 from "path";
|
|
710
|
+
import * as os2 from "os";
|
|
711
|
+
import * as crypto2 from "crypto";
|
|
712
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
713
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
714
|
+
var SERVER_NAME = "opencode_proxy";
|
|
715
|
+
var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
|
|
716
|
+
var DEFAULT_PROXY_TOOLS = [
|
|
717
|
+
{
|
|
718
|
+
name: "bash",
|
|
719
|
+
description: "Execute a shell command. Routed through opencode's bash tool so permission prompts flow through opencode's UI.",
|
|
720
|
+
inputSchema: {
|
|
721
|
+
type: "object",
|
|
722
|
+
properties: {
|
|
723
|
+
command: {
|
|
724
|
+
type: "string",
|
|
725
|
+
description: "The shell command to execute."
|
|
726
|
+
},
|
|
727
|
+
description: {
|
|
728
|
+
type: "string",
|
|
729
|
+
description: "Short human-readable description of what the command does."
|
|
730
|
+
},
|
|
731
|
+
timeout: {
|
|
732
|
+
type: "number",
|
|
733
|
+
description: "Optional timeout in milliseconds."
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
required: ["command"]
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "write",
|
|
741
|
+
description: "Write a file. Routed through opencode's write tool so permission prompts flow through opencode's UI.",
|
|
742
|
+
inputSchema: {
|
|
743
|
+
type: "object",
|
|
744
|
+
properties: {
|
|
745
|
+
filePath: {
|
|
746
|
+
type: "string",
|
|
747
|
+
description: "The file to write. Absolute paths are preferred."
|
|
748
|
+
},
|
|
749
|
+
content: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "The full content to write to the file."
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
required: ["filePath", "content"]
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: "edit",
|
|
759
|
+
description: "Replace text in an existing file. Routed through opencode's edit tool so permission prompts flow through opencode's UI.",
|
|
760
|
+
inputSchema: {
|
|
761
|
+
type: "object",
|
|
762
|
+
properties: {
|
|
763
|
+
filePath: {
|
|
764
|
+
type: "string",
|
|
765
|
+
description: "The file to edit. Absolute paths are preferred."
|
|
766
|
+
},
|
|
767
|
+
oldString: {
|
|
768
|
+
type: "string",
|
|
769
|
+
description: "The exact text to replace."
|
|
770
|
+
},
|
|
771
|
+
newString: {
|
|
772
|
+
type: "string",
|
|
773
|
+
description: "The replacement text."
|
|
774
|
+
},
|
|
775
|
+
replaceAll: {
|
|
776
|
+
type: "boolean",
|
|
777
|
+
description: "Replace all occurrences instead of just the first one."
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
required: ["filePath", "oldString", "newString"]
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "webfetch",
|
|
785
|
+
description: "Fetch content from a URL. Routed through opencode's webfetch tool so permission prompts flow through opencode's UI. Returns the page content in the requested format.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
url: {
|
|
790
|
+
type: "string",
|
|
791
|
+
description: "The URL to fetch content from. Must start with http:// or https://."
|
|
792
|
+
},
|
|
793
|
+
format: {
|
|
794
|
+
type: "string",
|
|
795
|
+
enum: ["text", "markdown", "html"],
|
|
796
|
+
description: "The format to return the content in. Defaults to markdown."
|
|
797
|
+
},
|
|
798
|
+
timeout: {
|
|
799
|
+
type: "number",
|
|
800
|
+
description: "Optional timeout in seconds (max 120)."
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
required: ["url"]
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
|
|
808
|
+
const calls = new EventEmitter2();
|
|
809
|
+
const pending = /* @__PURE__ */ new Map();
|
|
810
|
+
const server2 = createServer(async (req, res) => {
|
|
811
|
+
if (req.method !== "POST" || !req.url?.startsWith("/mcp")) {
|
|
812
|
+
res.statusCode = 404;
|
|
813
|
+
res.end();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const body = await readBody(req);
|
|
818
|
+
const request = JSON.parse(body);
|
|
819
|
+
if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
820
|
+
writeJson(res, {
|
|
821
|
+
jsonrpc: "2.0",
|
|
822
|
+
id: request?.id ?? null,
|
|
823
|
+
error: { code: -32600, message: "Invalid request" }
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
log.debug("proxy-mcp request", {
|
|
828
|
+
method: request.method,
|
|
829
|
+
id: request.id
|
|
830
|
+
});
|
|
831
|
+
if (request.method === "initialize") {
|
|
832
|
+
writeJson(res, {
|
|
833
|
+
jsonrpc: "2.0",
|
|
834
|
+
id: request.id ?? null,
|
|
835
|
+
result: {
|
|
836
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
837
|
+
capabilities: { tools: {} },
|
|
838
|
+
serverInfo: {
|
|
839
|
+
name: SERVER_NAME,
|
|
840
|
+
version: "0.1.0"
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (request.method === "notifications/initialized") {
|
|
847
|
+
res.statusCode = 204;
|
|
848
|
+
res.end();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (request.method === "tools/list") {
|
|
852
|
+
writeJson(res, {
|
|
853
|
+
jsonrpc: "2.0",
|
|
854
|
+
id: request.id ?? null,
|
|
855
|
+
result: {
|
|
856
|
+
tools: tools.map((t) => ({
|
|
857
|
+
name: t.name,
|
|
858
|
+
description: t.description,
|
|
859
|
+
inputSchema: t.inputSchema
|
|
860
|
+
}))
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (request.method === "tools/call") {
|
|
866
|
+
const params = request.params ?? {};
|
|
867
|
+
const toolName = String(params.name ?? "");
|
|
868
|
+
const input = params.arguments ?? {};
|
|
869
|
+
if (!tools.some((t) => t.name === toolName)) {
|
|
870
|
+
writeJson(res, {
|
|
871
|
+
jsonrpc: "2.0",
|
|
872
|
+
id: request.id ?? null,
|
|
873
|
+
error: {
|
|
874
|
+
code: -32601,
|
|
875
|
+
message: `Unknown proxy tool: ${toolName}`
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const callId = crypto2.randomUUID();
|
|
881
|
+
log.info("proxy-mcp tool call received", {
|
|
882
|
+
callId,
|
|
883
|
+
toolName,
|
|
884
|
+
hasInput: input != null
|
|
885
|
+
});
|
|
886
|
+
const result = await new Promise(
|
|
887
|
+
(resolve2, reject) => {
|
|
888
|
+
const entry = {
|
|
889
|
+
id: callId,
|
|
890
|
+
toolName,
|
|
891
|
+
input,
|
|
892
|
+
resolve: resolve2,
|
|
893
|
+
reject
|
|
894
|
+
};
|
|
895
|
+
pending.set(callId, entry);
|
|
896
|
+
calls.emit("call", entry);
|
|
897
|
+
}
|
|
898
|
+
).finally(() => {
|
|
899
|
+
pending.delete(callId);
|
|
900
|
+
});
|
|
901
|
+
if (result.kind === "error") {
|
|
902
|
+
writeJson(res, {
|
|
903
|
+
jsonrpc: "2.0",
|
|
904
|
+
id: request.id ?? null,
|
|
905
|
+
error: {
|
|
906
|
+
code: -32e3,
|
|
907
|
+
message: result.message
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
writeJson(res, {
|
|
913
|
+
jsonrpc: "2.0",
|
|
914
|
+
id: request.id ?? null,
|
|
915
|
+
result: {
|
|
916
|
+
content: [{ type: "text", text: result.text }],
|
|
917
|
+
isError: result.isError === true
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
writeJson(res, {
|
|
923
|
+
jsonrpc: "2.0",
|
|
924
|
+
id: request.id ?? null,
|
|
925
|
+
error: { code: -32601, message: `Unknown method: ${request.method}` }
|
|
926
|
+
});
|
|
927
|
+
} catch (error) {
|
|
928
|
+
log.warn("proxy-mcp error handling request", {
|
|
929
|
+
error: error instanceof Error ? error.message : String(error)
|
|
930
|
+
});
|
|
931
|
+
try {
|
|
932
|
+
writeJson(res, {
|
|
933
|
+
jsonrpc: "2.0",
|
|
934
|
+
id: null,
|
|
935
|
+
error: {
|
|
936
|
+
code: -32603,
|
|
937
|
+
message: error instanceof Error ? error.message : "Internal error"
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
} catch {
|
|
941
|
+
try {
|
|
942
|
+
res.statusCode = 500;
|
|
943
|
+
res.end();
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
await new Promise((resolve2, reject) => {
|
|
950
|
+
server2.once("error", reject);
|
|
951
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
952
|
+
server2.off("error", reject);
|
|
953
|
+
resolve2();
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
const addr = server2.address();
|
|
957
|
+
if (!addr) {
|
|
958
|
+
server2.close();
|
|
959
|
+
throw new Error("Failed to bind proxy MCP server");
|
|
960
|
+
}
|
|
961
|
+
const url = `http://127.0.0.1:${addr.port}/mcp`;
|
|
962
|
+
log.info("proxy-mcp server started", {
|
|
963
|
+
url,
|
|
964
|
+
tools: tools.map((t) => t.name)
|
|
965
|
+
});
|
|
966
|
+
let configFilePath = null;
|
|
967
|
+
const api = {
|
|
968
|
+
url,
|
|
969
|
+
serverName: SERVER_NAME,
|
|
970
|
+
tools,
|
|
971
|
+
calls,
|
|
972
|
+
configPath() {
|
|
973
|
+
if (configFilePath) return configFilePath;
|
|
974
|
+
const body = JSON.stringify(
|
|
975
|
+
{
|
|
976
|
+
mcpServers: {
|
|
977
|
+
[SERVER_NAME]: {
|
|
978
|
+
type: "http",
|
|
979
|
+
url
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
null,
|
|
984
|
+
2
|
|
985
|
+
);
|
|
986
|
+
const hash = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
987
|
+
const outPath = path2.join(
|
|
988
|
+
os2.tmpdir(),
|
|
989
|
+
`opencode-claude-code-proxy-${hash}.json`
|
|
990
|
+
);
|
|
991
|
+
fs2.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
992
|
+
configFilePath = outPath;
|
|
993
|
+
return outPath;
|
|
994
|
+
},
|
|
995
|
+
async close() {
|
|
996
|
+
for (const entry of pending.values()) {
|
|
997
|
+
entry.reject(new Error("proxy MCP server closed"));
|
|
998
|
+
}
|
|
999
|
+
pending.clear();
|
|
1000
|
+
await new Promise((resolve2) => {
|
|
1001
|
+
server2.close(() => resolve2());
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
return api;
|
|
1006
|
+
}
|
|
1007
|
+
function disallowedToolFlags(tools) {
|
|
1008
|
+
const nameMap = {
|
|
1009
|
+
bash: "Bash",
|
|
1010
|
+
read: "Read",
|
|
1011
|
+
write: "Write",
|
|
1012
|
+
edit: "Edit",
|
|
1013
|
+
glob: "Glob",
|
|
1014
|
+
grep: "Grep",
|
|
1015
|
+
webfetch: "WebFetch"
|
|
1016
|
+
};
|
|
1017
|
+
const out = [];
|
|
1018
|
+
for (const t of tools) {
|
|
1019
|
+
const mapped = nameMap[t.name.toLowerCase()];
|
|
1020
|
+
if (mapped) out.push(mapped);
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
function readBody(req) {
|
|
1025
|
+
return new Promise((resolve2, reject) => {
|
|
1026
|
+
const chunks = [];
|
|
1027
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1028
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
1029
|
+
req.on("error", reject);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
function writeJson(res, body) {
|
|
1033
|
+
const payload = JSON.stringify(body);
|
|
1034
|
+
res.statusCode = 200;
|
|
1035
|
+
res.setHeader("Content-Type", "application/json");
|
|
1036
|
+
res.setHeader("Content-Length", Buffer.byteLength(payload).toString());
|
|
1037
|
+
res.end(payload);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/proxy-broker.ts
|
|
1041
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
1042
|
+
var pendingBySession = /* @__PURE__ */ new Map();
|
|
1043
|
+
var emitter = new EventEmitter3();
|
|
1044
|
+
function eventName(sessionKey2) {
|
|
1045
|
+
return `pending:${sessionKey2}`;
|
|
1046
|
+
}
|
|
1047
|
+
function onPendingProxyCall(sessionKey2, handler) {
|
|
1048
|
+
const name = eventName(sessionKey2);
|
|
1049
|
+
emitter.on(name, handler);
|
|
1050
|
+
return () => emitter.off(name, handler);
|
|
1051
|
+
}
|
|
1052
|
+
function queuePendingProxyCall(sessionKey2, call) {
|
|
1053
|
+
const existing = pendingBySession.get(sessionKey2);
|
|
1054
|
+
if (existing) {
|
|
1055
|
+
existing.reject(
|
|
1056
|
+
new Error(`Another proxy tool call is already pending for ${sessionKey2}`)
|
|
1057
|
+
);
|
|
1058
|
+
pendingBySession.delete(sessionKey2);
|
|
1059
|
+
}
|
|
1060
|
+
const pending = {
|
|
1061
|
+
sessionKey: sessionKey2,
|
|
1062
|
+
toolCallId: call.id,
|
|
1063
|
+
toolName: call.toolName,
|
|
1064
|
+
input: call.input,
|
|
1065
|
+
resolve: call.resolve,
|
|
1066
|
+
reject: call.reject
|
|
1067
|
+
};
|
|
1068
|
+
pendingBySession.set(sessionKey2, pending);
|
|
1069
|
+
emitter.emit(eventName(sessionKey2), pending);
|
|
1070
|
+
log.info("queued pending proxy call", {
|
|
1071
|
+
sessionKey: sessionKey2,
|
|
1072
|
+
toolCallId: call.id,
|
|
1073
|
+
toolName: call.toolName
|
|
1074
|
+
});
|
|
1075
|
+
return pending;
|
|
1076
|
+
}
|
|
1077
|
+
function getPendingProxyCall(sessionKey2) {
|
|
1078
|
+
return pendingBySession.get(sessionKey2);
|
|
1079
|
+
}
|
|
1080
|
+
function resolvePendingProxyCall(sessionKey2, result) {
|
|
1081
|
+
const pending = pendingBySession.get(sessionKey2);
|
|
1082
|
+
if (!pending) return false;
|
|
1083
|
+
pendingBySession.delete(sessionKey2);
|
|
1084
|
+
pending.resolve(result);
|
|
1085
|
+
log.info("resolved pending proxy call", {
|
|
1086
|
+
sessionKey: sessionKey2,
|
|
1087
|
+
toolCallId: pending.toolCallId,
|
|
1088
|
+
toolName: pending.toolName
|
|
1089
|
+
});
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/claude-code-language-model.ts
|
|
1094
|
+
var ClaudeCodeLanguageModel = class {
|
|
1095
|
+
specificationVersion = "v3";
|
|
1096
|
+
modelId;
|
|
1097
|
+
config;
|
|
1098
|
+
constructor(modelId, config) {
|
|
1099
|
+
this.modelId = modelId;
|
|
1100
|
+
this.config = config;
|
|
1101
|
+
}
|
|
1102
|
+
supportedUrls = {};
|
|
1103
|
+
get provider() {
|
|
1104
|
+
return this.config.provider;
|
|
1105
|
+
}
|
|
1106
|
+
toUsage(rawUsage) {
|
|
1107
|
+
const iter = rawUsage?.iterations;
|
|
1108
|
+
const effective = iter?.length ? iter[iter.length - 1] : rawUsage;
|
|
1109
|
+
const noCache = effective?.input_tokens ?? 0;
|
|
1110
|
+
const cacheRead = effective?.cache_read_input_tokens ?? 0;
|
|
1111
|
+
const cacheWrite = effective?.cache_creation_input_tokens ?? 0;
|
|
1112
|
+
return {
|
|
1113
|
+
inputTokens: {
|
|
1114
|
+
total: noCache + cacheRead + cacheWrite,
|
|
1115
|
+
noCache,
|
|
1116
|
+
cacheRead: cacheRead || void 0,
|
|
1117
|
+
cacheWrite: cacheWrite || void 0
|
|
1118
|
+
},
|
|
1119
|
+
outputTokens: {
|
|
1120
|
+
total: effective?.output_tokens,
|
|
1121
|
+
text: effective?.output_tokens,
|
|
1122
|
+
reasoning: void 0
|
|
1123
|
+
},
|
|
1124
|
+
raw: rawUsage
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
toFinishReason(reason = "stop") {
|
|
1128
|
+
return {
|
|
1129
|
+
unified: reason,
|
|
1130
|
+
raw: reason
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
requestScope(options) {
|
|
1134
|
+
const tools = options?.tools;
|
|
1135
|
+
if (Array.isArray(tools)) return "tools";
|
|
1136
|
+
if (tools && typeof tools === "object") {
|
|
1137
|
+
return Object.keys(tools).length > 0 ? "tools" : "no-tools";
|
|
1138
|
+
}
|
|
1139
|
+
return "no-tools";
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Build the combined `--mcp-config` list: user-configured paths plus the
|
|
1143
|
+
* auto-bridged opencode MCP config (when enabled and present) and the
|
|
1144
|
+
* proxy MCP scratch file (when proxyTools are enabled).
|
|
1145
|
+
*/
|
|
1146
|
+
effectiveMcpConfig(cwd, proxyConfigPath) {
|
|
1147
|
+
const user = Array.isArray(this.config.mcpConfig) ? this.config.mcpConfig.slice() : this.config.mcpConfig ? [this.config.mcpConfig] : [];
|
|
1148
|
+
if (this.config.bridgeOpencodeMcp !== false) {
|
|
1149
|
+
const bridged = bridgeOpencodeMcp(cwd);
|
|
1150
|
+
if (bridged) user.push(bridged);
|
|
1151
|
+
}
|
|
1152
|
+
if (proxyConfigPath) user.push(proxyConfigPath);
|
|
1153
|
+
return user;
|
|
1154
|
+
}
|
|
1155
|
+
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
1156
|
+
resolvedProxyTools() {
|
|
1157
|
+
const names = this.config.proxyTools;
|
|
1158
|
+
if (!names || names.length === 0) return null;
|
|
1159
|
+
const defsByName = new Map(
|
|
1160
|
+
DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
|
|
1161
|
+
);
|
|
1162
|
+
const picked = [];
|
|
1163
|
+
for (const n of names) {
|
|
1164
|
+
const def = defsByName.get(String(n).toLowerCase());
|
|
1165
|
+
if (def) picked.push(def);
|
|
1166
|
+
}
|
|
1167
|
+
return picked.length > 0 ? picked : null;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Create a proxy MCP server for a single active Claude process/session.
|
|
1171
|
+
* The process lifecycle owns the server lifecycle via session-manager.
|
|
1172
|
+
*/
|
|
1173
|
+
async ensureProxyServer(tools, sessionKeyForCalls) {
|
|
1174
|
+
const srv = await createProxyMcpServer(tools);
|
|
1175
|
+
srv.calls.on("call", (call) => {
|
|
1176
|
+
queuePendingProxyCall(sessionKeyForCalls, call);
|
|
1177
|
+
});
|
|
1178
|
+
return srv;
|
|
1179
|
+
}
|
|
1180
|
+
extractPendingProxyResult(prompt, toolCallId) {
|
|
1181
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1182
|
+
const msg = prompt[i];
|
|
1183
|
+
if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
|
|
1184
|
+
for (const part of msg.content) {
|
|
1185
|
+
if (part.type !== "tool-result" || part.toolCallId !== toolCallId) continue;
|
|
1186
|
+
const output = part.output;
|
|
1187
|
+
if (!output || typeof output !== "object") {
|
|
1188
|
+
return {
|
|
1189
|
+
kind: "text",
|
|
1190
|
+
text: String(output ?? "")
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (output.type === "text") {
|
|
1194
|
+
return {
|
|
1195
|
+
kind: "text",
|
|
1196
|
+
text: String(output.value ?? "")
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
if (output.type === "json") {
|
|
1200
|
+
return {
|
|
1201
|
+
kind: "text",
|
|
1202
|
+
text: JSON.stringify(output.value)
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (output.type === "content" && Array.isArray(output.value)) {
|
|
1206
|
+
const text = output.value.filter((v) => v?.type === "text" && typeof v.text === "string").map((v) => v.text).join("\n");
|
|
1207
|
+
return {
|
|
1208
|
+
kind: "text",
|
|
1209
|
+
text
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
kind: "text",
|
|
1214
|
+
text: JSON.stringify(output)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Opencode sets `x-session-affinity: <sessionID>` on LLM calls for
|
|
1222
|
+
* third-party providers (packages/opencode/src/session/llm.ts). Use it so
|
|
1223
|
+
* two chats in the same cwd+model get separate CLI processes instead of
|
|
1224
|
+
* stomping on each other. Falls back to "default" when absent (older
|
|
1225
|
+
* opencode, direct AI-SDK use, title synthesis paths, etc).
|
|
1226
|
+
*/
|
|
1227
|
+
sessionAffinity(options) {
|
|
1228
|
+
const headers = options?.headers;
|
|
1229
|
+
if (!headers) return "default";
|
|
1230
|
+
for (const key of Object.keys(headers)) {
|
|
1231
|
+
if (key.toLowerCase() === "x-session-affinity") {
|
|
1232
|
+
const v = headers[key];
|
|
1233
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return "default";
|
|
1237
|
+
}
|
|
1238
|
+
controlRequestBehaviorForTool(toolName) {
|
|
1239
|
+
const configured = this.config.controlRequestToolBehaviors;
|
|
1240
|
+
if (configured && toolName) {
|
|
1241
|
+
const direct = configured[toolName] ?? configured[toolName.toLowerCase()];
|
|
1242
|
+
if (direct === "allow" || direct === "deny") return direct;
|
|
1243
|
+
const lower = toolName.toLowerCase();
|
|
1244
|
+
for (const [key, behavior] of Object.entries(configured)) {
|
|
1245
|
+
if (key.toLowerCase() === lower && (behavior === "allow" || behavior === "deny")) {
|
|
1246
|
+
return behavior;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return this.config.controlRequestBehavior ?? "allow";
|
|
1251
|
+
}
|
|
1252
|
+
writeControlResponse(proc, requestId, response) {
|
|
1253
|
+
const payload = {
|
|
1254
|
+
type: "control_response",
|
|
1255
|
+
response: {
|
|
1256
|
+
subtype: "success",
|
|
1257
|
+
request_id: requestId,
|
|
1258
|
+
response
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
try {
|
|
1262
|
+
proc.stdin?.write(JSON.stringify(payload) + "\n");
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
log.warn("failed to write control response", {
|
|
1265
|
+
requestId,
|
|
1266
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Handle Claude stream-json control requests (`can_use_tool`, etc.) and
|
|
1272
|
+
* respond via stdin with a matching `control_response`.
|
|
1273
|
+
*/
|
|
1274
|
+
handleControlRequest(msg, proc) {
|
|
1275
|
+
if (msg.type !== "control_request") return false;
|
|
1276
|
+
const requestId = msg.request_id;
|
|
1277
|
+
const request = msg.request;
|
|
1278
|
+
if (!requestId || !request?.subtype) return false;
|
|
1279
|
+
if (request.subtype === "can_use_tool") {
|
|
1280
|
+
const toolName = request.tool_name ?? "unknown";
|
|
1281
|
+
const behavior = this.controlRequestBehaviorForTool(toolName);
|
|
1282
|
+
if (behavior === "allow") {
|
|
1283
|
+
this.writeControlResponse(proc, requestId, {
|
|
1284
|
+
behavior: "allow",
|
|
1285
|
+
updatedInput: request.input ?? {},
|
|
1286
|
+
toolUseID: request.tool_use_id
|
|
1287
|
+
});
|
|
1288
|
+
log.info("control request auto-allowed", {
|
|
1289
|
+
requestId,
|
|
1290
|
+
toolName
|
|
1291
|
+
});
|
|
1292
|
+
} else {
|
|
1293
|
+
this.writeControlResponse(proc, requestId, {
|
|
1294
|
+
behavior: "deny",
|
|
1295
|
+
message: this.config.controlRequestDenyMessage ?? `Denied by opencode-claude-code policy for tool ${toolName}`,
|
|
1296
|
+
toolUseID: request.tool_use_id
|
|
1297
|
+
});
|
|
1298
|
+
log.info("control request auto-denied", {
|
|
1299
|
+
requestId,
|
|
1300
|
+
toolName
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
this.writeControlResponse(proc, requestId, {});
|
|
1306
|
+
log.debug("control request acknowledged", {
|
|
1307
|
+
requestId,
|
|
1308
|
+
subtype: request.subtype
|
|
1309
|
+
});
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
getReasoningEffort(providerOptions) {
|
|
1313
|
+
if (!providerOptions) return void 0;
|
|
1314
|
+
const ownKey = this.config.provider;
|
|
1315
|
+
const bag = providerOptions[ownKey] ?? providerOptions["claude-code"];
|
|
1316
|
+
const effort = bag?.reasoningEffort;
|
|
1317
|
+
const valid = [
|
|
1318
|
+
"minimal",
|
|
1319
|
+
"low",
|
|
1320
|
+
"medium",
|
|
1321
|
+
"high",
|
|
1322
|
+
"xhigh",
|
|
1323
|
+
"max"
|
|
1324
|
+
];
|
|
1325
|
+
return valid.includes(effort) ? effort : void 0;
|
|
1326
|
+
}
|
|
1327
|
+
latestUserText(prompt) {
|
|
1328
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1329
|
+
const msg = prompt[i];
|
|
1330
|
+
if (msg.role !== "user") continue;
|
|
1331
|
+
if (typeof msg.content === "string") {
|
|
1332
|
+
return String(msg.content).trim();
|
|
1333
|
+
}
|
|
1334
|
+
if (Array.isArray(msg.content)) {
|
|
1335
|
+
const text = msg.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => String(part.text).trim()).filter(Boolean).join(" ");
|
|
1336
|
+
if (text) return text;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return "";
|
|
1340
|
+
}
|
|
1341
|
+
synthesizeTitle(prompt) {
|
|
1342
|
+
const source = this.latestUserText(prompt).replace(/\s+/g, " ").replace(/[^\p{L}\p{N}\s-]/gu, " ").trim();
|
|
1343
|
+
if (!source) return "New Session";
|
|
1344
|
+
const stop = /* @__PURE__ */ new Set([
|
|
1345
|
+
"a",
|
|
1346
|
+
"an",
|
|
1347
|
+
"the",
|
|
1348
|
+
"and",
|
|
1349
|
+
"or",
|
|
1350
|
+
"but",
|
|
1351
|
+
"to",
|
|
1352
|
+
"for",
|
|
1353
|
+
"of",
|
|
1354
|
+
"in",
|
|
1355
|
+
"on",
|
|
1356
|
+
"at",
|
|
1357
|
+
"with",
|
|
1358
|
+
"can",
|
|
1359
|
+
"could",
|
|
1360
|
+
"would",
|
|
1361
|
+
"should",
|
|
1362
|
+
"please",
|
|
1363
|
+
"hi",
|
|
1364
|
+
"hello",
|
|
1365
|
+
"hey",
|
|
1366
|
+
"there",
|
|
1367
|
+
"you",
|
|
1368
|
+
"your",
|
|
1369
|
+
"this",
|
|
1370
|
+
"that",
|
|
1371
|
+
"is",
|
|
1372
|
+
"are",
|
|
1373
|
+
"was",
|
|
1374
|
+
"were",
|
|
1375
|
+
"be",
|
|
1376
|
+
"do",
|
|
1377
|
+
"does",
|
|
1378
|
+
"did",
|
|
1379
|
+
"summarize",
|
|
1380
|
+
"summary",
|
|
1381
|
+
"project"
|
|
1382
|
+
]);
|
|
1383
|
+
const words = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
1384
|
+
const picked = (words.length > 0 ? words : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1385
|
+
return picked || "New Session";
|
|
1386
|
+
}
|
|
1387
|
+
async doGenerateViaStream(options) {
|
|
1388
|
+
const result = await this.doStream(options);
|
|
1389
|
+
const reader = result.stream.getReader();
|
|
1390
|
+
let text = "";
|
|
1391
|
+
let reasoning = "";
|
|
1392
|
+
const toolCalls = [];
|
|
1393
|
+
let finishReason = this.toFinishReason("stop");
|
|
1394
|
+
let usage = this.toUsage();
|
|
1395
|
+
let providerMetadata;
|
|
1396
|
+
while (true) {
|
|
1397
|
+
const { value, done } = await reader.read();
|
|
1398
|
+
if (done) break;
|
|
1399
|
+
switch (value.type) {
|
|
1400
|
+
case "text-delta":
|
|
1401
|
+
text += value.delta ?? "";
|
|
1402
|
+
break;
|
|
1403
|
+
case "reasoning-delta":
|
|
1404
|
+
reasoning += value.delta ?? "";
|
|
1405
|
+
break;
|
|
1406
|
+
case "tool-call":
|
|
1407
|
+
toolCalls.push({
|
|
1408
|
+
type: "tool-call",
|
|
1409
|
+
toolCallId: value.toolCallId,
|
|
1410
|
+
toolName: value.toolName,
|
|
1411
|
+
input: value.input,
|
|
1412
|
+
providerExecuted: value.providerExecuted
|
|
1413
|
+
});
|
|
1414
|
+
break;
|
|
1415
|
+
case "finish":
|
|
1416
|
+
finishReason = value.finishReason ?? finishReason;
|
|
1417
|
+
usage = value.usage ?? usage;
|
|
1418
|
+
providerMetadata = value.providerMetadata ?? providerMetadata;
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const content = [];
|
|
1423
|
+
if (reasoning) {
|
|
1424
|
+
content.push({ type: "reasoning", text: reasoning });
|
|
1425
|
+
}
|
|
1426
|
+
if (text) {
|
|
1427
|
+
content.push({ type: "text", text, providerMetadata });
|
|
1428
|
+
}
|
|
1429
|
+
content.push(...toolCalls);
|
|
1430
|
+
return {
|
|
1431
|
+
content,
|
|
1432
|
+
finishReason,
|
|
1433
|
+
usage,
|
|
1434
|
+
request: result.request,
|
|
1435
|
+
response: {
|
|
1436
|
+
id: generateId(),
|
|
1437
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1438
|
+
modelId: this.modelId
|
|
1439
|
+
},
|
|
1440
|
+
providerMetadata,
|
|
1441
|
+
warnings: []
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
async doGenerate(options) {
|
|
1445
|
+
const warnings = [];
|
|
1446
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
1447
|
+
const scope = this.requestScope(options);
|
|
1448
|
+
const affinity = this.sessionAffinity(options);
|
|
1449
|
+
const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
|
|
1450
|
+
if (scope === "tools" && this.resolvedProxyTools()) {
|
|
1451
|
+
return this.doGenerateViaStream(options);
|
|
1452
|
+
}
|
|
1453
|
+
if (scope === "no-tools") {
|
|
1454
|
+
const text = this.synthesizeTitle(options.prompt);
|
|
1455
|
+
return {
|
|
1456
|
+
content: [{ type: "text", text }],
|
|
1457
|
+
finishReason: this.toFinishReason("stop"),
|
|
1458
|
+
usage: this.toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
1459
|
+
request: { body: { text: "" } },
|
|
1460
|
+
response: {
|
|
1461
|
+
id: generateId(),
|
|
1462
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1463
|
+
modelId: this.modelId
|
|
1464
|
+
},
|
|
1465
|
+
providerMetadata: {
|
|
1466
|
+
"claude-code": {
|
|
1467
|
+
synthetic: true,
|
|
1468
|
+
path: "no-tools"
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
warnings
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
1475
|
+
if (!hasPriorConversation) {
|
|
1476
|
+
deleteClaudeSessionId(sk);
|
|
1477
|
+
deleteActiveProcess(sk);
|
|
1478
|
+
}
|
|
1479
|
+
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
1480
|
+
const includeHistoryContext = !hasExistingSession && hasPriorConversation;
|
|
1481
|
+
const reasoningEffort = this.getReasoningEffort(options.providerOptions);
|
|
1482
|
+
const userMsg = getClaudeUserMessage(
|
|
1483
|
+
options.prompt,
|
|
1484
|
+
includeHistoryContext,
|
|
1485
|
+
reasoningEffort
|
|
1486
|
+
);
|
|
1487
|
+
const cliArgs = buildCliArgs({
|
|
1488
|
+
sessionKey: sk,
|
|
1489
|
+
skipPermissions: this.config.skipPermissions !== false,
|
|
1490
|
+
includeSessionId: false,
|
|
1491
|
+
model: this.modelId,
|
|
1492
|
+
permissionMode: this.config.permissionMode,
|
|
1493
|
+
mcpConfig: this.effectiveMcpConfig(cwd),
|
|
1494
|
+
strictMcpConfig: this.config.strictMcpConfig
|
|
1495
|
+
});
|
|
1496
|
+
log.info("doGenerate starting", {
|
|
1497
|
+
cwd,
|
|
1498
|
+
model: this.modelId,
|
|
1499
|
+
textLength: userMsg.length,
|
|
1500
|
+
includeHistoryContext
|
|
1501
|
+
});
|
|
1502
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
1503
|
+
const { createInterface: createInterface2 } = await import("readline");
|
|
1504
|
+
const proc = spawn2(this.config.cliPath, cliArgs, {
|
|
1505
|
+
cwd,
|
|
1506
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1507
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
1508
|
+
shell: process.platform === "win32"
|
|
1509
|
+
});
|
|
1510
|
+
const rl = createInterface2({ input: proc.stdout });
|
|
1511
|
+
let responseText = "";
|
|
1512
|
+
let thinkingText = "";
|
|
1513
|
+
let resultMeta = {};
|
|
1514
|
+
const toolCalls = [];
|
|
1515
|
+
const result = await new Promise((resolve2, reject) => {
|
|
1516
|
+
rl.on("line", (line) => {
|
|
1517
|
+
if (!line.trim()) return;
|
|
1518
|
+
try {
|
|
1519
|
+
const msg = JSON.parse(line);
|
|
1520
|
+
if (this.handleControlRequest(msg, proc)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
1524
|
+
if (msg.session_id) {
|
|
1525
|
+
setClaudeSessionId(sk, msg.session_id);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
1529
|
+
for (const block of msg.message.content) {
|
|
1530
|
+
if (block.type === "text" && block.text) {
|
|
1531
|
+
responseText += block.text;
|
|
1532
|
+
}
|
|
1533
|
+
if (block.type === "thinking" && block.thinking) {
|
|
1534
|
+
thinkingText += block.thinking;
|
|
1535
|
+
}
|
|
1536
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
1537
|
+
if (block.name === "AskUserQuestion" || block.name === "ask_user_question") {
|
|
1538
|
+
const parsedInput = block.input ?? {};
|
|
1539
|
+
const question = parsedInput?.question || "Question?";
|
|
1540
|
+
responseText += `
|
|
1541
|
+
|
|
1542
|
+
_Asking: ${question}_
|
|
1543
|
+
|
|
1544
|
+
`;
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (block.name === "ExitPlanMode") {
|
|
1548
|
+
const parsedInput = block.input ?? {};
|
|
1549
|
+
const plan = parsedInput?.plan || "";
|
|
1550
|
+
responseText += `
|
|
1551
|
+
|
|
1552
|
+
${plan}
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
**Do you want to proceed with this plan?** (yes/no)
|
|
1556
|
+
`;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
toolCalls.push({
|
|
1560
|
+
id: block.id,
|
|
1561
|
+
name: block.name,
|
|
1562
|
+
args: block.input ?? {}
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (msg.type === "content_block_start" && msg.content_block) {
|
|
1568
|
+
if (msg.content_block.type === "tool_use" && msg.content_block.id && msg.content_block.name) {
|
|
1569
|
+
toolCalls.push({
|
|
1570
|
+
id: msg.content_block.id,
|
|
1571
|
+
name: msg.content_block.name,
|
|
1572
|
+
args: {}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (msg.type === "content_block_delta" && msg.delta) {
|
|
1577
|
+
if (msg.delta.type === "text_delta" && msg.delta.text) {
|
|
1578
|
+
responseText += msg.delta.text;
|
|
1579
|
+
}
|
|
1580
|
+
if (msg.delta.type === "thinking_delta" && msg.delta.thinking) {
|
|
1581
|
+
thinkingText += msg.delta.thinking;
|
|
1582
|
+
}
|
|
1583
|
+
if (msg.delta.type === "input_json_delta" && msg.delta.partial_json && msg.index !== void 0) {
|
|
1584
|
+
const tc = toolCalls[msg.index];
|
|
1585
|
+
if (tc) {
|
|
1586
|
+
try {
|
|
1587
|
+
tc.args = JSON.parse(msg.delta.partial_json);
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (msg.type === "result") {
|
|
1594
|
+
if (msg.session_id) {
|
|
1595
|
+
setClaudeSessionId(sk, msg.session_id);
|
|
1596
|
+
}
|
|
1597
|
+
if (!responseText && msg.is_error && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
1598
|
+
responseText = msg.result;
|
|
1599
|
+
}
|
|
1600
|
+
resultMeta = {
|
|
1601
|
+
sessionId: msg.session_id,
|
|
1602
|
+
costUsd: msg.total_cost_usd,
|
|
1603
|
+
durationMs: msg.duration_ms,
|
|
1604
|
+
usage: msg.usage
|
|
1605
|
+
};
|
|
1606
|
+
resolve2({
|
|
1607
|
+
...resultMeta,
|
|
1608
|
+
text: responseText,
|
|
1609
|
+
thinking: thinkingText,
|
|
1610
|
+
toolCalls
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
} catch {
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
rl.on("close", () => {
|
|
1617
|
+
resolve2({
|
|
1618
|
+
...resultMeta,
|
|
1619
|
+
text: responseText,
|
|
1620
|
+
thinking: thinkingText,
|
|
1621
|
+
toolCalls
|
|
1622
|
+
});
|
|
1623
|
+
});
|
|
1624
|
+
proc.on("error", (err) => {
|
|
1625
|
+
log.error("process error", { error: err.message });
|
|
1626
|
+
reject(err);
|
|
1627
|
+
});
|
|
1628
|
+
proc.stderr?.on("data", (data) => {
|
|
1629
|
+
log.debug("stderr", { data: data.toString().slice(0, 200) });
|
|
1630
|
+
});
|
|
1631
|
+
proc.stdin?.write(userMsg + "\n");
|
|
1632
|
+
});
|
|
1633
|
+
const content = [];
|
|
1634
|
+
if (result.thinking) {
|
|
1635
|
+
content.push({
|
|
1636
|
+
type: "reasoning",
|
|
1637
|
+
text: result.thinking
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
if (result.text) {
|
|
1641
|
+
content.push({
|
|
1642
|
+
type: "text",
|
|
1643
|
+
text: result.text,
|
|
1644
|
+
providerMetadata: {
|
|
1645
|
+
"claude-code": {
|
|
1646
|
+
sessionId: result.sessionId ?? null,
|
|
1647
|
+
costUsd: result.costUsd ?? null,
|
|
1648
|
+
durationMs: result.durationMs ?? null
|
|
1649
|
+
},
|
|
1650
|
+
...typeof result.usage?.cache_creation_input_tokens === "number" ? {
|
|
1651
|
+
anthropic: {
|
|
1652
|
+
cacheCreationInputTokens: result.usage.cache_creation_input_tokens
|
|
1653
|
+
}
|
|
1654
|
+
} : {}
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
for (const tc of result.toolCalls) {
|
|
1659
|
+
const {
|
|
1660
|
+
name: mappedName,
|
|
1661
|
+
input: mappedInput,
|
|
1662
|
+
executed,
|
|
1663
|
+
skip
|
|
1664
|
+
} = mapTool(tc.name, tc.args);
|
|
1665
|
+
if (skip) continue;
|
|
1666
|
+
content.push({
|
|
1667
|
+
type: "tool-call",
|
|
1668
|
+
toolCallId: tc.id,
|
|
1669
|
+
toolName: mappedName,
|
|
1670
|
+
input: JSON.stringify(mappedInput),
|
|
1671
|
+
providerExecuted: executed
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
const usage = this.toUsage(result.usage);
|
|
1675
|
+
return {
|
|
1676
|
+
content,
|
|
1677
|
+
// Claude CLI's `result` message signals a fully-completed turn —
|
|
1678
|
+
// tools have already been executed internally and final assistant
|
|
1679
|
+
// text has been produced. Always report "stop" so opencode doesn't
|
|
1680
|
+
// loop expecting to run tools itself.
|
|
1681
|
+
finishReason: this.toFinishReason("stop"),
|
|
1682
|
+
usage,
|
|
1683
|
+
request: { body: { text: userMsg } },
|
|
1684
|
+
response: {
|
|
1685
|
+
id: result.sessionId ?? generateId(),
|
|
1686
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1687
|
+
modelId: this.modelId
|
|
1688
|
+
},
|
|
1689
|
+
providerMetadata: {
|
|
1690
|
+
"claude-code": {
|
|
1691
|
+
sessionId: result.sessionId ?? null,
|
|
1692
|
+
costUsd: result.costUsd ?? null,
|
|
1693
|
+
durationMs: result.durationMs ?? null
|
|
1694
|
+
},
|
|
1695
|
+
...typeof result.usage?.cache_creation_input_tokens === "number" ? {
|
|
1696
|
+
anthropic: {
|
|
1697
|
+
cacheCreationInputTokens: result.usage.cache_creation_input_tokens
|
|
1698
|
+
}
|
|
1699
|
+
} : {}
|
|
1700
|
+
},
|
|
1701
|
+
warnings
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
async doStream(options) {
|
|
1705
|
+
const warnings = [];
|
|
1706
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
1707
|
+
const cliPath = this.config.cliPath;
|
|
1708
|
+
const skipPermissions = this.config.skipPermissions !== false;
|
|
1709
|
+
const scope = this.requestScope(options);
|
|
1710
|
+
const affinity = this.sessionAffinity(options);
|
|
1711
|
+
const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
|
|
1712
|
+
const toUsage = this.toUsage.bind(this);
|
|
1713
|
+
const toFinishReason = this.toFinishReason.bind(this);
|
|
1714
|
+
const handleControlRequest = this.handleControlRequest.bind(this);
|
|
1715
|
+
if (scope === "no-tools") {
|
|
1716
|
+
const text = this.synthesizeTitle(options.prompt);
|
|
1717
|
+
const textId = generateId();
|
|
1718
|
+
const stream2 = new ReadableStream({
|
|
1719
|
+
start(controller) {
|
|
1720
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
1721
|
+
controller.enqueue({ type: "text-start", id: textId });
|
|
1722
|
+
controller.enqueue({
|
|
1723
|
+
type: "text-delta",
|
|
1724
|
+
id: textId,
|
|
1725
|
+
delta: text
|
|
1726
|
+
});
|
|
1727
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
1728
|
+
controller.enqueue({
|
|
1729
|
+
type: "finish",
|
|
1730
|
+
finishReason: toFinishReason("stop"),
|
|
1731
|
+
usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
|
|
1732
|
+
providerMetadata: {
|
|
1733
|
+
"claude-code": {
|
|
1734
|
+
synthetic: true,
|
|
1735
|
+
path: "no-tools"
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
controller.close();
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
return {
|
|
1743
|
+
stream: stream2,
|
|
1744
|
+
request: { body: { text: "" } }
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
1748
|
+
if (!hasPriorConversation) {
|
|
1749
|
+
deleteClaudeSessionId(sk);
|
|
1750
|
+
deleteActiveProcess(sk);
|
|
1751
|
+
}
|
|
1752
|
+
const hasExistingSession = !!getClaudeSessionId(sk);
|
|
1753
|
+
const hasActiveProcess = !!getActiveProcess(sk);
|
|
1754
|
+
const includeHistoryContext = !hasExistingSession && !hasActiveProcess && hasPriorConversation;
|
|
1755
|
+
const reasoningEffort = this.getReasoningEffort(options.providerOptions);
|
|
1756
|
+
const userMsg = getClaudeUserMessage(
|
|
1757
|
+
options.prompt,
|
|
1758
|
+
includeHistoryContext,
|
|
1759
|
+
reasoningEffort
|
|
1760
|
+
);
|
|
1761
|
+
const resolvedProxy = this.resolvedProxyTools();
|
|
1762
|
+
const self = this;
|
|
1763
|
+
const pendingProxyCall = getPendingProxyCall(sk);
|
|
1764
|
+
const pendingProxyResult = pendingProxyCall ? this.extractPendingProxyResult(options.prompt, pendingProxyCall.toolCallId) : null;
|
|
1765
|
+
log.info("doStream starting", {
|
|
1766
|
+
cwd,
|
|
1767
|
+
model: this.modelId,
|
|
1768
|
+
textLength: userMsg.length,
|
|
1769
|
+
includeHistoryContext,
|
|
1770
|
+
hasActiveProcess,
|
|
1771
|
+
reasoningEffort,
|
|
1772
|
+
proxyTools: resolvedProxy?.map((t) => t.name) ?? null
|
|
1773
|
+
});
|
|
1774
|
+
const stream = new ReadableStream({
|
|
1775
|
+
start(controller) {
|
|
1776
|
+
let activeProcess = getActiveProcess(sk);
|
|
1777
|
+
let proc;
|
|
1778
|
+
let lineEmitter;
|
|
1779
|
+
let proxyServer = activeProcess?.proxyServer ?? null;
|
|
1780
|
+
const setup = async () => {
|
|
1781
|
+
if (!proxyServer && resolvedProxy) {
|
|
1782
|
+
proxyServer = await self.ensureProxyServer(resolvedProxy, sk);
|
|
1783
|
+
}
|
|
1784
|
+
const cliArgs = buildCliArgs({
|
|
1785
|
+
sessionKey: sk,
|
|
1786
|
+
skipPermissions,
|
|
1787
|
+
model: self.modelId,
|
|
1788
|
+
permissionMode: self.config.permissionMode,
|
|
1789
|
+
mcpConfig: self.effectiveMcpConfig(cwd, proxyServer?.configPath()),
|
|
1790
|
+
strictMcpConfig: self.config.strictMcpConfig,
|
|
1791
|
+
disallowedTools: resolvedProxy ? disallowedToolFlags(resolvedProxy) : void 0
|
|
1792
|
+
});
|
|
1793
|
+
if (activeProcess) {
|
|
1794
|
+
proc = activeProcess.proc;
|
|
1795
|
+
lineEmitter = activeProcess.lineEmitter;
|
|
1796
|
+
log.debug("reusing active process", { sk });
|
|
1797
|
+
} else {
|
|
1798
|
+
const ap = spawnClaudeProcess(cliPath, cliArgs, cwd, sk, proxyServer);
|
|
1799
|
+
proc = ap.proc;
|
|
1800
|
+
lineEmitter = ap.lineEmitter;
|
|
1801
|
+
activeProcess = ap;
|
|
1802
|
+
}
|
|
1803
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
1804
|
+
let currentTextId = null;
|
|
1805
|
+
const textBlockIndices = /* @__PURE__ */ new Set();
|
|
1806
|
+
const startTextBlock = () => {
|
|
1807
|
+
if (currentTextId) {
|
|
1808
|
+
controller.enqueue({ type: "text-end", id: currentTextId });
|
|
1809
|
+
}
|
|
1810
|
+
const id = generateId();
|
|
1811
|
+
currentTextId = id;
|
|
1812
|
+
controller.enqueue({ type: "text-start", id });
|
|
1813
|
+
return id;
|
|
1814
|
+
};
|
|
1815
|
+
const endTextBlock = () => {
|
|
1816
|
+
if (currentTextId) {
|
|
1817
|
+
controller.enqueue({ type: "text-end", id: currentTextId });
|
|
1818
|
+
currentTextId = null;
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const reasoningIds = /* @__PURE__ */ new Map();
|
|
1822
|
+
const reasoningStarted = /* @__PURE__ */ new Map();
|
|
1823
|
+
let turnCompleted = false;
|
|
1824
|
+
let controllerClosed = false;
|
|
1825
|
+
let pendingProxyUnsubscribe = null;
|
|
1826
|
+
let resultFallbackTimer = null;
|
|
1827
|
+
let hasReceivedContent = false;
|
|
1828
|
+
const clearFallbackTimer = () => {
|
|
1829
|
+
if (resultFallbackTimer) {
|
|
1830
|
+
clearTimeout(resultFallbackTimer);
|
|
1831
|
+
resultFallbackTimer = null;
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
const startResultFallback = () => {
|
|
1835
|
+
clearFallbackTimer();
|
|
1836
|
+
if (!hasReceivedContent || controllerClosed) return;
|
|
1837
|
+
resultFallbackTimer = setTimeout(() => {
|
|
1838
|
+
if (controllerClosed) return;
|
|
1839
|
+
log.warn("result fallback timer fired \u2014 closing stream without result event");
|
|
1840
|
+
closeHandler();
|
|
1841
|
+
}, 5e3);
|
|
1842
|
+
};
|
|
1843
|
+
const toolCallMap = /* @__PURE__ */ new Map();
|
|
1844
|
+
const skipResultForIds = /* @__PURE__ */ new Set();
|
|
1845
|
+
const toolCallsById = /* @__PURE__ */ new Map();
|
|
1846
|
+
let resultMeta = {};
|
|
1847
|
+
const finishWithToolCall = (call) => {
|
|
1848
|
+
if (controllerClosed) return;
|
|
1849
|
+
controller.enqueue({
|
|
1850
|
+
type: "tool-input-start",
|
|
1851
|
+
id: call.toolCallId,
|
|
1852
|
+
toolName: call.toolName
|
|
1853
|
+
});
|
|
1854
|
+
controller.enqueue({
|
|
1855
|
+
type: "tool-call",
|
|
1856
|
+
toolCallId: call.toolCallId,
|
|
1857
|
+
toolName: call.toolName,
|
|
1858
|
+
input: JSON.stringify(call.input),
|
|
1859
|
+
providerExecuted: false
|
|
1860
|
+
});
|
|
1861
|
+
skipResultForIds.add(call.toolCallId);
|
|
1862
|
+
controller.enqueue({
|
|
1863
|
+
type: "finish",
|
|
1864
|
+
finishReason: toFinishReason("tool-calls"),
|
|
1865
|
+
usage: toUsage(resultMeta.usage),
|
|
1866
|
+
providerMetadata: {
|
|
1867
|
+
"claude-code": resultMeta
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
controllerClosed = true;
|
|
1871
|
+
lineEmitter.off("line", lineHandler);
|
|
1872
|
+
lineEmitter.off("close", closeHandler);
|
|
1873
|
+
pendingProxyUnsubscribe?.();
|
|
1874
|
+
pendingProxyUnsubscribe = null;
|
|
1875
|
+
try {
|
|
1876
|
+
controller.close();
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
const lineHandler = (line) => {
|
|
1881
|
+
if (!line.trim()) return;
|
|
1882
|
+
if (controllerClosed) return;
|
|
1883
|
+
try {
|
|
1884
|
+
const msg = JSON.parse(line);
|
|
1885
|
+
if (handleControlRequest(msg, proc)) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
log.debug("stream message", {
|
|
1889
|
+
type: msg.type,
|
|
1890
|
+
subtype: msg.subtype
|
|
1891
|
+
});
|
|
1892
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
1893
|
+
if (msg.session_id) {
|
|
1894
|
+
setClaudeSessionId(sk, msg.session_id);
|
|
1895
|
+
log.info("session initialized", {
|
|
1896
|
+
claudeSessionId: msg.session_id
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
|
|
1901
|
+
const block = msg.content_block;
|
|
1902
|
+
const idx = msg.index;
|
|
1903
|
+
if (block.type === "thinking") {
|
|
1904
|
+
const reasoningId = generateId();
|
|
1905
|
+
reasoningIds.set(idx, reasoningId);
|
|
1906
|
+
controller.enqueue({
|
|
1907
|
+
type: "reasoning-start",
|
|
1908
|
+
id: reasoningId
|
|
1909
|
+
});
|
|
1910
|
+
reasoningStarted.set(idx, true);
|
|
1911
|
+
}
|
|
1912
|
+
if (block.type === "text") {
|
|
1913
|
+
clearFallbackTimer();
|
|
1914
|
+
textBlockIndices.add(idx);
|
|
1915
|
+
if (block.text) {
|
|
1916
|
+
if (!currentTextId) startTextBlock();
|
|
1917
|
+
controller.enqueue({
|
|
1918
|
+
type: "text-delta",
|
|
1919
|
+
id: currentTextId,
|
|
1920
|
+
delta: block.text
|
|
1921
|
+
});
|
|
1922
|
+
hasReceivedContent = true;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
1926
|
+
clearFallbackTimer();
|
|
1927
|
+
toolCallMap.set(idx, {
|
|
1928
|
+
id: block.id,
|
|
1929
|
+
name: block.name,
|
|
1930
|
+
inputJson: ""
|
|
1931
|
+
});
|
|
1932
|
+
if (block.name !== "AskUserQuestion" && block.name !== "ask_user_question" && block.name !== "ExitPlanMode" && !block.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
1933
|
+
const { name: mappedName, skip, executed } = mapTool(block.name);
|
|
1934
|
+
if (!skip) {
|
|
1935
|
+
controller.enqueue({
|
|
1936
|
+
type: "tool-input-start",
|
|
1937
|
+
id: block.id,
|
|
1938
|
+
toolName: mappedName,
|
|
1939
|
+
providerExecuted: executed
|
|
1940
|
+
});
|
|
1941
|
+
log.info("tool started", {
|
|
1942
|
+
name: block.name,
|
|
1943
|
+
mappedName,
|
|
1944
|
+
id: block.id
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (msg.type === "content_block_delta" && msg.delta && msg.index !== void 0) {
|
|
1951
|
+
const delta = msg.delta;
|
|
1952
|
+
const idx = msg.index;
|
|
1953
|
+
if (delta.type === "thinking_delta" && delta.thinking) {
|
|
1954
|
+
const reasoningId = reasoningIds.get(idx);
|
|
1955
|
+
if (reasoningId) {
|
|
1956
|
+
controller.enqueue({
|
|
1957
|
+
type: "reasoning-delta",
|
|
1958
|
+
id: reasoningId,
|
|
1959
|
+
delta: delta.thinking
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
1964
|
+
if (!currentTextId) startTextBlock();
|
|
1965
|
+
controller.enqueue({
|
|
1966
|
+
type: "text-delta",
|
|
1967
|
+
id: currentTextId,
|
|
1968
|
+
delta: delta.text
|
|
1969
|
+
});
|
|
1970
|
+
hasReceivedContent = true;
|
|
1971
|
+
}
|
|
1972
|
+
if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
1973
|
+
const tc = toolCallMap.get(idx);
|
|
1974
|
+
if (tc) {
|
|
1975
|
+
tc.inputJson += delta.partial_json;
|
|
1976
|
+
controller.enqueue({
|
|
1977
|
+
type: "tool-input-delta",
|
|
1978
|
+
id: tc.id,
|
|
1979
|
+
delta: delta.partial_json
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
if (msg.type === "content_block_stop" && msg.index !== void 0) {
|
|
1985
|
+
const idx = msg.index;
|
|
1986
|
+
const reasoningId = reasoningIds.get(idx);
|
|
1987
|
+
if (reasoningId && reasoningStarted.get(idx)) {
|
|
1988
|
+
controller.enqueue({
|
|
1989
|
+
type: "reasoning-end",
|
|
1990
|
+
id: reasoningId
|
|
1991
|
+
});
|
|
1992
|
+
reasoningStarted.delete(idx);
|
|
1993
|
+
}
|
|
1994
|
+
if (textBlockIndices.has(idx)) {
|
|
1995
|
+
endTextBlock();
|
|
1996
|
+
textBlockIndices.delete(idx);
|
|
1997
|
+
startResultFallback();
|
|
1998
|
+
}
|
|
1999
|
+
const tc = toolCallMap.get(idx);
|
|
2000
|
+
if (tc) {
|
|
2001
|
+
let parsedInput = {};
|
|
2002
|
+
try {
|
|
2003
|
+
parsedInput = JSON.parse(tc.inputJson || "{}");
|
|
2004
|
+
} catch {
|
|
2005
|
+
}
|
|
2006
|
+
if (tc.name === "AskUserQuestion" || tc.name === "ask_user_question") {
|
|
2007
|
+
let question = "Question?";
|
|
2008
|
+
if (parsedInput?.questions && Array.isArray(parsedInput.questions) && parsedInput.questions.length > 0) {
|
|
2009
|
+
question = parsedInput.questions[0].question || parsedInput.questions[0].text || "Question?";
|
|
2010
|
+
} else {
|
|
2011
|
+
question = parsedInput?.question || parsedInput?.text || "Question?";
|
|
2012
|
+
}
|
|
2013
|
+
const askId = startTextBlock();
|
|
2014
|
+
controller.enqueue({
|
|
2015
|
+
type: "text-delta",
|
|
2016
|
+
id: askId,
|
|
2017
|
+
delta: `
|
|
2018
|
+
|
|
2019
|
+
_Asking: ${question}_
|
|
2020
|
+
|
|
2021
|
+
`
|
|
2022
|
+
});
|
|
2023
|
+
endTextBlock();
|
|
2024
|
+
} else if (tc.name === "ExitPlanMode") {
|
|
2025
|
+
const plan = parsedInput?.plan || "";
|
|
2026
|
+
const planId = startTextBlock();
|
|
2027
|
+
controller.enqueue({
|
|
2028
|
+
type: "text-delta",
|
|
2029
|
+
id: planId,
|
|
2030
|
+
delta: `
|
|
2031
|
+
|
|
2032
|
+
${plan}
|
|
2033
|
+
|
|
2034
|
+
---
|
|
2035
|
+
**Do you want to proceed with this plan?** (yes/no)
|
|
2036
|
+
`
|
|
2037
|
+
});
|
|
2038
|
+
endTextBlock();
|
|
2039
|
+
} else if (tc.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
2040
|
+
log.debug("ignoring proxy tool_use block; broker handles it", {
|
|
2041
|
+
name: tc.name,
|
|
2042
|
+
id: tc.id
|
|
2043
|
+
});
|
|
2044
|
+
} else {
|
|
2045
|
+
const {
|
|
2046
|
+
name: mappedName,
|
|
2047
|
+
input: mappedInput,
|
|
2048
|
+
executed,
|
|
2049
|
+
skip
|
|
2050
|
+
} = mapTool(tc.name, parsedInput);
|
|
2051
|
+
if (!skip) {
|
|
2052
|
+
toolCallsById.set(tc.id, {
|
|
2053
|
+
id: tc.id,
|
|
2054
|
+
name: tc.name,
|
|
2055
|
+
input: parsedInput
|
|
2056
|
+
});
|
|
2057
|
+
if (!executed) skipResultForIds.add(tc.id);
|
|
2058
|
+
controller.enqueue({
|
|
2059
|
+
type: "tool-call",
|
|
2060
|
+
toolCallId: tc.id,
|
|
2061
|
+
toolName: mappedName,
|
|
2062
|
+
input: JSON.stringify(mappedInput),
|
|
2063
|
+
providerExecuted: executed
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
log.info("tool call complete", {
|
|
2067
|
+
name: tc.name,
|
|
2068
|
+
mappedName,
|
|
2069
|
+
id: tc.id,
|
|
2070
|
+
executed
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
2076
|
+
const hasText = msg.message.content.some(
|
|
2077
|
+
(b) => b.type === "text" && b.text
|
|
2078
|
+
);
|
|
2079
|
+
const hasToolUse = msg.message.content.some(
|
|
2080
|
+
(b) => b.type === "tool_use"
|
|
2081
|
+
);
|
|
2082
|
+
if (hasText) {
|
|
2083
|
+
hasReceivedContent = true;
|
|
2084
|
+
}
|
|
2085
|
+
if (hasText && !hasToolUse) {
|
|
2086
|
+
startResultFallback();
|
|
2087
|
+
}
|
|
2088
|
+
if (hasToolUse) {
|
|
2089
|
+
clearFallbackTimer();
|
|
2090
|
+
}
|
|
2091
|
+
for (const block of msg.message.content) {
|
|
2092
|
+
if (block.type === "text" && block.text) {
|
|
2093
|
+
const blockId = startTextBlock();
|
|
2094
|
+
controller.enqueue({
|
|
2095
|
+
type: "text-delta",
|
|
2096
|
+
id: blockId,
|
|
2097
|
+
delta: block.text
|
|
2098
|
+
});
|
|
2099
|
+
endTextBlock();
|
|
2100
|
+
hasReceivedContent = true;
|
|
2101
|
+
}
|
|
2102
|
+
if (block.type === "thinking" && block.thinking) {
|
|
2103
|
+
const thinkingId = generateId();
|
|
2104
|
+
controller.enqueue({
|
|
2105
|
+
type: "reasoning-start",
|
|
2106
|
+
id: thinkingId
|
|
2107
|
+
});
|
|
2108
|
+
controller.enqueue({
|
|
2109
|
+
type: "reasoning-delta",
|
|
2110
|
+
id: thinkingId,
|
|
2111
|
+
delta: block.thinking
|
|
2112
|
+
});
|
|
2113
|
+
controller.enqueue({
|
|
2114
|
+
type: "reasoning-end",
|
|
2115
|
+
id: thinkingId
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
2119
|
+
const parsedInput = block.input ?? {};
|
|
2120
|
+
toolCallsById.set(block.id, {
|
|
2121
|
+
id: block.id,
|
|
2122
|
+
name: block.name,
|
|
2123
|
+
input: parsedInput
|
|
2124
|
+
});
|
|
2125
|
+
if (block.name === "AskUserQuestion" || block.name === "ask_user_question") {
|
|
2126
|
+
let question = "Question?";
|
|
2127
|
+
if (parsedInput?.questions && Array.isArray(parsedInput.questions) && parsedInput.questions.length > 0) {
|
|
2128
|
+
const q = parsedInput.questions[0];
|
|
2129
|
+
question = q.question || q.text || "Question?";
|
|
2130
|
+
} else {
|
|
2131
|
+
question = parsedInput?.question || parsedInput?.text || "Question?";
|
|
2132
|
+
}
|
|
2133
|
+
const askId = startTextBlock();
|
|
2134
|
+
controller.enqueue({
|
|
2135
|
+
type: "text-delta",
|
|
2136
|
+
id: askId,
|
|
2137
|
+
delta: `
|
|
2138
|
+
|
|
2139
|
+
_Asking: ${question}_
|
|
2140
|
+
|
|
2141
|
+
`
|
|
2142
|
+
});
|
|
2143
|
+
endTextBlock();
|
|
2144
|
+
} else if (block.name === "ExitPlanMode") {
|
|
2145
|
+
const plan = parsedInput?.plan || "";
|
|
2146
|
+
const planId = startTextBlock();
|
|
2147
|
+
controller.enqueue({
|
|
2148
|
+
type: "text-delta",
|
|
2149
|
+
id: planId,
|
|
2150
|
+
delta: `
|
|
2151
|
+
|
|
2152
|
+
${plan}
|
|
2153
|
+
|
|
2154
|
+
---
|
|
2155
|
+
**Do you want to proceed with this plan?** (yes/no)
|
|
2156
|
+
`
|
|
2157
|
+
});
|
|
2158
|
+
endTextBlock();
|
|
2159
|
+
} else if (block.name.startsWith(PROXY_TOOL_PREFIX)) {
|
|
2160
|
+
log.debug("ignoring proxy tool_use from assistant message", {
|
|
2161
|
+
name: block.name,
|
|
2162
|
+
id: block.id
|
|
2163
|
+
});
|
|
2164
|
+
} else {
|
|
2165
|
+
const {
|
|
2166
|
+
name: mappedName,
|
|
2167
|
+
input: mappedInput,
|
|
2168
|
+
executed,
|
|
2169
|
+
skip
|
|
2170
|
+
} = mapTool(block.name, parsedInput);
|
|
2171
|
+
if (!skip) {
|
|
2172
|
+
if (!executed) skipResultForIds.add(block.id);
|
|
2173
|
+
controller.enqueue({
|
|
2174
|
+
type: "tool-input-start",
|
|
2175
|
+
id: block.id,
|
|
2176
|
+
toolName: mappedName,
|
|
2177
|
+
providerExecuted: executed
|
|
2178
|
+
});
|
|
2179
|
+
controller.enqueue({
|
|
2180
|
+
type: "tool-call",
|
|
2181
|
+
toolCallId: block.id,
|
|
2182
|
+
toolName: mappedName,
|
|
2183
|
+
input: JSON.stringify(mappedInput),
|
|
2184
|
+
providerExecuted: executed
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
log.info("tool_use from assistant message", {
|
|
2188
|
+
name: block.name,
|
|
2189
|
+
mappedName,
|
|
2190
|
+
id: block.id,
|
|
2191
|
+
executed
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (block.type === "tool_result") {
|
|
2196
|
+
log.debug("tool_result", {
|
|
2197
|
+
toolUseId: block.tool_use_id
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (msg.type === "user" && msg.message?.content) {
|
|
2203
|
+
for (const block of msg.message.content) {
|
|
2204
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
2205
|
+
if (skipResultForIds.has(block.tool_use_id)) {
|
|
2206
|
+
log.debug("skipping tool-result (opencode runs it)", {
|
|
2207
|
+
toolUseId: block.tool_use_id
|
|
2208
|
+
});
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
const toolCall = toolCallsById.get(block.tool_use_id);
|
|
2212
|
+
if (toolCall) {
|
|
2213
|
+
let resultText = "";
|
|
2214
|
+
if (typeof block.content === "string") {
|
|
2215
|
+
resultText = block.content;
|
|
2216
|
+
} else if (Array.isArray(block.content)) {
|
|
2217
|
+
resultText = block.content.filter(
|
|
2218
|
+
(c) => c.type === "text" && typeof c.text === "string"
|
|
2219
|
+
).map((c) => c.text).join("\n");
|
|
2220
|
+
}
|
|
2221
|
+
controller.enqueue({
|
|
2222
|
+
type: "tool-result",
|
|
2223
|
+
toolCallId: block.tool_use_id,
|
|
2224
|
+
toolName: toolCall.name,
|
|
2225
|
+
result: {
|
|
2226
|
+
output: resultText,
|
|
2227
|
+
title: toolCall.name,
|
|
2228
|
+
metadata: {}
|
|
2229
|
+
},
|
|
2230
|
+
providerExecuted: true
|
|
2231
|
+
});
|
|
2232
|
+
log.info("tool result emitted", {
|
|
2233
|
+
toolUseId: block.tool_use_id,
|
|
2234
|
+
name: toolCall.name
|
|
2235
|
+
});
|
|
2236
|
+
toolCallsById.delete(block.tool_use_id);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (msg.type === "result") {
|
|
2242
|
+
clearFallbackTimer();
|
|
2243
|
+
if (msg.session_id) {
|
|
2244
|
+
setClaudeSessionId(sk, msg.session_id);
|
|
2245
|
+
}
|
|
2246
|
+
if (!currentTextId && msg.is_error && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
2247
|
+
const errId = startTextBlock();
|
|
2248
|
+
controller.enqueue({
|
|
2249
|
+
type: "text-delta",
|
|
2250
|
+
id: errId,
|
|
2251
|
+
delta: msg.result
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
resultMeta = {
|
|
2255
|
+
sessionId: msg.session_id,
|
|
2256
|
+
costUsd: msg.total_cost_usd,
|
|
2257
|
+
durationMs: msg.duration_ms,
|
|
2258
|
+
usage: msg.usage
|
|
2259
|
+
};
|
|
2260
|
+
log.info("conversation result", {
|
|
2261
|
+
sessionId: msg.session_id,
|
|
2262
|
+
durationMs: msg.duration_ms,
|
|
2263
|
+
numTurns: msg.num_turns,
|
|
2264
|
+
isError: msg.is_error
|
|
2265
|
+
});
|
|
2266
|
+
turnCompleted = true;
|
|
2267
|
+
endTextBlock();
|
|
2268
|
+
for (const [idx, reasoningId] of reasoningIds) {
|
|
2269
|
+
if (reasoningStarted.get(idx)) {
|
|
2270
|
+
controller.enqueue({
|
|
2271
|
+
type: "reasoning-end",
|
|
2272
|
+
id: reasoningId
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
controller.enqueue({
|
|
2277
|
+
type: "finish",
|
|
2278
|
+
finishReason: toFinishReason("stop"),
|
|
2279
|
+
usage: toUsage(msg.usage),
|
|
2280
|
+
providerMetadata: {
|
|
2281
|
+
"claude-code": resultMeta,
|
|
2282
|
+
...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
|
|
2283
|
+
anthropic: {
|
|
2284
|
+
cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
|
|
2285
|
+
}
|
|
2286
|
+
} : {}
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
controllerClosed = true;
|
|
2290
|
+
lineEmitter.off("line", lineHandler);
|
|
2291
|
+
lineEmitter.off("close", closeHandler);
|
|
2292
|
+
try {
|
|
2293
|
+
controller.close();
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
} catch (e) {
|
|
2298
|
+
log.debug("failed to parse line", {
|
|
2299
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
const closeHandler = () => {
|
|
2304
|
+
log.debug("readline closed");
|
|
2305
|
+
if (controllerClosed) return;
|
|
2306
|
+
clearFallbackTimer();
|
|
2307
|
+
controllerClosed = true;
|
|
2308
|
+
lineEmitter.off("line", lineHandler);
|
|
2309
|
+
lineEmitter.off("close", closeHandler);
|
|
2310
|
+
pendingProxyUnsubscribe?.();
|
|
2311
|
+
pendingProxyUnsubscribe = null;
|
|
2312
|
+
endTextBlock();
|
|
2313
|
+
controller.enqueue({
|
|
2314
|
+
type: "finish",
|
|
2315
|
+
finishReason: toFinishReason("stop"),
|
|
2316
|
+
usage: toUsage(),
|
|
2317
|
+
providerMetadata: {
|
|
2318
|
+
"claude-code": resultMeta
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
try {
|
|
2322
|
+
controller.close();
|
|
2323
|
+
} catch {
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
lineEmitter.on("line", lineHandler);
|
|
2327
|
+
lineEmitter.on("close", closeHandler);
|
|
2328
|
+
pendingProxyUnsubscribe = onPendingProxyCall(sk, (call) => {
|
|
2329
|
+
log.info("received pending proxy call for session", {
|
|
2330
|
+
sessionKey: sk,
|
|
2331
|
+
toolCallId: call.toolCallId,
|
|
2332
|
+
toolName: call.toolName
|
|
2333
|
+
});
|
|
2334
|
+
finishWithToolCall(call);
|
|
2335
|
+
});
|
|
2336
|
+
proc.on("error", (err) => {
|
|
2337
|
+
log.error("process error", { error: err.message });
|
|
2338
|
+
clearFallbackTimer();
|
|
2339
|
+
if (controllerClosed) return;
|
|
2340
|
+
controllerClosed = true;
|
|
2341
|
+
pendingProxyUnsubscribe?.();
|
|
2342
|
+
pendingProxyUnsubscribe = null;
|
|
2343
|
+
controller.enqueue({ type: "error", error: err });
|
|
2344
|
+
try {
|
|
2345
|
+
controller.close();
|
|
2346
|
+
} catch {
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
if (options.abortSignal) {
|
|
2350
|
+
options.abortSignal.addEventListener("abort", () => {
|
|
2351
|
+
if (turnCompleted || controllerClosed) return;
|
|
2352
|
+
if (!hasReceivedContent) {
|
|
2353
|
+
log.info(
|
|
2354
|
+
"abort signal received before content, closing stream immediately",
|
|
2355
|
+
{ cwd }
|
|
2356
|
+
);
|
|
2357
|
+
controllerClosed = true;
|
|
2358
|
+
lineEmitter.off("line", lineHandler);
|
|
2359
|
+
lineEmitter.off("close", closeHandler);
|
|
2360
|
+
pendingProxyUnsubscribe?.();
|
|
2361
|
+
pendingProxyUnsubscribe = null;
|
|
2362
|
+
try {
|
|
2363
|
+
controller.close();
|
|
2364
|
+
} catch {
|
|
2365
|
+
}
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
log.info(
|
|
2369
|
+
"abort signal received mid-turn, starting grace period",
|
|
2370
|
+
{ cwd }
|
|
2371
|
+
);
|
|
2372
|
+
startResultFallback();
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
if (pendingProxyCall && pendingProxyResult) {
|
|
2376
|
+
log.info("resolving pending proxy call from tool result prompt", {
|
|
2377
|
+
sessionKey: sk,
|
|
2378
|
+
toolCallId: pendingProxyCall.toolCallId,
|
|
2379
|
+
toolName: pendingProxyCall.toolName
|
|
2380
|
+
});
|
|
2381
|
+
const resolved = resolvePendingProxyCall(sk, pendingProxyResult);
|
|
2382
|
+
if (!resolved) {
|
|
2383
|
+
log.warn("failed to resolve pending proxy call; no pending state", {
|
|
2384
|
+
sessionKey: sk,
|
|
2385
|
+
toolCallId: pendingProxyCall.toolCallId
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
proc.stdin?.write(userMsg + "\n");
|
|
2391
|
+
log.debug("sent user message", { textLength: userMsg.length });
|
|
2392
|
+
};
|
|
2393
|
+
void setup().catch((err) => {
|
|
2394
|
+
log.error("failed to set up doStream", {
|
|
2395
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2396
|
+
});
|
|
2397
|
+
controller.enqueue({
|
|
2398
|
+
type: "error",
|
|
2399
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
2400
|
+
});
|
|
2401
|
+
try {
|
|
2402
|
+
controller.close();
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
},
|
|
2407
|
+
cancel() {
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
return {
|
|
2411
|
+
stream,
|
|
2412
|
+
request: { body: { text: userMsg } },
|
|
2413
|
+
response: { headers: {} }
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
// src/models.ts
|
|
2419
|
+
var PROVIDER_ID = "claude-code";
|
|
2420
|
+
var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
2421
|
+
var reasoningVariants = {
|
|
2422
|
+
low: { reasoningEffort: "low" },
|
|
2423
|
+
medium: { reasoningEffort: "medium" },
|
|
2424
|
+
high: { reasoningEffort: "high" },
|
|
2425
|
+
xhigh: { reasoningEffort: "xhigh" },
|
|
2426
|
+
max: { reasoningEffort: "max" }
|
|
2427
|
+
};
|
|
2428
|
+
var baseCapabilities = {
|
|
2429
|
+
temperature: false,
|
|
2430
|
+
attachment: true,
|
|
2431
|
+
toolcall: true,
|
|
2432
|
+
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
|
2433
|
+
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
2434
|
+
interleaved: false
|
|
2435
|
+
};
|
|
2436
|
+
function defineModel(opts) {
|
|
2437
|
+
return {
|
|
2438
|
+
id: opts.id,
|
|
2439
|
+
providerID: PROVIDER_ID,
|
|
2440
|
+
api: { id: opts.id, url: "", npm: NPM },
|
|
2441
|
+
name: opts.name,
|
|
2442
|
+
family: opts.family,
|
|
2443
|
+
capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
|
|
2444
|
+
cost: {
|
|
2445
|
+
input: opts.cost.input,
|
|
2446
|
+
output: opts.cost.output,
|
|
2447
|
+
cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
|
|
2448
|
+
},
|
|
2449
|
+
limit: { context: opts.context, output: opts.output },
|
|
2450
|
+
status: opts.status ?? "active",
|
|
2451
|
+
options: {},
|
|
2452
|
+
headers: {},
|
|
2453
|
+
release_date: opts.releaseDate,
|
|
2454
|
+
variants: opts.reasoning ? reasoningVariants : void 0
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
var haikuCost = { input: 1e-6, output: 5e-6, cacheRead: 1e-7, cacheWrite: 125e-8 };
|
|
2458
|
+
var sonnetCost = { input: 3e-6, output: 15e-6, cacheRead: 3e-7, cacheWrite: 375e-8 };
|
|
2459
|
+
var opusCost = { input: 15e-6, output: 75e-6, cacheRead: 15e-7, cacheWrite: 1875e-8 };
|
|
2460
|
+
var defaultModels = {
|
|
2461
|
+
"claude-haiku-4-5": defineModel({
|
|
2462
|
+
id: "claude-haiku-4-5",
|
|
2463
|
+
name: "Claude Code Haiku 4.5",
|
|
2464
|
+
family: "haiku",
|
|
2465
|
+
reasoning: false,
|
|
2466
|
+
context: 2e5,
|
|
2467
|
+
output: 8192,
|
|
2468
|
+
cost: haikuCost,
|
|
2469
|
+
releaseDate: "2024-10-22"
|
|
2470
|
+
}),
|
|
2471
|
+
"claude-sonnet-4-5": defineModel({
|
|
2472
|
+
id: "claude-sonnet-4-5",
|
|
2473
|
+
name: "Claude Code Sonnet 4.5",
|
|
2474
|
+
family: "sonnet",
|
|
2475
|
+
reasoning: true,
|
|
2476
|
+
context: 1e6,
|
|
2477
|
+
output: 16384,
|
|
2478
|
+
cost: sonnetCost,
|
|
2479
|
+
releaseDate: "2025-04-14"
|
|
2480
|
+
}),
|
|
2481
|
+
"claude-sonnet-4-6": defineModel({
|
|
2482
|
+
id: "claude-sonnet-4-6",
|
|
2483
|
+
name: "Claude Code Sonnet 4.6",
|
|
2484
|
+
family: "sonnet",
|
|
2485
|
+
reasoning: true,
|
|
2486
|
+
context: 1e6,
|
|
2487
|
+
output: 16384,
|
|
2488
|
+
cost: sonnetCost,
|
|
2489
|
+
releaseDate: "2025-06-19"
|
|
2490
|
+
}),
|
|
2491
|
+
"claude-opus-4-5": defineModel({
|
|
2492
|
+
id: "claude-opus-4-5",
|
|
2493
|
+
name: "Claude Code Opus 4.5",
|
|
2494
|
+
family: "opus",
|
|
2495
|
+
reasoning: true,
|
|
2496
|
+
context: 1e6,
|
|
2497
|
+
output: 16384,
|
|
2498
|
+
cost: opusCost,
|
|
2499
|
+
releaseDate: "2025-04-14"
|
|
2500
|
+
}),
|
|
2501
|
+
"claude-opus-4-6": defineModel({
|
|
2502
|
+
id: "claude-opus-4-6",
|
|
2503
|
+
name: "Claude Code Opus 4.6",
|
|
2504
|
+
family: "opus",
|
|
2505
|
+
reasoning: true,
|
|
2506
|
+
context: 1e6,
|
|
2507
|
+
output: 16384,
|
|
2508
|
+
cost: opusCost,
|
|
2509
|
+
releaseDate: "2025-06-19"
|
|
2510
|
+
}),
|
|
2511
|
+
"claude-opus-4-7": defineModel({
|
|
2512
|
+
id: "claude-opus-4-7",
|
|
2513
|
+
name: "Claude Code Opus 4.7",
|
|
2514
|
+
family: "opus",
|
|
2515
|
+
reasoning: true,
|
|
2516
|
+
context: 1e6,
|
|
2517
|
+
output: 16384,
|
|
2518
|
+
cost: opusCost,
|
|
2519
|
+
releaseDate: "2025-07-16"
|
|
2520
|
+
})
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
// src/index.ts
|
|
2524
|
+
function createClaudeCode(settings = {}) {
|
|
2525
|
+
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
2526
|
+
const providerName = settings.name ?? "claude-code";
|
|
2527
|
+
const proxyTools = settings.proxyTools ?? ["Bash", "Edit", "Write", "WebFetch"];
|
|
2528
|
+
const createModel = (modelId) => {
|
|
2529
|
+
return new ClaudeCodeLanguageModel(modelId, {
|
|
2530
|
+
provider: providerName,
|
|
2531
|
+
cliPath,
|
|
2532
|
+
cwd: settings.cwd,
|
|
2533
|
+
skipPermissions: settings.skipPermissions ?? true,
|
|
2534
|
+
permissionMode: settings.permissionMode,
|
|
2535
|
+
mcpConfig: settings.mcpConfig,
|
|
2536
|
+
strictMcpConfig: settings.strictMcpConfig,
|
|
2537
|
+
bridgeOpencodeMcp: settings.bridgeOpencodeMcp ?? true,
|
|
2538
|
+
controlRequestBehavior: settings.controlRequestBehavior ?? "allow",
|
|
2539
|
+
controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
|
|
2540
|
+
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
2541
|
+
proxyTools
|
|
2542
|
+
});
|
|
2543
|
+
};
|
|
2544
|
+
const provider = function(modelId) {
|
|
2545
|
+
return createModel(modelId);
|
|
2546
|
+
};
|
|
2547
|
+
provider.specificationVersion = "v3";
|
|
2548
|
+
provider.languageModel = createModel;
|
|
2549
|
+
return provider;
|
|
2550
|
+
}
|
|
2551
|
+
var PROVIDER_ID2 = "claude-code";
|
|
2552
|
+
var PACKAGE_NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
|
|
2553
|
+
function pluginEntrypoint() {
|
|
2554
|
+
return import.meta.url.startsWith("file:") ? import.meta.url : PACKAGE_NPM;
|
|
2555
|
+
}
|
|
2556
|
+
function mergeDefaultVariants(models = {}) {
|
|
2557
|
+
const result = { ...models };
|
|
2558
|
+
for (const [id, model] of Object.entries(defaultModels)) {
|
|
2559
|
+
if (!model.variants) continue;
|
|
2560
|
+
const existing = result[id] && typeof result[id] === "object" ? result[id] : {};
|
|
2561
|
+
const variants = existing.variants && typeof existing.variants === "object" ? existing.variants : {};
|
|
2562
|
+
result[id] = {
|
|
2563
|
+
...existing,
|
|
2564
|
+
variants: {
|
|
2565
|
+
...model.variants,
|
|
2566
|
+
...variants
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
return result;
|
|
2571
|
+
}
|
|
2572
|
+
function defaultModelsForProvider(providerModels) {
|
|
2573
|
+
const models = Object.fromEntries(
|
|
2574
|
+
Object.entries(defaultModels).map(([id, model]) => {
|
|
2575
|
+
const existing = providerModels[id];
|
|
2576
|
+
return [
|
|
2577
|
+
id,
|
|
2578
|
+
{
|
|
2579
|
+
...model,
|
|
2580
|
+
api: {
|
|
2581
|
+
...model.api,
|
|
2582
|
+
npm: existing?.api?.npm ?? model.api.npm,
|
|
2583
|
+
url: existing?.api?.url ?? model.api.url
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
];
|
|
2587
|
+
})
|
|
2588
|
+
);
|
|
2589
|
+
for (const [id, model] of Object.entries(providerModels)) {
|
|
2590
|
+
if (!(id in models)) models[id] = model;
|
|
2591
|
+
}
|
|
2592
|
+
return models;
|
|
2593
|
+
}
|
|
2594
|
+
function providerConfig(existing) {
|
|
2595
|
+
return {
|
|
2596
|
+
name: existing?.name,
|
|
2597
|
+
npm: existing?.npm ?? pluginEntrypoint(),
|
|
2598
|
+
options: {
|
|
2599
|
+
cliPath: "claude",
|
|
2600
|
+
proxyTools: ["Bash", "Edit", "Write", "WebFetch"],
|
|
2601
|
+
...existing?.options ?? {}
|
|
2602
|
+
},
|
|
2603
|
+
models: mergeDefaultVariants(existing?.models)
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
var server = async () => ({
|
|
2607
|
+
config: async (config) => {
|
|
2608
|
+
config.provider ??= {};
|
|
2609
|
+
const existing = config.provider[PROVIDER_ID2];
|
|
2610
|
+
config.provider[PROVIDER_ID2] = {
|
|
2611
|
+
...existing,
|
|
2612
|
+
...providerConfig(existing)
|
|
2613
|
+
};
|
|
2614
|
+
},
|
|
2615
|
+
provider: {
|
|
2616
|
+
id: PROVIDER_ID2,
|
|
2617
|
+
models: async (provider) => defaultModelsForProvider(provider.models)
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
var index_default = {
|
|
2621
|
+
id: "@khalilgharbaoui/opencode-claude-code-plugin",
|
|
2622
|
+
server
|
|
2623
|
+
};
|
|
2624
|
+
export {
|
|
2625
|
+
ClaudeCodeLanguageModel,
|
|
2626
|
+
bridgeOpencodeMcp,
|
|
2627
|
+
createClaudeCode,
|
|
2628
|
+
index_default as default,
|
|
2629
|
+
defaultModels
|
|
2630
|
+
};
|
|
2631
|
+
//# sourceMappingURL=index.js.map
|