@khalilgharbaoui/opencode-claude-code-plugin 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
- return info.status === "rejected" || info.overageStatus === "rejected";
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 mergeOpencodeMcp(cwd, runtimeStatus) {
3915
+ function opencodeConfigLayers(cwd) {
3785
3916
  const worktree = detectWorktree(cwd);
3786
- let merged = {};
3787
- merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
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) merged = mergeMcp(merged, extractMcpBlock(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
- merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
3940
+ layers.push(loadProjectFilesInDir(dir));
3810
3941
  }
3811
3942
  for (const dir of dotOpencodeDirs(cwd, worktree)) {
3812
- merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
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)) {
@@ -4129,6 +4272,7 @@ function logStartupDiagnostics(providers, opencodeVersion) {
4129
4272
 
4130
4273
  // src/doctor.ts
4131
4274
  var DOCTOR_COMMAND = "claude-code-doctor";
4275
+ var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls";
4132
4276
  var DOCTOR_MARKER = "\u258C **claude-code doctor**";
4133
4277
  function isRecord3(value) {
4134
4278
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -5340,6 +5484,33 @@ function registerBundledSkillPath(config) {
5340
5484
  paths.push(dir);
5341
5485
  return true;
5342
5486
  }
5487
+ function readBundledSkillInfos() {
5488
+ return discoverBundledSkills().flatMap((skill) => {
5489
+ const file = path7.join(skill.dir, "SKILL.md");
5490
+ let text;
5491
+ try {
5492
+ text = fs5.readFileSync(file, "utf8");
5493
+ } catch {
5494
+ return [];
5495
+ }
5496
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
5497
+ const frontmatter = match?.[1] ?? "";
5498
+ const field = (key) => {
5499
+ const line = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter)?.[1]?.trim();
5500
+ return line ? line.replace(/^(["'])(.*)\1$/, "$2") : void 0;
5501
+ };
5502
+ const description = field("description");
5503
+ return [
5504
+ {
5505
+ id: skill.name,
5506
+ name: field("name") ?? skill.name,
5507
+ ...description ? { description } : {},
5508
+ path: file,
5509
+ content: match ? text.slice(match[0].length) : text
5510
+ }
5511
+ ];
5512
+ });
5513
+ }
5343
5514
  function linkSkill(source, target) {
5344
5515
  try {
5345
5516
  fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
@@ -5989,7 +6160,7 @@ function spawnInteractiveProcess(opts) {
5989
6160
  }
5990
6161
 
5991
6162
  // src/claude-code-language-model.ts
5992
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
6163
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
5993
6164
  import { unlink as unlink3 } from "fs/promises";
5994
6165
  import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
5995
6166
  import { randomUUID as randomUUID5 } from "crypto";
@@ -6018,6 +6189,22 @@ function resolveSessionAffinity(headers, providerOptions, providerKey) {
6018
6189
  }
6019
6190
  return "default";
6020
6191
  }
6192
+ function resolveOpencodeAgent(headers, providerOptions, providerKey) {
6193
+ if (providerOptions) {
6194
+ const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
6195
+ const agent = bag?.opencodeAgent;
6196
+ if (typeof agent === "string") return agent;
6197
+ }
6198
+ if (headers) {
6199
+ for (const key of Object.keys(headers)) {
6200
+ if (key.toLowerCase() === "x-opencode-agent") {
6201
+ const value = headers[key];
6202
+ if (typeof value === "string" && value.length > 0) return value;
6203
+ }
6204
+ }
6205
+ }
6206
+ return void 0;
6207
+ }
6021
6208
  var KNOWN_DELTA_TYPES = /* @__PURE__ */ new Set([
6022
6209
  "thinking_delta",
6023
6210
  "text_delta",
@@ -6217,9 +6404,9 @@ ${body}`;
6217
6404
  }
6218
6405
  });
6219
6406
  }
6220
- function readPromptFileIfPresent(path9) {
6407
+ function readPromptFileIfPresent(path10) {
6221
6408
  try {
6222
- const content = readFileSync4(path9, "utf8").trim();
6409
+ const content = readFileSync5(path10, "utf8").trim();
6223
6410
  return content || void 0;
6224
6411
  } catch {
6225
6412
  return void 0;
@@ -6334,10 +6521,10 @@ ${options.compressionSummary.trim()}`
6334
6521
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
6335
6522
  const content = parts.join("\n\n");
6336
6523
  if (!content) return void 0;
6337
- const path9 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
6524
+ const path10 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
6338
6525
  try {
6339
- writeFileSync4(path9, content, "utf8");
6340
- return path9;
6526
+ writeFileSync4(path10, content, "utf8");
6527
+ return path10;
6341
6528
  } catch (err) {
6342
6529
  log.warn("failed to write system prompt file", { error: String(err) });
6343
6530
  return void 0;
@@ -6423,6 +6610,19 @@ var ClaudeCodeLanguageModel = class {
6423
6610
  raw: reason
6424
6611
  };
6425
6612
  }
6613
+ /**
6614
+ * Whether this call only names the session, which gets the synthetic stub
6615
+ * rather than a `claude` spawn. opencode 1.x sends a title request with no
6616
+ * tools, and that is the whole test there. opencode 2 sends its tool set
6617
+ * along with it (measured on 2.0.11: `scope: "tools"`, agent `title`), so
6618
+ * every new V2 session paid for a second `claude` process just to title
6619
+ * itself; for a V2 model the request kind, carried as the `title` agent,
6620
+ * decides instead.
6621
+ */
6622
+ isTitleRequest(scope, options) {
6623
+ if (scope === "no-tools") return true;
6624
+ return this.config.hostApi === "v2" && this.getOpencodeAgent(options) === "title";
6625
+ }
6426
6626
  requestScope(options) {
6427
6627
  const tools = options?.tools;
6428
6628
  if (Array.isArray(tools)) return "tools";
@@ -6834,15 +7034,15 @@ var ClaudeCodeLanguageModel = class {
6834
7034
  ];
6835
7035
  return valid.includes(effort) ? effort : void 0;
6836
7036
  }
6837
- getOpencodeAgent(providerOptions) {
6838
- if (!providerOptions) return void 0;
6839
- const ownKey = this.config.provider;
6840
- const bag = providerOptions[ownKey] ?? providerOptions["claude-code"];
6841
- const agent = bag?.opencodeAgent;
6842
- return typeof agent === "string" ? agent : void 0;
7037
+ getOpencodeAgent(options) {
7038
+ return resolveOpencodeAgent(
7039
+ options?.headers,
7040
+ options.providerOptions,
7041
+ this.config.provider
7042
+ );
6843
7043
  }
6844
7044
  isCompactionCall(options) {
6845
- return this.getOpencodeAgent(options.providerOptions) === "compaction";
7045
+ return this.getOpencodeAgent(options) === "compaction";
6846
7046
  }
6847
7047
  /**
6848
7048
  * Pick the model used to handle /compact. Precedence:
@@ -6978,6 +7178,18 @@ var ClaudeCodeLanguageModel = class {
6978
7178
  };
6979
7179
  }
6980
7180
  async doGenerate(options) {
7181
+ const result = await this.doGenerateForHost(options);
7182
+ if (this.config.hostApi !== "v2") return result;
7183
+ const translate = createHostToolPartTranslator("v2");
7184
+ return {
7185
+ ...result,
7186
+ content: result.content.flatMap((part) => {
7187
+ const next = translate(part);
7188
+ return next ? [next] : [];
7189
+ })
7190
+ };
7191
+ }
7192
+ async doGenerateForHost(options) {
6981
7193
  if (!this.isCompactionCall(options) && this.requestScope(options) !== "no-tools" && parseSideQuestion(options.prompt)) {
6982
7194
  return this.doGenerateViaStream(options);
6983
7195
  }
@@ -6986,16 +7198,16 @@ var ClaudeCodeLanguageModel = class {
6986
7198
  const affinity = this.sessionAffinity(options);
6987
7199
  const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
6988
7200
  const effectiveModelId = resolveAgentModel(
6989
- this.getOpencodeAgent(options.providerOptions),
7201
+ this.getOpencodeAgent(options),
6990
7202
  this.modelId
6991
7203
  );
6992
7204
  const reasoningEffort = resolveAgentEffort(
6993
- this.getOpencodeAgent(options.providerOptions),
7205
+ this.getOpencodeAgent(options),
6994
7206
  this.getReasoningEffort(options.providerOptions)
6995
7207
  );
6996
7208
  const baseKey = sessionKey(
6997
7209
  cwd,
6998
- `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options.providerOptions) ?? null])}`
7210
+ `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
6999
7211
  );
7000
7212
  const sk = effortSessionKey(baseKey, reasoningEffort);
7001
7213
  const compactionMode = this.isCompactionCall(options);
@@ -7005,10 +7217,10 @@ var ClaudeCodeLanguageModel = class {
7005
7217
  if (compactionMode) {
7006
7218
  return this.doGenerateViaStream(options);
7007
7219
  }
7008
- if (scope === "no-tools") {
7220
+ if (this.isTitleRequest(scope, options)) {
7009
7221
  log.info("doGenerate no-tools title stub", {
7010
7222
  compactionMode,
7011
- opencodeAgent: this.getOpencodeAgent(options.providerOptions),
7223
+ opencodeAgent: this.getOpencodeAgent(options),
7012
7224
  providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
7013
7225
  });
7014
7226
  const text = this.synthesizeTitle(options.prompt);
@@ -7408,7 +7620,19 @@ ${plan}
7408
7620
  warnings
7409
7621
  };
7410
7622
  }
7623
+ /**
7624
+ * Tool parts leave in opencode 1.x's vocabulary; on opencode 2.x they are
7625
+ * renamed at this one edge (src/host-tools.ts). On V1 the stream is
7626
+ * returned untouched.
7627
+ */
7411
7628
  async doStream(options) {
7629
+ const result = await this.doStreamForHost(options);
7630
+ return {
7631
+ ...result,
7632
+ stream: translateStreamForHost(result.stream, this.config.hostApi ?? "v1")
7633
+ };
7634
+ }
7635
+ async doStreamForHost(options) {
7412
7636
  const warnings = [];
7413
7637
  const skipPermissions = this.config.skipPermissions !== false;
7414
7638
  const scope = this.requestScope(options);
@@ -7416,16 +7640,16 @@ ${plan}
7416
7640
  const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
7417
7641
  const compactionMode = this.isCompactionCall(options);
7418
7642
  const effectiveModelId = compactionMode ? this.resolveCompactionModel() : resolveAgentModel(
7419
- this.getOpencodeAgent(options.providerOptions),
7643
+ this.getOpencodeAgent(options),
7420
7644
  this.modelId
7421
7645
  );
7422
7646
  const reasoningEffort = compactionMode ? void 0 : resolveAgentEffort(
7423
- this.getOpencodeAgent(options.providerOptions),
7647
+ this.getOpencodeAgent(options),
7424
7648
  this.getReasoningEffort(options.providerOptions)
7425
7649
  );
7426
7650
  const baseKey = sessionKey(
7427
7651
  cwd,
7428
- `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options.providerOptions) ?? null])}`
7652
+ `${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
7429
7653
  );
7430
7654
  const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : effortSessionKey(baseKey, reasoningEffort);
7431
7655
  const toUsage = this.toUsage.bind(this);
@@ -7526,10 +7750,10 @@ ${plan}
7526
7750
  });
7527
7751
  return { stream: stream2, request: { body: { text: aside.question } } };
7528
7752
  }
7529
- if (scope === "no-tools" && !compactionMode) {
7753
+ if (this.isTitleRequest(scope, options) && !compactionMode) {
7530
7754
  log.info("doStream no-tools title stub", {
7531
7755
  compactionMode,
7532
- opencodeAgent: this.getOpencodeAgent(options.providerOptions),
7756
+ opencodeAgent: this.getOpencodeAgent(options),
7533
7757
  providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
7534
7758
  });
7535
7759
  const text = this.synthesizeTitle(options.prompt);
@@ -7719,7 +7943,7 @@ ${plan}
7719
7943
  proxyTools: resolvedProxy?.map((t) => t.name) ?? null,
7720
7944
  compactionMode,
7721
7945
  scope,
7722
- opencodeAgent: this.getOpencodeAgent(options.providerOptions),
7946
+ opencodeAgent: this.getOpencodeAgent(options),
7723
7947
  providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
7724
7948
  });
7725
7949
  const stream = new ReadableStream({
@@ -8381,7 +8605,7 @@ ${plan}
8381
8605
  });
8382
8606
  }
8383
8607
  activeProcess?.pendingProxyCompletions?.clear();
8384
- if (accountLimitHit && failoverAskActive) {
8608
+ if (accountLimitHit && failoverAskActive && msg.is_error === true) {
8385
8609
  const call = createAccountFailoverQuestionCall(sk, {
8386
8610
  sourceAccount,
8387
8611
  candidates: failoverAccounts,
@@ -9487,7 +9711,7 @@ ${plan}
9487
9711
  // src/cleanup-stale.ts
9488
9712
  import {
9489
9713
  existsSync as existsSync4,
9490
- readFileSync as readFileSync5,
9714
+ readFileSync as readFileSync6,
9491
9715
  realpathSync,
9492
9716
  rmSync as rmSync3,
9493
9717
  writeFileSync as writeFileSync5
@@ -9514,7 +9738,7 @@ function userIntendsToUseUnscoped() {
9514
9738
  const cfg = userOpencodeJsonPath();
9515
9739
  if (!existsSync4(cfg)) return false;
9516
9740
  try {
9517
- const json = JSON.parse(readFileSync5(cfg, "utf8"));
9741
+ const json = JSON.parse(readFileSync6(cfg, "utf8"));
9518
9742
  const plugins = json.plugin;
9519
9743
  if (!Array.isArray(plugins)) return false;
9520
9744
  return plugins.some(
@@ -9563,7 +9787,7 @@ function cleanupOne(cacheRoot, ourDir) {
9563
9787
  if (!existsSync4(pkgJsonPath)) return;
9564
9788
  let pkg = {};
9565
9789
  try {
9566
- pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
9790
+ pkg = JSON.parse(readFileSync6(pkgJsonPath, "utf8"));
9567
9791
  } catch {
9568
9792
  return;
9569
9793
  }
@@ -9582,7 +9806,7 @@ function cleanupOne(cacheRoot, ourDir) {
9582
9806
  const cachePkgJson = join9(cacheRoot, "package.json");
9583
9807
  if (!existsSync4(cachePkgJson)) return;
9584
9808
  try {
9585
- const cfg = JSON.parse(readFileSync5(cachePkgJson, "utf8"));
9809
+ const cfg = JSON.parse(readFileSync6(cachePkgJson, "utf8"));
9586
9810
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
9587
9811
  delete cfg.dependencies[STALE_PACKAGE_NAME];
9588
9812
  writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -9595,6 +9819,414 @@ function cleanupOne(cacheRoot, ourDir) {
9595
9819
  }
9596
9820
  }
9597
9821
 
9822
+ // src/v2-client.ts
9823
+ import path9 from "path";
9824
+ function formatAgentTypeList(agents) {
9825
+ const lines = agents.flatMap((agent) => {
9826
+ if (agent.hidden === true || agent.mode === "primary") return [];
9827
+ const name = typeof agent.id === "string" ? agent.id : agent.name;
9828
+ if (typeof name !== "string" || name.length === 0) return [];
9829
+ const blurb = typeof agent.description === "string" ? agent.description.trim() : "";
9830
+ return [`- ${name}: ${blurb || "(no description)"}`];
9831
+ });
9832
+ if (lines.length === 0) return void 0;
9833
+ return `Available agent types and the tools they have access to:
9834
+ ${lines.join("\n")}`;
9835
+ }
9836
+ function describe(value) {
9837
+ if (typeof value === "string") return value;
9838
+ if (typeof value === "function") {
9839
+ try {
9840
+ const result = value();
9841
+ return typeof result === "string" ? result : "";
9842
+ } catch {
9843
+ return "";
9844
+ }
9845
+ }
9846
+ return "";
9847
+ }
9848
+ function toV1Session(info) {
9849
+ if (!info || typeof info !== "object") return void 0;
9850
+ const session = info;
9851
+ const root = session.location?.directory;
9852
+ const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path9.resolve(root, session.subpath) : root : void 0;
9853
+ return { ...info, directory, parentID: session.parentID };
9854
+ }
9855
+ function createV1ClientShim(ctx) {
9856
+ const client = {};
9857
+ const mcpList = ctx.mcp?.list;
9858
+ if (typeof mcpList === "function") {
9859
+ client.mcp = {
9860
+ // V1: `{ data: { [server]: { status } } }`. V2: `{ data: [{ name, status: { status } }] }`.
9861
+ status: async () => {
9862
+ const result = await mcpList.call(ctx.mcp);
9863
+ const data = {};
9864
+ for (const server2 of result?.data ?? []) {
9865
+ const status = server2?.status?.status;
9866
+ if (typeof server2?.name === "string" && typeof status === "string") {
9867
+ data[server2.name] = { status };
9868
+ }
9869
+ }
9870
+ return { data };
9871
+ }
9872
+ };
9873
+ }
9874
+ const toolList = ctx.tool?.list;
9875
+ if (typeof toolList === "function") {
9876
+ client.tool = {
9877
+ // V1: `{ data: [{ id, description, parameters }] }`. V2 returns the tools
9878
+ // themselves, with an Effect schema for input rather than JSON Schema, so
9879
+ // `parameters` is empty; only `proxyOpencodeTools` reads it.
9880
+ list: async () => {
9881
+ const tools = await toolList.call(ctx.tool);
9882
+ const data = (tools ?? []).flatMap((tool) => {
9883
+ const id = typeof tool?.id === "string" ? tool.id : tool?.name;
9884
+ return typeof id === "string" ? [{ id, description: describe(tool.description), parameters: {} }] : [];
9885
+ });
9886
+ const subagent = data.find((tool) => tool.id === "subagent");
9887
+ if (subagent && !data.some((tool) => tool.id === "task")) {
9888
+ let agentList;
9889
+ const agentApi = ctx.agent?.list;
9890
+ if (typeof agentApi === "function") {
9891
+ try {
9892
+ agentList = formatAgentTypeList((await agentApi.call(ctx.agent))?.data ?? []);
9893
+ } catch {
9894
+ agentList = void 0;
9895
+ }
9896
+ }
9897
+ const description = agentList ? `${subagent.description}
9898
+
9899
+ ${agentList}` : subagent.description;
9900
+ data.push({ ...subagent, id: "task", description });
9901
+ }
9902
+ return { data };
9903
+ }
9904
+ };
9905
+ }
9906
+ const sessionGet = ctx.session?.get;
9907
+ if (typeof sessionGet === "function") {
9908
+ client.session = {
9909
+ get: async (options) => ({
9910
+ data: toV1Session(await sessionGet.call(ctx.session, { sessionID: options.path.id }))
9911
+ })
9912
+ };
9913
+ }
9914
+ return client;
9915
+ }
9916
+
9917
+ // src/v2.ts
9918
+ var V2_PLUGIN_PACKAGE = "aisdk:@khalilgharbaoui/opencode-claude-code-plugin";
9919
+ var SESSION_AFFINITY_HEADER = "x-session-affinity";
9920
+ var OPENCODE_AGENT_HEADER = "x-opencode-agent";
9921
+ var PLUGIN_ONLY_SETTINGS = ["accounts", "defaultSubagentModel"];
9922
+ var OPENCODE_SDK_OPTION_KEYS = ["fetch", "headers", "body", "name"];
9923
+ function isClaudeCodeProviderId(providerID) {
9924
+ if (typeof providerID !== "string") return false;
9925
+ return providerID === BASE_PROVIDER_ID || providerID.startsWith(`${BASE_PROVIDER_ID}-`);
9926
+ }
9927
+ function agentForRequest(kind, agent) {
9928
+ if (kind === "compaction") return "compaction";
9929
+ if (kind === "title") return "title";
9930
+ return agent;
9931
+ }
9932
+ function releasedAt(date) {
9933
+ const ms = date ? Date.parse(date) : Number.NaN;
9934
+ return Number.isFinite(ms) ? ms : 0;
9935
+ }
9936
+ function modalities(flags) {
9937
+ return Object.entries(flags).filter(([, enabled]) => enabled === true).map(([name]) => name);
9938
+ }
9939
+ function toV2Model(model, providerID, modelId) {
9940
+ return {
9941
+ id: modelId,
9942
+ modelID: modelId,
9943
+ providerID,
9944
+ name: model.name,
9945
+ family: model.family,
9946
+ capabilities: {
9947
+ tools: model.capabilities.toolcall,
9948
+ input: modalities(model.capabilities.input),
9949
+ output: modalities(model.capabilities.output)
9950
+ },
9951
+ variants: Object.entries(model.variants ?? {}).map(([id, settings]) => ({
9952
+ id,
9953
+ settings: { ...settings }
9954
+ })),
9955
+ time: { released: releasedAt(model.release_date) },
9956
+ cost: [
9957
+ {
9958
+ input: model.cost.input,
9959
+ output: model.cost.output,
9960
+ cache: { read: model.cost.cache.read, write: model.cost.cache.write }
9961
+ }
9962
+ ],
9963
+ status: model.status ?? "active",
9964
+ enabled: true,
9965
+ limit: { context: model.limit.context, output: model.limit.output },
9966
+ package: V2_PLUGIN_PACKAGE
9967
+ };
9968
+ }
9969
+ function v2ModelsForProvider(providerID, modelSuffix) {
9970
+ return Object.entries(defaultModels).map(
9971
+ ([id, model]) => toV2Model(model, providerID, modelSuffix ? `${id}@${modelSuffix}` : id)
9972
+ );
9973
+ }
9974
+ function stripPluginOnly(settings) {
9975
+ const result = { ...settings ?? {} };
9976
+ for (const key of PLUGIN_ONLY_SETTINGS) delete result[key];
9977
+ return result;
9978
+ }
9979
+ function planV2Providers(seedSettings, defaultProxyTools) {
9980
+ const base = {
9981
+ cliPath: "claude",
9982
+ proxyTools: [...defaultProxyTools],
9983
+ ...stripPluginOnly(seedSettings)
9984
+ };
9985
+ const accounts = resolveAccounts(seedSettings?.accounts);
9986
+ if (!accounts) {
9987
+ return [
9988
+ {
9989
+ info: {
9990
+ id: BASE_PROVIDER_ID,
9991
+ name: "Claude Code",
9992
+ activation: "enabled",
9993
+ package: V2_PLUGIN_PACKAGE,
9994
+ settings: { ...base, providerID: BASE_PROVIDER_ID }
9995
+ },
9996
+ models: v2ModelsForProvider(BASE_PROVIDER_ID)
9997
+ }
9998
+ ];
9999
+ }
10000
+ return accounts.map((account) => {
10001
+ const providerID = accountProviderId(account);
10002
+ return {
10003
+ info: {
10004
+ id: providerID,
10005
+ name: accountDisplayName(account),
10006
+ activation: "enabled",
10007
+ package: V2_PLUGIN_PACKAGE,
10008
+ settings: {
10009
+ ...base,
10010
+ account,
10011
+ // The resolved list, so this account's language model can offer the
10012
+ // others when it runs out of usage (src/account-failover.ts).
10013
+ failoverAccounts: accounts,
10014
+ providerID,
10015
+ // Kept separately because the wrapper replaces `cliPath`, and a
10016
+ // failover builds another account's wrapper on the same base.
10017
+ baseCliPath: String(base.cliPath ?? "claude")
10018
+ }
10019
+ },
10020
+ models: v2ModelsForProvider(providerID, accountModelSuffix(account))
10021
+ };
10022
+ });
10023
+ }
10024
+ async function resolveSdkSettings(providerID, planned, eventOptions) {
10025
+ const fromEvent = { ...eventOptions };
10026
+ for (const key of OPENCODE_SDK_OPTION_KEYS) delete fromEvent[key];
10027
+ const merged = {
10028
+ ...stripPluginOnly({ ...planned ?? {}, ...fromEvent }),
10029
+ providerID,
10030
+ // The only place a model is told it serves opencode 2.
10031
+ hostApi: "v2"
10032
+ };
10033
+ const account = typeof merged.account === "string" ? merged.account : void 0;
10034
+ if (account) {
10035
+ const base = String(merged.baseCliPath ?? merged.cliPath ?? "claude");
10036
+ const runtime = await ensureAccountRuntime(account, base);
10037
+ Object.assign(merged, runtime, { baseCliPath: base });
10038
+ }
10039
+ return merged;
10040
+ }
10041
+ function plainObject(value) {
10042
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
10043
+ }
10044
+ function configuredSeedSettings(config, pluginOptions) {
10045
+ const provider = plainObject(plainObject(config.provider)?.[BASE_PROVIDER_ID]);
10046
+ return {
10047
+ ...plainObject(provider?.options) ?? {},
10048
+ ...plainObject(provider?.settings) ?? {},
10049
+ ...pluginOptions ?? {}
10050
+ };
10051
+ }
10052
+ function isOpencodeV2Context(ctx) {
10053
+ const context = plainObject(ctx);
10054
+ if (!context) return false;
10055
+ const version = plainObject(context.app)?.version;
10056
+ if (typeof version === "string") {
10057
+ const major = Number.parseInt(version.replace(/^v/, ""), 10);
10058
+ if (Number.isFinite(major) && major < 2) return false;
10059
+ }
10060
+ const hasHook = (domain, name) => typeof plainObject(domain)?.[name] === "function";
10061
+ return hasHook(context.provider, "transform") && hasHook(context.aisdk, "hook") && hasHook(context.session, "hook");
10062
+ }
10063
+ function commandPromptText(command, args) {
10064
+ const rest = (args ?? "").trim();
10065
+ return rest ? `/${command} ${rest}` : `/${command}`;
10066
+ }
10067
+ function deletedSessionIdV2(event) {
10068
+ if (!event || event.type !== "session.deleted") return void 0;
10069
+ const id = event.data?.sessionID;
10070
+ return typeof id === "string" && id.length > 0 ? id : void 0;
10071
+ }
10072
+ async function releaseDeletedSessions(subscribe, signal) {
10073
+ try {
10074
+ for await (const event of subscribe({ signal })) {
10075
+ if (signal.aborted) break;
10076
+ const sessionID = deletedSessionIdV2(event);
10077
+ if (!sessionID) continue;
10078
+ const released = deleteActiveProcessesForSession(sessionID);
10079
+ if (released.length > 0) {
10080
+ log.info("released claude state for deleted session", { sessionID, released });
10081
+ }
10082
+ }
10083
+ } catch (err) {
10084
+ if (!signal.aborted) {
10085
+ log.warn("opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction", {
10086
+ error: err instanceof Error ? err.message : String(err)
10087
+ });
10088
+ }
10089
+ }
10090
+ }
10091
+ function createV2Setup(deps) {
10092
+ return async (ctx) => {
10093
+ if (!isOpencodeV2Context(ctx)) {
10094
+ log.debug("setup called outside opencode 2; leaving it to server()", {
10095
+ version: ctx?.app?.version
10096
+ });
10097
+ return () => void 0;
10098
+ }
10099
+ ensureProcessExitCleanup();
10100
+ setOpencodeClient(createV1ClientShim(ctx));
10101
+ const directory = ctx.location?.directory;
10102
+ setOpencodeProjectDirectory(isUsableDirectory(directory) ? directory : void 0);
10103
+ if (deps.buildAgentRegistry) {
10104
+ try {
10105
+ const config = deps.loadConfig(directory ?? process.cwd());
10106
+ await deps.buildAgentRegistry({
10107
+ ...config,
10108
+ provider: {
10109
+ ...plainObject(config.provider) ?? {},
10110
+ [BASE_PROVIDER_ID]: {
10111
+ options: configuredSeedSettings(config, plainObject(ctx.options))
10112
+ }
10113
+ }
10114
+ });
10115
+ } catch (err) {
10116
+ log.warn("failed to build the agent registry for opencode 2", {
10117
+ error: err instanceof Error ? err.message : String(err)
10118
+ });
10119
+ }
10120
+ }
10121
+ const planned = /* @__PURE__ */ new Map();
10122
+ const registrations = [];
10123
+ registrations.push(
10124
+ await ctx.provider.transform((editor) => {
10125
+ const seed = editor.get(BASE_PROVIDER_ID);
10126
+ const seedSettings = {
10127
+ ...seed?.provider.settings ?? {},
10128
+ ...configuredSeedSettings(
10129
+ deps.loadConfig(directory ?? process.cwd()),
10130
+ plainObject(ctx.options)
10131
+ )
10132
+ };
10133
+ const plans = planV2Providers(seedSettings, deps.defaultProxyTools);
10134
+ planned.clear();
10135
+ for (const plan of plans) {
10136
+ planned.set(plan.info.id, plan.info.settings ?? {});
10137
+ if (editor.get(plan.info.id)) {
10138
+ editor.update(plan.info.id, (provider) => {
10139
+ provider.name = plan.info.name;
10140
+ provider.activation = plan.info.activation;
10141
+ provider.package = plan.info.package;
10142
+ provider.settings = plan.info.settings;
10143
+ });
10144
+ editor.models.set(plan.info.id, plan.models);
10145
+ } else {
10146
+ editor.add({ info: plan.info, models: plan.models });
10147
+ }
10148
+ }
10149
+ if (seed && !plans.some((plan) => plan.info.id === BASE_PROVIDER_ID)) {
10150
+ editor.remove(BASE_PROVIDER_ID);
10151
+ }
10152
+ logStartupDiagnostics(
10153
+ Object.fromEntries(
10154
+ plans.map((plan) => [plan.info.id, { name: plan.info.name, options: plan.info.settings }])
10155
+ ),
10156
+ ctx.app?.version
10157
+ );
10158
+ })
10159
+ );
10160
+ registrations.push(
10161
+ await ctx.aisdk.hook("sdk", async (event) => {
10162
+ const providerID = event.model.providerID;
10163
+ if (!isClaudeCodeProviderId(providerID)) return;
10164
+ const settings = await resolveSdkSettings(providerID, planned.get(providerID), event.options);
10165
+ event.sdk = deps.createProvider(settings);
10166
+ log.debug("v2 sdk created", { providerID, cliPath: settings.cliPath });
10167
+ })
10168
+ );
10169
+ registrations.push(
10170
+ await ctx.session.hook("model.request", (event) => {
10171
+ if (!isClaudeCodeProviderId(event.model.providerID)) return;
10172
+ event.headers[SESSION_AFFINITY_HEADER] = event.sessionID;
10173
+ event.headers[OPENCODE_AGENT_HEADER] = agentForRequest(event.kind, event.agent);
10174
+ })
10175
+ );
10176
+ const prompt = ctx.session.prompt;
10177
+ if (ctx.command?.transform && typeof prompt === "function") {
10178
+ const send = (input, command, delivery) => prompt.call(ctx.session, {
10179
+ ...input.prompt,
10180
+ sessionID: input.sessionID,
10181
+ text: commandPromptText(command, input.prompt.text),
10182
+ delivery
10183
+ }).then(() => void 0);
10184
+ registrations.push(
10185
+ await ctx.command.transform((editor) => {
10186
+ editor.add({
10187
+ name: DOCTOR_COMMAND,
10188
+ description: DOCTOR_COMMAND_DESCRIPTION,
10189
+ execute: (input) => send(input, DOCTOR_COMMAND, input.delivery)
10190
+ });
10191
+ editor.add({
10192
+ name: "btw",
10193
+ description: BTW_COMMAND_DESCRIPTION,
10194
+ execute: (input) => send(input, "btw", "queue")
10195
+ });
10196
+ })
10197
+ );
10198
+ }
10199
+ const bundledSkills = readBundledSkillInfos();
10200
+ if (typeof ctx.skill?.transform === "function" && bundledSkills.length > 0) {
10201
+ registrations.push(
10202
+ await ctx.skill.transform((editor) => {
10203
+ const added = [];
10204
+ for (const skill of bundledSkills) {
10205
+ if (editor.get(skill.id)) continue;
10206
+ editor.add(skill);
10207
+ added.push(skill.id);
10208
+ }
10209
+ log.debug("v2 bundled skills registered", { added });
10210
+ })
10211
+ );
10212
+ }
10213
+ const events = new AbortController();
10214
+ if (typeof ctx.event?.subscribe === "function") {
10215
+ void releaseDeletedSessions(ctx.event.subscribe.bind(ctx.event), events.signal);
10216
+ }
10217
+ log.info("claude-code plugin set up for opencode 2", {
10218
+ opencode: ctx.app?.version,
10219
+ directory
10220
+ });
10221
+ return async () => {
10222
+ events.abort();
10223
+ for (const registration of registrations.splice(0)) {
10224
+ await registration.dispose().catch(() => void 0);
10225
+ }
10226
+ };
10227
+ };
10228
+ }
10229
+
9598
10230
  // src/index.ts
9599
10231
  function pickOpencodeDirectory(input) {
9600
10232
  if (!input || typeof input !== "object") return void 0;
@@ -9617,7 +10249,7 @@ function registerSideQuestionCommand(config) {
9617
10249
  if (config.command.btw) return false;
9618
10250
  config.command.btw = {
9619
10251
  template: "/btw $ARGUMENTS",
9620
- description: "Ask a side question in the live Claude Code session without changing its context"
10252
+ description: BTW_COMMAND_DESCRIPTION
9621
10253
  };
9622
10254
  return true;
9623
10255
  }
@@ -9626,7 +10258,7 @@ function registerDoctorCommand(config) {
9626
10258
  if (config.command[DOCTOR_COMMAND]) return false;
9627
10259
  config.command[DOCTOR_COMMAND] = {
9628
10260
  template: `/${DOCTOR_COMMAND} $ARGUMENTS`,
9629
- description: "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls"
10261
+ description: DOCTOR_COMMAND_DESCRIPTION
9630
10262
  };
9631
10263
  return true;
9632
10264
  }
@@ -9675,6 +10307,7 @@ function createClaudeCode(settings = {}) {
9675
10307
  return new ClaudeCodeLanguageModel(modelId, {
9676
10308
  provider: providerName,
9677
10309
  cliPath,
10310
+ hostApi: settings.hostApi,
9678
10311
  cwd: settings.cwd,
9679
10312
  account: settings.account,
9680
10313
  configDir: settings.configDir,
@@ -10004,13 +10637,20 @@ var server = async (input) => {
10004
10637
  };
10005
10638
  var index_default = {
10006
10639
  id: "@khalilgharbaoui/opencode-claude-code-plugin",
10007
- server
10640
+ server,
10641
+ setup: createV2Setup({
10642
+ createProvider: createClaudeCode,
10643
+ defaultProxyTools: DEFAULT_PROXY_TOOL_NAMES,
10644
+ loadConfig: loadMergedOpencodeConfig,
10645
+ buildAgentRegistry: (config) => buildAgentRegistry(config)
10646
+ })
10008
10647
  };
10009
10648
  export {
10010
10649
  ClaudeCodeLanguageModel,
10011
10650
  DEFAULT_PROXY_TOOL_NAMES,
10012
10651
  _resetPlanModeWarningForTests,
10013
10652
  bridgeOpencodeMcp,
10653
+ buildAgentRegistry,
10014
10654
  claudeCodeProviders,
10015
10655
  configModelsForProvider,
10016
10656
  createClaudeCode,