@khalilgharbaoui/opencode-claude-code-plugin 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.d.ts +226 -1
- package/dist/index.js +743 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +14 -0
package/dist/index.js
CHANGED
|
@@ -386,6 +386,134 @@ function mapTool(name, input, opts) {
|
|
|
386
386
|
return { name, input, executed: true };
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
// src/host-tools.ts
|
|
390
|
+
function defined(input) {
|
|
391
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
|
|
392
|
+
}
|
|
393
|
+
var V2_TOOLS = {
|
|
394
|
+
bash: {
|
|
395
|
+
name: "shell",
|
|
396
|
+
// V2's shell takes no `description`; `workdir`, `timeout` and
|
|
397
|
+
// `background` keep their names.
|
|
398
|
+
input: (input) => defined({
|
|
399
|
+
command: input.command,
|
|
400
|
+
workdir: input.workdir,
|
|
401
|
+
timeout: input.timeout,
|
|
402
|
+
background: input.background
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
edit: {
|
|
406
|
+
name: "edit",
|
|
407
|
+
input: (input) => defined({
|
|
408
|
+
path: input.filePath ?? input.path,
|
|
409
|
+
oldString: input.oldString,
|
|
410
|
+
newString: input.newString,
|
|
411
|
+
replaceAll: input.replaceAll
|
|
412
|
+
})
|
|
413
|
+
},
|
|
414
|
+
write: {
|
|
415
|
+
name: "write",
|
|
416
|
+
input: (input) => defined({ path: input.filePath ?? input.path, content: input.content })
|
|
417
|
+
},
|
|
418
|
+
read: {
|
|
419
|
+
name: "read",
|
|
420
|
+
input: (input) => defined({ path: input.filePath ?? input.path, offset: input.offset, limit: input.limit })
|
|
421
|
+
},
|
|
422
|
+
glob: { name: "glob" },
|
|
423
|
+
grep: { name: "grep" },
|
|
424
|
+
webfetch: { name: "webfetch" },
|
|
425
|
+
websearch: { name: "websearch" },
|
|
426
|
+
question: { name: "question" },
|
|
427
|
+
task: {
|
|
428
|
+
name: "subagent",
|
|
429
|
+
input: (input) => defined({
|
|
430
|
+
agent: input.subagent_type ?? input.agent,
|
|
431
|
+
description: input.description,
|
|
432
|
+
prompt: input.prompt,
|
|
433
|
+
sessionID: input.task_id ?? input.sessionID,
|
|
434
|
+
background: input.background
|
|
435
|
+
})
|
|
436
|
+
},
|
|
437
|
+
todowrite: null,
|
|
438
|
+
plan_enter: null,
|
|
439
|
+
plan_exit: null,
|
|
440
|
+
notebookedit: null
|
|
441
|
+
};
|
|
442
|
+
function parseInput(raw) {
|
|
443
|
+
if (typeof raw !== "string") return raw ?? {};
|
|
444
|
+
try {
|
|
445
|
+
const parsed = JSON.parse(raw);
|
|
446
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
447
|
+
} catch {
|
|
448
|
+
return {};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function createHostToolPartTranslator(dialect = "v1") {
|
|
452
|
+
if (dialect !== "v2") return (part) => part;
|
|
453
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
454
|
+
const translated = /* @__PURE__ */ new Map();
|
|
455
|
+
const classify2 = (id, name) => {
|
|
456
|
+
if (dropped.has(id)) return null;
|
|
457
|
+
const known = translated.get(id);
|
|
458
|
+
if (known) return known;
|
|
459
|
+
if (typeof name !== "string") return void 0;
|
|
460
|
+
const tool = V2_TOOLS[name];
|
|
461
|
+
if (tool === null) {
|
|
462
|
+
dropped.add(id);
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
if (tool) translated.set(id, tool);
|
|
466
|
+
return tool;
|
|
467
|
+
};
|
|
468
|
+
return (part) => {
|
|
469
|
+
switch (part.type) {
|
|
470
|
+
case "tool-input-start": {
|
|
471
|
+
const tool = classify2(String(part.id), part.toolName);
|
|
472
|
+
if (tool === null) return null;
|
|
473
|
+
return tool ? { ...part, toolName: tool.name } : part;
|
|
474
|
+
}
|
|
475
|
+
case "tool-input-delta": {
|
|
476
|
+
const tool = classify2(String(part.id), void 0);
|
|
477
|
+
if (tool === null) return null;
|
|
478
|
+
return tool?.input ? null : part;
|
|
479
|
+
}
|
|
480
|
+
case "tool-input-end": {
|
|
481
|
+
return classify2(String(part.id), void 0) === null ? null : part;
|
|
482
|
+
}
|
|
483
|
+
case "tool-call": {
|
|
484
|
+
const tool = classify2(String(part.toolCallId), part.toolName);
|
|
485
|
+
if (tool === null) return null;
|
|
486
|
+
if (!tool) return part;
|
|
487
|
+
const input = tool.input ? tool.input(parseInput(part.input)) : parseInput(part.input);
|
|
488
|
+
return {
|
|
489
|
+
...part,
|
|
490
|
+
toolName: tool.name,
|
|
491
|
+
input: typeof part.input === "string" ? JSON.stringify(input) : input
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
case "tool-result": {
|
|
495
|
+
const tool = classify2(String(part.toolCallId), part.toolName);
|
|
496
|
+
if (tool === null) return null;
|
|
497
|
+
return tool ? { ...part, toolName: tool.name } : part;
|
|
498
|
+
}
|
|
499
|
+
default:
|
|
500
|
+
return part;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function translateStreamForHost(stream, dialect = "v1") {
|
|
505
|
+
if (dialect !== "v2") return stream;
|
|
506
|
+
const translate = createHostToolPartTranslator(dialect);
|
|
507
|
+
return stream.pipeThrough(
|
|
508
|
+
new TransformStream({
|
|
509
|
+
transform(part, controller) {
|
|
510
|
+
const next = translate(part);
|
|
511
|
+
if (next) controller.enqueue(next);
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
389
517
|
// src/accounts.ts
|
|
390
518
|
import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
|
|
391
519
|
import path from "path";
|
|
@@ -596,7 +724,9 @@ function rateLimitKey(info) {
|
|
|
596
724
|
].join("|");
|
|
597
725
|
}
|
|
598
726
|
function isRateLimitRejected(info) {
|
|
599
|
-
|
|
727
|
+
if (info.status === "rejected") return true;
|
|
728
|
+
if (info.status === "allowed" || info.status === "allowed_warning") return false;
|
|
729
|
+
return info.overageStatus === "rejected";
|
|
600
730
|
}
|
|
601
731
|
function describeRateLimit(info) {
|
|
602
732
|
if (!info.status && !info.overageStatus) return null;
|
|
@@ -3254,6 +3384,7 @@ var BtwHandledError = class extends Error {
|
|
|
3254
3384
|
super(message);
|
|
3255
3385
|
}
|
|
3256
3386
|
};
|
|
3387
|
+
var BTW_COMMAND_DESCRIPTION = "Ask a side question in the live Claude Code session without changing its context";
|
|
3257
3388
|
var BTW_NO_SESSION_MESSAGE = "/btw needs a live Claude Code session in this conversation. Send a normal message with a Claude Code model first, then ask again.";
|
|
3258
3389
|
var BTW_INLINE_HANDLED_MESSAGE = "/btw was answered inside the running turn; nothing to add to this conversation.";
|
|
3259
3390
|
var BTW_TURN_TOO_LONG_MESSAGE = "/btw gave up waiting for this turn to end. Ask again once it is over.";
|
|
@@ -3781,14 +3912,14 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
|
|
|
3781
3912
|
excludeServers
|
|
3782
3913
|
});
|
|
3783
3914
|
}
|
|
3784
|
-
function
|
|
3915
|
+
function opencodeConfigLayers(cwd) {
|
|
3785
3916
|
const worktree = detectWorktree(cwd);
|
|
3786
|
-
|
|
3787
|
-
|
|
3917
|
+
const layers = [];
|
|
3918
|
+
layers.push(loadGlobalConfig());
|
|
3788
3919
|
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3789
3920
|
if (explicitConfig && fileExists(explicitConfig)) {
|
|
3790
3921
|
const parsed = readAndParse(explicitConfig);
|
|
3791
|
-
if (parsed)
|
|
3922
|
+
if (parsed) layers.push(parsed);
|
|
3792
3923
|
}
|
|
3793
3924
|
const projectFiles = walkUp({
|
|
3794
3925
|
start: cwd,
|
|
@@ -3806,10 +3937,22 @@ function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
|
3806
3937
|
}
|
|
3807
3938
|
}
|
|
3808
3939
|
for (const dir of projectDirs.slice().reverse()) {
|
|
3809
|
-
|
|
3940
|
+
layers.push(loadProjectFilesInDir(dir));
|
|
3810
3941
|
}
|
|
3811
3942
|
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
3812
|
-
|
|
3943
|
+
layers.push(loadProjectFilesInDir(dir));
|
|
3944
|
+
}
|
|
3945
|
+
return layers;
|
|
3946
|
+
}
|
|
3947
|
+
function loadMergedOpencodeConfig(cwd) {
|
|
3948
|
+
let merged = {};
|
|
3949
|
+
for (const layer of opencodeConfigLayers(cwd)) merged = deepMerge(merged, layer);
|
|
3950
|
+
return merged;
|
|
3951
|
+
}
|
|
3952
|
+
function mergeOpencodeMcp(cwd, runtimeStatus) {
|
|
3953
|
+
let merged = {};
|
|
3954
|
+
for (const layer of opencodeConfigLayers(cwd)) {
|
|
3955
|
+
merged = mergeMcp(merged, extractMcpBlock(layer));
|
|
3813
3956
|
}
|
|
3814
3957
|
if (runtimeStatus) {
|
|
3815
3958
|
for (const name of Object.keys(merged)) {
|
|
@@ -3945,6 +4088,39 @@ async function fetchSessionParentId(sessionID) {
|
|
|
3945
4088
|
return void 0;
|
|
3946
4089
|
}
|
|
3947
4090
|
}
|
|
4091
|
+
async function fetchSessionRunState(sessionID) {
|
|
4092
|
+
if (!sessionID || sessionID === "default") return "unknown";
|
|
4093
|
+
const status = opencodeClient?.session?.status;
|
|
4094
|
+
if (!status) return "unknown";
|
|
4095
|
+
try {
|
|
4096
|
+
const res = await status.call(opencodeClient.session);
|
|
4097
|
+
const data = res.data;
|
|
4098
|
+
if (!data || typeof data !== "object") return "unknown";
|
|
4099
|
+
const entry = data[sessionID];
|
|
4100
|
+
if (!entry || typeof entry !== "object") return "idle";
|
|
4101
|
+
const type = entry.type;
|
|
4102
|
+
return type === "idle" ? "idle" : "busy";
|
|
4103
|
+
} catch (err) {
|
|
4104
|
+
log.debug("failed to read opencode session status", {
|
|
4105
|
+
sessionID,
|
|
4106
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4107
|
+
});
|
|
4108
|
+
return "unknown";
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
async function settleSessionRunState(sessionID, options = {}) {
|
|
4112
|
+
const pollMs = options.pollMs ?? 150;
|
|
4113
|
+
const windowMs = options.windowMs ?? 1500;
|
|
4114
|
+
const started = Date.now();
|
|
4115
|
+
let last = "unknown";
|
|
4116
|
+
for (; ; ) {
|
|
4117
|
+
const state = await fetchSessionRunState(sessionID);
|
|
4118
|
+
if (state === "busy") return "busy";
|
|
4119
|
+
if (state !== "unknown") last = state;
|
|
4120
|
+
if (Date.now() - started >= windowMs) return last;
|
|
4121
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollMs));
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
3948
4124
|
async function getRuntimeMcpStatus() {
|
|
3949
4125
|
const client = opencodeClient;
|
|
3950
4126
|
if (!client?.mcp?.status) return void 0;
|
|
@@ -4129,6 +4305,7 @@ function logStartupDiagnostics(providers, opencodeVersion) {
|
|
|
4129
4305
|
|
|
4130
4306
|
// src/doctor.ts
|
|
4131
4307
|
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
4308
|
+
var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls";
|
|
4132
4309
|
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
4133
4310
|
function isRecord3(value) {
|
|
4134
4311
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -5340,6 +5517,33 @@ function registerBundledSkillPath(config) {
|
|
|
5340
5517
|
paths.push(dir);
|
|
5341
5518
|
return true;
|
|
5342
5519
|
}
|
|
5520
|
+
function readBundledSkillInfos() {
|
|
5521
|
+
return discoverBundledSkills().flatMap((skill) => {
|
|
5522
|
+
const file = path7.join(skill.dir, "SKILL.md");
|
|
5523
|
+
let text;
|
|
5524
|
+
try {
|
|
5525
|
+
text = fs5.readFileSync(file, "utf8");
|
|
5526
|
+
} catch {
|
|
5527
|
+
return [];
|
|
5528
|
+
}
|
|
5529
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
5530
|
+
const frontmatter = match?.[1] ?? "";
|
|
5531
|
+
const field = (key) => {
|
|
5532
|
+
const line = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter)?.[1]?.trim();
|
|
5533
|
+
return line ? line.replace(/^(["'])(.*)\1$/, "$2") : void 0;
|
|
5534
|
+
};
|
|
5535
|
+
const description = field("description");
|
|
5536
|
+
return [
|
|
5537
|
+
{
|
|
5538
|
+
id: skill.name,
|
|
5539
|
+
name: field("name") ?? skill.name,
|
|
5540
|
+
...description ? { description } : {},
|
|
5541
|
+
path: file,
|
|
5542
|
+
content: match ? text.slice(match[0].length) : text
|
|
5543
|
+
}
|
|
5544
|
+
];
|
|
5545
|
+
});
|
|
5546
|
+
}
|
|
5343
5547
|
function linkSkill(source, target) {
|
|
5344
5548
|
try {
|
|
5345
5549
|
fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
@@ -5989,7 +6193,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
5989
6193
|
}
|
|
5990
6194
|
|
|
5991
6195
|
// src/claude-code-language-model.ts
|
|
5992
|
-
import { readFileSync as
|
|
6196
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
5993
6197
|
import { unlink as unlink3 } from "fs/promises";
|
|
5994
6198
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
5995
6199
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -6018,6 +6222,32 @@ function resolveSessionAffinity(headers, providerOptions, providerKey) {
|
|
|
6018
6222
|
}
|
|
6019
6223
|
return "default";
|
|
6020
6224
|
}
|
|
6225
|
+
function resolveOpencodeAgent(headers, providerOptions, providerKey) {
|
|
6226
|
+
if (providerOptions) {
|
|
6227
|
+
const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
|
|
6228
|
+
const agent = bag?.opencodeAgent;
|
|
6229
|
+
if (typeof agent === "string") return agent;
|
|
6230
|
+
}
|
|
6231
|
+
if (headers) {
|
|
6232
|
+
for (const key of Object.keys(headers)) {
|
|
6233
|
+
if (key.toLowerCase() === "x-opencode-agent") {
|
|
6234
|
+
const value = headers[key];
|
|
6235
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
return void 0;
|
|
6240
|
+
}
|
|
6241
|
+
function describeAbortReason(reason) {
|
|
6242
|
+
if (reason === void 0) return "undefined";
|
|
6243
|
+
if (reason instanceof Error) return `${reason.name}: ${reason.message}`;
|
|
6244
|
+
if (typeof reason === "string") return reason;
|
|
6245
|
+
try {
|
|
6246
|
+
return JSON.stringify(reason) ?? typeof reason;
|
|
6247
|
+
} catch {
|
|
6248
|
+
return typeof reason;
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6021
6251
|
var KNOWN_DELTA_TYPES = /* @__PURE__ */ new Set([
|
|
6022
6252
|
"thinking_delta",
|
|
6023
6253
|
"text_delta",
|
|
@@ -6217,9 +6447,9 @@ ${body}`;
|
|
|
6217
6447
|
}
|
|
6218
6448
|
});
|
|
6219
6449
|
}
|
|
6220
|
-
function readPromptFileIfPresent(
|
|
6450
|
+
function readPromptFileIfPresent(path10) {
|
|
6221
6451
|
try {
|
|
6222
|
-
const content =
|
|
6452
|
+
const content = readFileSync5(path10, "utf8").trim();
|
|
6223
6453
|
return content || void 0;
|
|
6224
6454
|
} catch {
|
|
6225
6455
|
return void 0;
|
|
@@ -6334,10 +6564,10 @@ ${options.compressionSummary.trim()}`
|
|
|
6334
6564
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
6335
6565
|
const content = parts.join("\n\n");
|
|
6336
6566
|
if (!content) return void 0;
|
|
6337
|
-
const
|
|
6567
|
+
const path10 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
6338
6568
|
try {
|
|
6339
|
-
writeFileSync4(
|
|
6340
|
-
return
|
|
6569
|
+
writeFileSync4(path10, content, "utf8");
|
|
6570
|
+
return path10;
|
|
6341
6571
|
} catch (err) {
|
|
6342
6572
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
6343
6573
|
return void 0;
|
|
@@ -6423,6 +6653,19 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6423
6653
|
raw: reason
|
|
6424
6654
|
};
|
|
6425
6655
|
}
|
|
6656
|
+
/**
|
|
6657
|
+
* Whether this call only names the session, which gets the synthetic stub
|
|
6658
|
+
* rather than a `claude` spawn. opencode 1.x sends a title request with no
|
|
6659
|
+
* tools, and that is the whole test there. opencode 2 sends its tool set
|
|
6660
|
+
* along with it (measured on 2.0.11: `scope: "tools"`, agent `title`), so
|
|
6661
|
+
* every new V2 session paid for a second `claude` process just to title
|
|
6662
|
+
* itself; for a V2 model the request kind, carried as the `title` agent,
|
|
6663
|
+
* decides instead.
|
|
6664
|
+
*/
|
|
6665
|
+
isTitleRequest(scope, options) {
|
|
6666
|
+
if (scope === "no-tools") return true;
|
|
6667
|
+
return this.config.hostApi === "v2" && this.getOpencodeAgent(options) === "title";
|
|
6668
|
+
}
|
|
6426
6669
|
requestScope(options) {
|
|
6427
6670
|
const tools = options?.tools;
|
|
6428
6671
|
if (Array.isArray(tools)) return "tools";
|
|
@@ -6834,15 +7077,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6834
7077
|
];
|
|
6835
7078
|
return valid.includes(effort) ? effort : void 0;
|
|
6836
7079
|
}
|
|
6837
|
-
getOpencodeAgent(
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
7080
|
+
getOpencodeAgent(options) {
|
|
7081
|
+
return resolveOpencodeAgent(
|
|
7082
|
+
options?.headers,
|
|
7083
|
+
options.providerOptions,
|
|
7084
|
+
this.config.provider
|
|
7085
|
+
);
|
|
6843
7086
|
}
|
|
6844
7087
|
isCompactionCall(options) {
|
|
6845
|
-
return this.getOpencodeAgent(options
|
|
7088
|
+
return this.getOpencodeAgent(options) === "compaction";
|
|
6846
7089
|
}
|
|
6847
7090
|
/**
|
|
6848
7091
|
* Pick the model used to handle /compact. Precedence:
|
|
@@ -6978,6 +7221,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6978
7221
|
};
|
|
6979
7222
|
}
|
|
6980
7223
|
async doGenerate(options) {
|
|
7224
|
+
const result = await this.doGenerateForHost(options);
|
|
7225
|
+
if (this.config.hostApi !== "v2") return result;
|
|
7226
|
+
const translate = createHostToolPartTranslator("v2");
|
|
7227
|
+
return {
|
|
7228
|
+
...result,
|
|
7229
|
+
content: result.content.flatMap((part) => {
|
|
7230
|
+
const next = translate(part);
|
|
7231
|
+
return next ? [next] : [];
|
|
7232
|
+
})
|
|
7233
|
+
};
|
|
7234
|
+
}
|
|
7235
|
+
async doGenerateForHost(options) {
|
|
6981
7236
|
if (!this.isCompactionCall(options) && this.requestScope(options) !== "no-tools" && parseSideQuestion(options.prompt)) {
|
|
6982
7237
|
return this.doGenerateViaStream(options);
|
|
6983
7238
|
}
|
|
@@ -6986,16 +7241,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6986
7241
|
const affinity = this.sessionAffinity(options);
|
|
6987
7242
|
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
6988
7243
|
const effectiveModelId = resolveAgentModel(
|
|
6989
|
-
this.getOpencodeAgent(options
|
|
7244
|
+
this.getOpencodeAgent(options),
|
|
6990
7245
|
this.modelId
|
|
6991
7246
|
);
|
|
6992
7247
|
const reasoningEffort = resolveAgentEffort(
|
|
6993
|
-
this.getOpencodeAgent(options
|
|
7248
|
+
this.getOpencodeAgent(options),
|
|
6994
7249
|
this.getReasoningEffort(options.providerOptions)
|
|
6995
7250
|
);
|
|
6996
7251
|
const baseKey = sessionKey(
|
|
6997
7252
|
cwd,
|
|
6998
|
-
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options
|
|
7253
|
+
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
|
|
6999
7254
|
);
|
|
7000
7255
|
const sk = effortSessionKey(baseKey, reasoningEffort);
|
|
7001
7256
|
const compactionMode = this.isCompactionCall(options);
|
|
@@ -7005,10 +7260,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7005
7260
|
if (compactionMode) {
|
|
7006
7261
|
return this.doGenerateViaStream(options);
|
|
7007
7262
|
}
|
|
7008
|
-
if (scope
|
|
7263
|
+
if (this.isTitleRequest(scope, options)) {
|
|
7009
7264
|
log.info("doGenerate no-tools title stub", {
|
|
7010
7265
|
compactionMode,
|
|
7011
|
-
opencodeAgent: this.getOpencodeAgent(options
|
|
7266
|
+
opencodeAgent: this.getOpencodeAgent(options),
|
|
7012
7267
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
7013
7268
|
});
|
|
7014
7269
|
const text = this.synthesizeTitle(options.prompt);
|
|
@@ -7408,7 +7663,19 @@ ${plan}
|
|
|
7408
7663
|
warnings
|
|
7409
7664
|
};
|
|
7410
7665
|
}
|
|
7666
|
+
/**
|
|
7667
|
+
* Tool parts leave in opencode 1.x's vocabulary; on opencode 2.x they are
|
|
7668
|
+
* renamed at this one edge (src/host-tools.ts). On V1 the stream is
|
|
7669
|
+
* returned untouched.
|
|
7670
|
+
*/
|
|
7411
7671
|
async doStream(options) {
|
|
7672
|
+
const result = await this.doStreamForHost(options);
|
|
7673
|
+
return {
|
|
7674
|
+
...result,
|
|
7675
|
+
stream: translateStreamForHost(result.stream, this.config.hostApi ?? "v1")
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7678
|
+
async doStreamForHost(options) {
|
|
7412
7679
|
const warnings = [];
|
|
7413
7680
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
7414
7681
|
const scope = this.requestScope(options);
|
|
@@ -7416,16 +7683,16 @@ ${plan}
|
|
|
7416
7683
|
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
7417
7684
|
const compactionMode = this.isCompactionCall(options);
|
|
7418
7685
|
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
|
|
7419
|
-
this.getOpencodeAgent(options
|
|
7686
|
+
this.getOpencodeAgent(options),
|
|
7420
7687
|
this.modelId
|
|
7421
7688
|
);
|
|
7422
7689
|
const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
|
|
7423
|
-
this.getOpencodeAgent(options
|
|
7690
|
+
this.getOpencodeAgent(options),
|
|
7424
7691
|
this.getReasoningEffort(options.providerOptions)
|
|
7425
7692
|
);
|
|
7426
7693
|
const baseKey = sessionKey(
|
|
7427
7694
|
cwd,
|
|
7428
|
-
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options
|
|
7695
|
+
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
|
|
7429
7696
|
);
|
|
7430
7697
|
const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : effortSessionKey(baseKey, reasoningEffort);
|
|
7431
7698
|
const toUsage = this.toUsage.bind(this);
|
|
@@ -7526,10 +7793,10 @@ ${plan}
|
|
|
7526
7793
|
});
|
|
7527
7794
|
return { stream: stream2, request: { body: { text: aside.question } } };
|
|
7528
7795
|
}
|
|
7529
|
-
if (scope
|
|
7796
|
+
if (this.isTitleRequest(scope, options) && !compactionMode) {
|
|
7530
7797
|
log.info("doStream no-tools title stub", {
|
|
7531
7798
|
compactionMode,
|
|
7532
|
-
opencodeAgent: this.getOpencodeAgent(options
|
|
7799
|
+
opencodeAgent: this.getOpencodeAgent(options),
|
|
7533
7800
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
7534
7801
|
});
|
|
7535
7802
|
const text = this.synthesizeTitle(options.prompt);
|
|
@@ -7719,7 +7986,7 @@ ${plan}
|
|
|
7719
7986
|
proxyTools: resolvedProxy?.map((t) => t.name) ?? null,
|
|
7720
7987
|
compactionMode,
|
|
7721
7988
|
scope,
|
|
7722
|
-
opencodeAgent: this.getOpencodeAgent(options
|
|
7989
|
+
opencodeAgent: this.getOpencodeAgent(options),
|
|
7723
7990
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
7724
7991
|
});
|
|
7725
7992
|
const stream = new ReadableStream({
|
|
@@ -8381,7 +8648,7 @@ ${plan}
|
|
|
8381
8648
|
});
|
|
8382
8649
|
}
|
|
8383
8650
|
activeProcess?.pendingProxyCompletions?.clear();
|
|
8384
|
-
if (accountLimitHit && failoverAskActive) {
|
|
8651
|
+
if (accountLimitHit && failoverAskActive && msg.is_error === true) {
|
|
8385
8652
|
const call = createAccountFailoverQuestionCall(sk, {
|
|
8386
8653
|
sourceAccount,
|
|
8387
8654
|
candidates: failoverAccounts,
|
|
@@ -9354,14 +9621,28 @@ ${plan}
|
|
|
9354
9621
|
options.abortSignal.addEventListener("abort", () => {
|
|
9355
9622
|
autoContinueState.aborted = true;
|
|
9356
9623
|
if (turnCompleted || controllerClosed) {
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9624
|
+
const stoppedProcess = activeProcess;
|
|
9625
|
+
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
9626
|
+
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
9627
|
+
void settleSessionRunState(affinity).then((stopped) => {
|
|
9628
|
+
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
9629
|
+
if (stopped === "busy" || !stillParked) {
|
|
9630
|
+
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
9631
|
+
sk,
|
|
9632
|
+
session: stopped,
|
|
9633
|
+
stillParked,
|
|
9634
|
+
reason
|
|
9635
|
+
});
|
|
9636
|
+
return;
|
|
9637
|
+
}
|
|
9638
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
9639
|
+
void interruptTurn(stoppedProcess).then((idle) => {
|
|
9640
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
9641
|
+
});
|
|
9642
|
+
releaseAbandonedProxyCalls(
|
|
9643
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
9644
|
+
);
|
|
9361
9645
|
});
|
|
9362
|
-
releaseAbandonedProxyCalls(
|
|
9363
|
-
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
9364
|
-
);
|
|
9365
9646
|
}
|
|
9366
9647
|
return;
|
|
9367
9648
|
}
|
|
@@ -9487,7 +9768,7 @@ ${plan}
|
|
|
9487
9768
|
// src/cleanup-stale.ts
|
|
9488
9769
|
import {
|
|
9489
9770
|
existsSync as existsSync4,
|
|
9490
|
-
readFileSync as
|
|
9771
|
+
readFileSync as readFileSync6,
|
|
9491
9772
|
realpathSync,
|
|
9492
9773
|
rmSync as rmSync3,
|
|
9493
9774
|
writeFileSync as writeFileSync5
|
|
@@ -9514,7 +9795,7 @@ function userIntendsToUseUnscoped() {
|
|
|
9514
9795
|
const cfg = userOpencodeJsonPath();
|
|
9515
9796
|
if (!existsSync4(cfg)) return false;
|
|
9516
9797
|
try {
|
|
9517
|
-
const json = JSON.parse(
|
|
9798
|
+
const json = JSON.parse(readFileSync6(cfg, "utf8"));
|
|
9518
9799
|
const plugins = json.plugin;
|
|
9519
9800
|
if (!Array.isArray(plugins)) return false;
|
|
9520
9801
|
return plugins.some(
|
|
@@ -9563,7 +9844,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
9563
9844
|
if (!existsSync4(pkgJsonPath)) return;
|
|
9564
9845
|
let pkg = {};
|
|
9565
9846
|
try {
|
|
9566
|
-
pkg = JSON.parse(
|
|
9847
|
+
pkg = JSON.parse(readFileSync6(pkgJsonPath, "utf8"));
|
|
9567
9848
|
} catch {
|
|
9568
9849
|
return;
|
|
9569
9850
|
}
|
|
@@ -9582,7 +9863,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
9582
9863
|
const cachePkgJson = join9(cacheRoot, "package.json");
|
|
9583
9864
|
if (!existsSync4(cachePkgJson)) return;
|
|
9584
9865
|
try {
|
|
9585
|
-
const cfg = JSON.parse(
|
|
9866
|
+
const cfg = JSON.parse(readFileSync6(cachePkgJson, "utf8"));
|
|
9586
9867
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
9587
9868
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
9588
9869
|
writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
@@ -9595,6 +9876,414 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
9595
9876
|
}
|
|
9596
9877
|
}
|
|
9597
9878
|
|
|
9879
|
+
// src/v2-client.ts
|
|
9880
|
+
import path9 from "path";
|
|
9881
|
+
function formatAgentTypeList(agents) {
|
|
9882
|
+
const lines = agents.flatMap((agent) => {
|
|
9883
|
+
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
9884
|
+
const name = typeof agent.id === "string" ? agent.id : agent.name;
|
|
9885
|
+
if (typeof name !== "string" || name.length === 0) return [];
|
|
9886
|
+
const blurb = typeof agent.description === "string" ? agent.description.trim() : "";
|
|
9887
|
+
return [`- ${name}: ${blurb || "(no description)"}`];
|
|
9888
|
+
});
|
|
9889
|
+
if (lines.length === 0) return void 0;
|
|
9890
|
+
return `Available agent types and the tools they have access to:
|
|
9891
|
+
${lines.join("\n")}`;
|
|
9892
|
+
}
|
|
9893
|
+
function describe(value) {
|
|
9894
|
+
if (typeof value === "string") return value;
|
|
9895
|
+
if (typeof value === "function") {
|
|
9896
|
+
try {
|
|
9897
|
+
const result = value();
|
|
9898
|
+
return typeof result === "string" ? result : "";
|
|
9899
|
+
} catch {
|
|
9900
|
+
return "";
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
return "";
|
|
9904
|
+
}
|
|
9905
|
+
function toV1Session(info) {
|
|
9906
|
+
if (!info || typeof info !== "object") return void 0;
|
|
9907
|
+
const session = info;
|
|
9908
|
+
const root = session.location?.directory;
|
|
9909
|
+
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path9.resolve(root, session.subpath) : root : void 0;
|
|
9910
|
+
return { ...info, directory, parentID: session.parentID };
|
|
9911
|
+
}
|
|
9912
|
+
function createV1ClientShim(ctx) {
|
|
9913
|
+
const client = {};
|
|
9914
|
+
const mcpList = ctx.mcp?.list;
|
|
9915
|
+
if (typeof mcpList === "function") {
|
|
9916
|
+
client.mcp = {
|
|
9917
|
+
// V1: `{ data: { [server]: { status } } }`. V2: `{ data: [{ name, status: { status } }] }`.
|
|
9918
|
+
status: async () => {
|
|
9919
|
+
const result = await mcpList.call(ctx.mcp);
|
|
9920
|
+
const data = {};
|
|
9921
|
+
for (const server2 of result?.data ?? []) {
|
|
9922
|
+
const status = server2?.status?.status;
|
|
9923
|
+
if (typeof server2?.name === "string" && typeof status === "string") {
|
|
9924
|
+
data[server2.name] = { status };
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
return { data };
|
|
9928
|
+
}
|
|
9929
|
+
};
|
|
9930
|
+
}
|
|
9931
|
+
const toolList = ctx.tool?.list;
|
|
9932
|
+
if (typeof toolList === "function") {
|
|
9933
|
+
client.tool = {
|
|
9934
|
+
// V1: `{ data: [{ id, description, parameters }] }`. V2 returns the tools
|
|
9935
|
+
// themselves, with an Effect schema for input rather than JSON Schema, so
|
|
9936
|
+
// `parameters` is empty; only `proxyOpencodeTools` reads it.
|
|
9937
|
+
list: async () => {
|
|
9938
|
+
const tools = await toolList.call(ctx.tool);
|
|
9939
|
+
const data = (tools ?? []).flatMap((tool) => {
|
|
9940
|
+
const id = typeof tool?.id === "string" ? tool.id : tool?.name;
|
|
9941
|
+
return typeof id === "string" ? [{ id, description: describe(tool.description), parameters: {} }] : [];
|
|
9942
|
+
});
|
|
9943
|
+
const subagent = data.find((tool) => tool.id === "subagent");
|
|
9944
|
+
if (subagent && !data.some((tool) => tool.id === "task")) {
|
|
9945
|
+
let agentList;
|
|
9946
|
+
const agentApi = ctx.agent?.list;
|
|
9947
|
+
if (typeof agentApi === "function") {
|
|
9948
|
+
try {
|
|
9949
|
+
agentList = formatAgentTypeList((await agentApi.call(ctx.agent))?.data ?? []);
|
|
9950
|
+
} catch {
|
|
9951
|
+
agentList = void 0;
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
const description = agentList ? `${subagent.description}
|
|
9955
|
+
|
|
9956
|
+
${agentList}` : subagent.description;
|
|
9957
|
+
data.push({ ...subagent, id: "task", description });
|
|
9958
|
+
}
|
|
9959
|
+
return { data };
|
|
9960
|
+
}
|
|
9961
|
+
};
|
|
9962
|
+
}
|
|
9963
|
+
const sessionGet = ctx.session?.get;
|
|
9964
|
+
if (typeof sessionGet === "function") {
|
|
9965
|
+
client.session = {
|
|
9966
|
+
get: async (options) => ({
|
|
9967
|
+
data: toV1Session(await sessionGet.call(ctx.session, { sessionID: options.path.id }))
|
|
9968
|
+
})
|
|
9969
|
+
};
|
|
9970
|
+
}
|
|
9971
|
+
return client;
|
|
9972
|
+
}
|
|
9973
|
+
|
|
9974
|
+
// src/v2.ts
|
|
9975
|
+
var V2_PLUGIN_PACKAGE = "aisdk:@khalilgharbaoui/opencode-claude-code-plugin";
|
|
9976
|
+
var SESSION_AFFINITY_HEADER = "x-session-affinity";
|
|
9977
|
+
var OPENCODE_AGENT_HEADER = "x-opencode-agent";
|
|
9978
|
+
var PLUGIN_ONLY_SETTINGS = ["accounts", "defaultSubagentModel"];
|
|
9979
|
+
var OPENCODE_SDK_OPTION_KEYS = ["fetch", "headers", "body", "name"];
|
|
9980
|
+
function isClaudeCodeProviderId(providerID) {
|
|
9981
|
+
if (typeof providerID !== "string") return false;
|
|
9982
|
+
return providerID === BASE_PROVIDER_ID || providerID.startsWith(`${BASE_PROVIDER_ID}-`);
|
|
9983
|
+
}
|
|
9984
|
+
function agentForRequest(kind, agent) {
|
|
9985
|
+
if (kind === "compaction") return "compaction";
|
|
9986
|
+
if (kind === "title") return "title";
|
|
9987
|
+
return agent;
|
|
9988
|
+
}
|
|
9989
|
+
function releasedAt(date) {
|
|
9990
|
+
const ms = date ? Date.parse(date) : Number.NaN;
|
|
9991
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
9992
|
+
}
|
|
9993
|
+
function modalities(flags) {
|
|
9994
|
+
return Object.entries(flags).filter(([, enabled]) => enabled === true).map(([name]) => name);
|
|
9995
|
+
}
|
|
9996
|
+
function toV2Model(model, providerID, modelId) {
|
|
9997
|
+
return {
|
|
9998
|
+
id: modelId,
|
|
9999
|
+
modelID: modelId,
|
|
10000
|
+
providerID,
|
|
10001
|
+
name: model.name,
|
|
10002
|
+
family: model.family,
|
|
10003
|
+
capabilities: {
|
|
10004
|
+
tools: model.capabilities.toolcall,
|
|
10005
|
+
input: modalities(model.capabilities.input),
|
|
10006
|
+
output: modalities(model.capabilities.output)
|
|
10007
|
+
},
|
|
10008
|
+
variants: Object.entries(model.variants ?? {}).map(([id, settings]) => ({
|
|
10009
|
+
id,
|
|
10010
|
+
settings: { ...settings }
|
|
10011
|
+
})),
|
|
10012
|
+
time: { released: releasedAt(model.release_date) },
|
|
10013
|
+
cost: [
|
|
10014
|
+
{
|
|
10015
|
+
input: model.cost.input,
|
|
10016
|
+
output: model.cost.output,
|
|
10017
|
+
cache: { read: model.cost.cache.read, write: model.cost.cache.write }
|
|
10018
|
+
}
|
|
10019
|
+
],
|
|
10020
|
+
status: model.status ?? "active",
|
|
10021
|
+
enabled: true,
|
|
10022
|
+
limit: { context: model.limit.context, output: model.limit.output },
|
|
10023
|
+
package: V2_PLUGIN_PACKAGE
|
|
10024
|
+
};
|
|
10025
|
+
}
|
|
10026
|
+
function v2ModelsForProvider(providerID, modelSuffix) {
|
|
10027
|
+
return Object.entries(defaultModels).map(
|
|
10028
|
+
([id, model]) => toV2Model(model, providerID, modelSuffix ? `${id}@${modelSuffix}` : id)
|
|
10029
|
+
);
|
|
10030
|
+
}
|
|
10031
|
+
function stripPluginOnly(settings) {
|
|
10032
|
+
const result = { ...settings ?? {} };
|
|
10033
|
+
for (const key of PLUGIN_ONLY_SETTINGS) delete result[key];
|
|
10034
|
+
return result;
|
|
10035
|
+
}
|
|
10036
|
+
function planV2Providers(seedSettings, defaultProxyTools) {
|
|
10037
|
+
const base = {
|
|
10038
|
+
cliPath: "claude",
|
|
10039
|
+
proxyTools: [...defaultProxyTools],
|
|
10040
|
+
...stripPluginOnly(seedSettings)
|
|
10041
|
+
};
|
|
10042
|
+
const accounts = resolveAccounts(seedSettings?.accounts);
|
|
10043
|
+
if (!accounts) {
|
|
10044
|
+
return [
|
|
10045
|
+
{
|
|
10046
|
+
info: {
|
|
10047
|
+
id: BASE_PROVIDER_ID,
|
|
10048
|
+
name: "Claude Code",
|
|
10049
|
+
activation: "enabled",
|
|
10050
|
+
package: V2_PLUGIN_PACKAGE,
|
|
10051
|
+
settings: { ...base, providerID: BASE_PROVIDER_ID }
|
|
10052
|
+
},
|
|
10053
|
+
models: v2ModelsForProvider(BASE_PROVIDER_ID)
|
|
10054
|
+
}
|
|
10055
|
+
];
|
|
10056
|
+
}
|
|
10057
|
+
return accounts.map((account) => {
|
|
10058
|
+
const providerID = accountProviderId(account);
|
|
10059
|
+
return {
|
|
10060
|
+
info: {
|
|
10061
|
+
id: providerID,
|
|
10062
|
+
name: accountDisplayName(account),
|
|
10063
|
+
activation: "enabled",
|
|
10064
|
+
package: V2_PLUGIN_PACKAGE,
|
|
10065
|
+
settings: {
|
|
10066
|
+
...base,
|
|
10067
|
+
account,
|
|
10068
|
+
// The resolved list, so this account's language model can offer the
|
|
10069
|
+
// others when it runs out of usage (src/account-failover.ts).
|
|
10070
|
+
failoverAccounts: accounts,
|
|
10071
|
+
providerID,
|
|
10072
|
+
// Kept separately because the wrapper replaces `cliPath`, and a
|
|
10073
|
+
// failover builds another account's wrapper on the same base.
|
|
10074
|
+
baseCliPath: String(base.cliPath ?? "claude")
|
|
10075
|
+
}
|
|
10076
|
+
},
|
|
10077
|
+
models: v2ModelsForProvider(providerID, accountModelSuffix(account))
|
|
10078
|
+
};
|
|
10079
|
+
});
|
|
10080
|
+
}
|
|
10081
|
+
async function resolveSdkSettings(providerID, planned, eventOptions) {
|
|
10082
|
+
const fromEvent = { ...eventOptions };
|
|
10083
|
+
for (const key of OPENCODE_SDK_OPTION_KEYS) delete fromEvent[key];
|
|
10084
|
+
const merged = {
|
|
10085
|
+
...stripPluginOnly({ ...planned ?? {}, ...fromEvent }),
|
|
10086
|
+
providerID,
|
|
10087
|
+
// The only place a model is told it serves opencode 2.
|
|
10088
|
+
hostApi: "v2"
|
|
10089
|
+
};
|
|
10090
|
+
const account = typeof merged.account === "string" ? merged.account : void 0;
|
|
10091
|
+
if (account) {
|
|
10092
|
+
const base = String(merged.baseCliPath ?? merged.cliPath ?? "claude");
|
|
10093
|
+
const runtime = await ensureAccountRuntime(account, base);
|
|
10094
|
+
Object.assign(merged, runtime, { baseCliPath: base });
|
|
10095
|
+
}
|
|
10096
|
+
return merged;
|
|
10097
|
+
}
|
|
10098
|
+
function plainObject(value) {
|
|
10099
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
10100
|
+
}
|
|
10101
|
+
function configuredSeedSettings(config, pluginOptions) {
|
|
10102
|
+
const provider = plainObject(plainObject(config.provider)?.[BASE_PROVIDER_ID]);
|
|
10103
|
+
return {
|
|
10104
|
+
...plainObject(provider?.options) ?? {},
|
|
10105
|
+
...plainObject(provider?.settings) ?? {},
|
|
10106
|
+
...pluginOptions ?? {}
|
|
10107
|
+
};
|
|
10108
|
+
}
|
|
10109
|
+
function isOpencodeV2Context(ctx) {
|
|
10110
|
+
const context = plainObject(ctx);
|
|
10111
|
+
if (!context) return false;
|
|
10112
|
+
const version = plainObject(context.app)?.version;
|
|
10113
|
+
if (typeof version === "string") {
|
|
10114
|
+
const major = Number.parseInt(version.replace(/^v/, ""), 10);
|
|
10115
|
+
if (Number.isFinite(major) && major < 2) return false;
|
|
10116
|
+
}
|
|
10117
|
+
const hasHook = (domain, name) => typeof plainObject(domain)?.[name] === "function";
|
|
10118
|
+
return hasHook(context.provider, "transform") && hasHook(context.aisdk, "hook") && hasHook(context.session, "hook");
|
|
10119
|
+
}
|
|
10120
|
+
function commandPromptText(command, args) {
|
|
10121
|
+
const rest = (args ?? "").trim();
|
|
10122
|
+
return rest ? `/${command} ${rest}` : `/${command}`;
|
|
10123
|
+
}
|
|
10124
|
+
function deletedSessionIdV2(event) {
|
|
10125
|
+
if (!event || event.type !== "session.deleted") return void 0;
|
|
10126
|
+
const id = event.data?.sessionID;
|
|
10127
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
10128
|
+
}
|
|
10129
|
+
async function releaseDeletedSessions(subscribe, signal) {
|
|
10130
|
+
try {
|
|
10131
|
+
for await (const event of subscribe({ signal })) {
|
|
10132
|
+
if (signal.aborted) break;
|
|
10133
|
+
const sessionID = deletedSessionIdV2(event);
|
|
10134
|
+
if (!sessionID) continue;
|
|
10135
|
+
const released = deleteActiveProcessesForSession(sessionID);
|
|
10136
|
+
if (released.length > 0) {
|
|
10137
|
+
log.info("released claude state for deleted session", { sessionID, released });
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
10140
|
+
} catch (err) {
|
|
10141
|
+
if (!signal.aborted) {
|
|
10142
|
+
log.warn("opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction", {
|
|
10143
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10144
|
+
});
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
function createV2Setup(deps) {
|
|
10149
|
+
return async (ctx) => {
|
|
10150
|
+
if (!isOpencodeV2Context(ctx)) {
|
|
10151
|
+
log.debug("setup called outside opencode 2; leaving it to server()", {
|
|
10152
|
+
version: ctx?.app?.version
|
|
10153
|
+
});
|
|
10154
|
+
return () => void 0;
|
|
10155
|
+
}
|
|
10156
|
+
ensureProcessExitCleanup();
|
|
10157
|
+
setOpencodeClient(createV1ClientShim(ctx));
|
|
10158
|
+
const directory = ctx.location?.directory;
|
|
10159
|
+
setOpencodeProjectDirectory(isUsableDirectory(directory) ? directory : void 0);
|
|
10160
|
+
if (deps.buildAgentRegistry) {
|
|
10161
|
+
try {
|
|
10162
|
+
const config = deps.loadConfig(directory ?? process.cwd());
|
|
10163
|
+
await deps.buildAgentRegistry({
|
|
10164
|
+
...config,
|
|
10165
|
+
provider: {
|
|
10166
|
+
...plainObject(config.provider) ?? {},
|
|
10167
|
+
[BASE_PROVIDER_ID]: {
|
|
10168
|
+
options: configuredSeedSettings(config, plainObject(ctx.options))
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
});
|
|
10172
|
+
} catch (err) {
|
|
10173
|
+
log.warn("failed to build the agent registry for opencode 2", {
|
|
10174
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10175
|
+
});
|
|
10176
|
+
}
|
|
10177
|
+
}
|
|
10178
|
+
const planned = /* @__PURE__ */ new Map();
|
|
10179
|
+
const registrations = [];
|
|
10180
|
+
registrations.push(
|
|
10181
|
+
await ctx.provider.transform((editor) => {
|
|
10182
|
+
const seed = editor.get(BASE_PROVIDER_ID);
|
|
10183
|
+
const seedSettings = {
|
|
10184
|
+
...seed?.provider.settings ?? {},
|
|
10185
|
+
...configuredSeedSettings(
|
|
10186
|
+
deps.loadConfig(directory ?? process.cwd()),
|
|
10187
|
+
plainObject(ctx.options)
|
|
10188
|
+
)
|
|
10189
|
+
};
|
|
10190
|
+
const plans = planV2Providers(seedSettings, deps.defaultProxyTools);
|
|
10191
|
+
planned.clear();
|
|
10192
|
+
for (const plan of plans) {
|
|
10193
|
+
planned.set(plan.info.id, plan.info.settings ?? {});
|
|
10194
|
+
if (editor.get(plan.info.id)) {
|
|
10195
|
+
editor.update(plan.info.id, (provider) => {
|
|
10196
|
+
provider.name = plan.info.name;
|
|
10197
|
+
provider.activation = plan.info.activation;
|
|
10198
|
+
provider.package = plan.info.package;
|
|
10199
|
+
provider.settings = plan.info.settings;
|
|
10200
|
+
});
|
|
10201
|
+
editor.models.set(plan.info.id, plan.models);
|
|
10202
|
+
} else {
|
|
10203
|
+
editor.add({ info: plan.info, models: plan.models });
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
if (seed && !plans.some((plan) => plan.info.id === BASE_PROVIDER_ID)) {
|
|
10207
|
+
editor.remove(BASE_PROVIDER_ID);
|
|
10208
|
+
}
|
|
10209
|
+
logStartupDiagnostics(
|
|
10210
|
+
Object.fromEntries(
|
|
10211
|
+
plans.map((plan) => [plan.info.id, { name: plan.info.name, options: plan.info.settings }])
|
|
10212
|
+
),
|
|
10213
|
+
ctx.app?.version
|
|
10214
|
+
);
|
|
10215
|
+
})
|
|
10216
|
+
);
|
|
10217
|
+
registrations.push(
|
|
10218
|
+
await ctx.aisdk.hook("sdk", async (event) => {
|
|
10219
|
+
const providerID = event.model.providerID;
|
|
10220
|
+
if (!isClaudeCodeProviderId(providerID)) return;
|
|
10221
|
+
const settings = await resolveSdkSettings(providerID, planned.get(providerID), event.options);
|
|
10222
|
+
event.sdk = deps.createProvider(settings);
|
|
10223
|
+
log.debug("v2 sdk created", { providerID, cliPath: settings.cliPath });
|
|
10224
|
+
})
|
|
10225
|
+
);
|
|
10226
|
+
registrations.push(
|
|
10227
|
+
await ctx.session.hook("model.request", (event) => {
|
|
10228
|
+
if (!isClaudeCodeProviderId(event.model.providerID)) return;
|
|
10229
|
+
event.headers[SESSION_AFFINITY_HEADER] = event.sessionID;
|
|
10230
|
+
event.headers[OPENCODE_AGENT_HEADER] = agentForRequest(event.kind, event.agent);
|
|
10231
|
+
})
|
|
10232
|
+
);
|
|
10233
|
+
const prompt = ctx.session.prompt;
|
|
10234
|
+
if (ctx.command?.transform && typeof prompt === "function") {
|
|
10235
|
+
const send = (input, command, delivery) => prompt.call(ctx.session, {
|
|
10236
|
+
...input.prompt,
|
|
10237
|
+
sessionID: input.sessionID,
|
|
10238
|
+
text: commandPromptText(command, input.prompt.text),
|
|
10239
|
+
delivery
|
|
10240
|
+
}).then(() => void 0);
|
|
10241
|
+
registrations.push(
|
|
10242
|
+
await ctx.command.transform((editor) => {
|
|
10243
|
+
editor.add({
|
|
10244
|
+
name: DOCTOR_COMMAND,
|
|
10245
|
+
description: DOCTOR_COMMAND_DESCRIPTION,
|
|
10246
|
+
execute: (input) => send(input, DOCTOR_COMMAND, input.delivery)
|
|
10247
|
+
});
|
|
10248
|
+
editor.add({
|
|
10249
|
+
name: "btw",
|
|
10250
|
+
description: BTW_COMMAND_DESCRIPTION,
|
|
10251
|
+
execute: (input) => send(input, "btw", "queue")
|
|
10252
|
+
});
|
|
10253
|
+
})
|
|
10254
|
+
);
|
|
10255
|
+
}
|
|
10256
|
+
const bundledSkills = readBundledSkillInfos();
|
|
10257
|
+
if (typeof ctx.skill?.transform === "function" && bundledSkills.length > 0) {
|
|
10258
|
+
registrations.push(
|
|
10259
|
+
await ctx.skill.transform((editor) => {
|
|
10260
|
+
const added = [];
|
|
10261
|
+
for (const skill of bundledSkills) {
|
|
10262
|
+
if (editor.get(skill.id)) continue;
|
|
10263
|
+
editor.add(skill);
|
|
10264
|
+
added.push(skill.id);
|
|
10265
|
+
}
|
|
10266
|
+
log.debug("v2 bundled skills registered", { added });
|
|
10267
|
+
})
|
|
10268
|
+
);
|
|
10269
|
+
}
|
|
10270
|
+
const events = new AbortController();
|
|
10271
|
+
if (typeof ctx.event?.subscribe === "function") {
|
|
10272
|
+
void releaseDeletedSessions(ctx.event.subscribe.bind(ctx.event), events.signal);
|
|
10273
|
+
}
|
|
10274
|
+
log.info("claude-code plugin set up for opencode 2", {
|
|
10275
|
+
opencode: ctx.app?.version,
|
|
10276
|
+
directory
|
|
10277
|
+
});
|
|
10278
|
+
return async () => {
|
|
10279
|
+
events.abort();
|
|
10280
|
+
for (const registration of registrations.splice(0)) {
|
|
10281
|
+
await registration.dispose().catch(() => void 0);
|
|
10282
|
+
}
|
|
10283
|
+
};
|
|
10284
|
+
};
|
|
10285
|
+
}
|
|
10286
|
+
|
|
9598
10287
|
// src/index.ts
|
|
9599
10288
|
function pickOpencodeDirectory(input) {
|
|
9600
10289
|
if (!input || typeof input !== "object") return void 0;
|
|
@@ -9617,7 +10306,7 @@ function registerSideQuestionCommand(config) {
|
|
|
9617
10306
|
if (config.command.btw) return false;
|
|
9618
10307
|
config.command.btw = {
|
|
9619
10308
|
template: "/btw $ARGUMENTS",
|
|
9620
|
-
description:
|
|
10309
|
+
description: BTW_COMMAND_DESCRIPTION
|
|
9621
10310
|
};
|
|
9622
10311
|
return true;
|
|
9623
10312
|
}
|
|
@@ -9626,7 +10315,7 @@ function registerDoctorCommand(config) {
|
|
|
9626
10315
|
if (config.command[DOCTOR_COMMAND]) return false;
|
|
9627
10316
|
config.command[DOCTOR_COMMAND] = {
|
|
9628
10317
|
template: `/${DOCTOR_COMMAND} $ARGUMENTS`,
|
|
9629
|
-
description:
|
|
10318
|
+
description: DOCTOR_COMMAND_DESCRIPTION
|
|
9630
10319
|
};
|
|
9631
10320
|
return true;
|
|
9632
10321
|
}
|
|
@@ -9675,6 +10364,7 @@ function createClaudeCode(settings = {}) {
|
|
|
9675
10364
|
return new ClaudeCodeLanguageModel(modelId, {
|
|
9676
10365
|
provider: providerName,
|
|
9677
10366
|
cliPath,
|
|
10367
|
+
hostApi: settings.hostApi,
|
|
9678
10368
|
cwd: settings.cwd,
|
|
9679
10369
|
account: settings.account,
|
|
9680
10370
|
configDir: settings.configDir,
|
|
@@ -10004,13 +10694,20 @@ var server = async (input) => {
|
|
|
10004
10694
|
};
|
|
10005
10695
|
var index_default = {
|
|
10006
10696
|
id: "@khalilgharbaoui/opencode-claude-code-plugin",
|
|
10007
|
-
server
|
|
10697
|
+
server,
|
|
10698
|
+
setup: createV2Setup({
|
|
10699
|
+
createProvider: createClaudeCode,
|
|
10700
|
+
defaultProxyTools: DEFAULT_PROXY_TOOL_NAMES,
|
|
10701
|
+
loadConfig: loadMergedOpencodeConfig,
|
|
10702
|
+
buildAgentRegistry: (config) => buildAgentRegistry(config)
|
|
10703
|
+
})
|
|
10008
10704
|
};
|
|
10009
10705
|
export {
|
|
10010
10706
|
ClaudeCodeLanguageModel,
|
|
10011
10707
|
DEFAULT_PROXY_TOOL_NAMES,
|
|
10012
10708
|
_resetPlanModeWarningForTests,
|
|
10013
10709
|
bridgeOpencodeMcp,
|
|
10710
|
+
buildAgentRegistry,
|
|
10014
10711
|
claudeCodeProviders,
|
|
10015
10712
|
configModelsForProvider,
|
|
10016
10713
|
createClaudeCode,
|