@khalilgharbaoui/opencode-claude-code-plugin 0.60.2 → 0.61.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -473,6 +473,8 @@ interface ClaudeCodeConfig {
473
473
  bridgeSkipNativeSkills?: boolean;
474
474
  /** Append a one-line cost / duration / cache footer to each finished turn. */
475
475
  turnStats?: boolean;
476
+ /** Turn Claude Code's todo tools on for every model (`CLAUDE_CODE_ENABLE_TODO_TOOLS=1`). */
477
+ enableTodoTools?: boolean;
476
478
  /** Branch a forked opencode session off the parent's Claude conversation. */
477
479
  forkSessions?: boolean;
478
480
  /** Resume a conversation's Claude session after opencode restarts. Default true. */
@@ -992,6 +994,19 @@ interface ClaudeCodeProviderSettings {
992
994
  * `permission_denials` always reach `providerMetadata`.
993
995
  */
994
996
  turnStats?: boolean;
997
+ /**
998
+ * Start every `claude` with `CLAUDE_CODE_ENABLE_TODO_TOOLS=1`, so the CLI
999
+ * offers its todo tools (`TaskCreate` and family) whatever the model.
1000
+ * Claude Code offers them on its own only to Claude 3.x, Opus 4.0 to 4.7,
1001
+ * Sonnet 4.0 to 4.6 and Haiku 4.5; on Opus 4.8, the 5.x models and Fable
1002
+ * no todo list reaches opencode without this (h #g259).
1003
+ *
1004
+ * Off by default, because the CLI turned the tools off for those models on
1005
+ * purpose. Fills a gap only: a `CLAUDE_CODE_ENABLE_TODO_TOOLS` already in
1006
+ * opencode's environment, `0` included, is passed through and decides.
1007
+ * Never on a compaction spawn, which runs no tools.
1008
+ */
1009
+ enableTodoTools?: boolean;
995
1010
  /**
996
1011
  * When a new opencode session turns out to be a fork of one this provider
997
1012
  * already served, branch the parent's Claude conversation with
package/dist/index.js CHANGED
@@ -3808,23 +3808,36 @@ import { createHash } from "crypto";
3808
3808
  var fingerprints = /* @__PURE__ */ new Map();
3809
3809
  var fingerprintSeq = 0;
3810
3810
  var MAX_FORK_FINGERPRINTS = 64;
3811
- var DIGEST_VERSION = 2;
3811
+ var DIGEST_VERSION = 3;
3812
3812
  var DCP_MESSAGE_ID_TAG = /<dcp-message-id(?:\s[^>]*)?>[^<]*<\/dcp-message-id>/g;
3813
3813
  var DCP_COMPACT_REF_LINE = /^@(?:\d+|b\d+|blocked)@(?: \[[^\]\n]*\])?$/;
3814
- function withoutDcpRefs(text) {
3815
- let out = text.includes("<dcp-message-id") ? text.replace(DCP_MESSAGE_ID_TAG, "") : text;
3814
+ var DCP_SYSTEM_REMINDER_BLOCK = /<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g;
3815
+ function withoutDcpRefs(text, version) {
3816
+ let out = text;
3817
+ let removed = false;
3818
+ if (version >= 3 && out.includes("<dcp-system-reminder")) {
3819
+ out = out.replace(DCP_SYSTEM_REMINDER_BLOCK, "");
3820
+ removed = true;
3821
+ }
3822
+ if (out.includes("<dcp-message-id")) {
3823
+ out = out.replace(DCP_MESSAGE_ID_TAG, "");
3824
+ removed = true;
3825
+ }
3816
3826
  for (; ; ) {
3817
3827
  const trimmed = out.trimEnd();
3818
- if (!trimmed.endsWith("@") && !trimmed.endsWith("]")) return out;
3828
+ if (!trimmed.endsWith("@") && !trimmed.endsWith("]")) break;
3819
3829
  const lineStart = trimmed.lastIndexOf("\n") + 1;
3820
- if (!DCP_COMPACT_REF_LINE.test(trimmed.slice(lineStart))) return out;
3830
+ if (!DCP_COMPACT_REF_LINE.test(trimmed.slice(lineStart))) break;
3821
3831
  out = trimmed.slice(0, lineStart);
3832
+ removed = true;
3822
3833
  }
3834
+ if (version < 3 || !removed) return out;
3835
+ return out.replace(/\n{3,}/g, "\n\n").trim();
3823
3836
  }
3824
3837
  function hashPart(hash, part, version) {
3825
3838
  const p = part;
3826
3839
  const type = typeof p?.type === "string" ? p.type : "unknown";
3827
- const content = (text) => version >= 2 ? withoutDcpRefs(text) : text;
3840
+ const content = (text) => version >= 2 ? withoutDcpRefs(text, version) : text;
3828
3841
  switch (type) {
3829
3842
  case "text":
3830
3843
  hash.update("text\0");
@@ -3840,7 +3853,9 @@ function hashPart(hash, part, version) {
3840
3853
  hash.update("result\0");
3841
3854
  hash.update(String(p?.toolName ?? ""));
3842
3855
  hash.update("\0");
3843
- hash.update(stableJson(p?.output, version >= 2 ? withoutDcpRefs : void 0));
3856
+ hash.update(
3857
+ stableJson(p?.output, version >= 2 ? (text) => withoutDcpRefs(text, version) : void 0)
3858
+ );
3844
3859
  break;
3845
3860
  case "file":
3846
3861
  case "image": {
@@ -3888,7 +3903,7 @@ function messageDigest(msg, version) {
3888
3903
  hash.update("\0");
3889
3904
  if (typeof msg.content === "string") {
3890
3905
  hash.update("text\0");
3891
- hash.update(version >= 2 ? withoutDcpRefs(msg.content) : msg.content);
3906
+ hash.update(version >= 2 ? withoutDcpRefs(msg.content, version) : msg.content);
3892
3907
  hash.update("");
3893
3908
  } else if (Array.isArray(msg.content)) {
3894
3909
  for (const part of msg.content) hashPart(hash, part, version);
@@ -7429,6 +7444,7 @@ function findForeignAccountSibling(opts) {
7429
7444
  if (modelSiblingSignature(key, providers) !== signature) return void 0;
7430
7445
  const account = sessionKeyAccount(key, providers);
7431
7446
  if (account === void 0 || account === ownAccount) return void 0;
7447
+ if (opts.only && !opts.only(account)) return void 0;
7432
7448
  return { siblingKey: key, account };
7433
7449
  };
7434
7450
  for (const key of opts.extraKeys ?? []) {
@@ -7872,6 +7888,7 @@ function claudeSpawnEnv(opts) {
7872
7888
  if (opts?.configDir) {
7873
7889
  env.CLAUDE_CONFIG_DIR = opts.configDir;
7874
7890
  }
7891
+ Object.assign(env, todoToolsEnv(opts?.enableTodoTools === true));
7875
7892
  if (opts?.ignoreAnthropicApiKey) {
7876
7893
  delete env.ANTHROPIC_API_KEY;
7877
7894
  delete env.ANTHROPIC_AUTH_TOKEN;
@@ -7881,6 +7898,10 @@ function claudeSpawnEnv(opts) {
7881
7898
  }
7882
7899
  return env;
7883
7900
  }
7901
+ function todoToolsEnv(enabled) {
7902
+ if (!enabled || process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS !== void 0) return {};
7903
+ return { CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" };
7904
+ }
7884
7905
  function touch(key) {
7885
7906
  const existing = activeProcesses.get(key);
7886
7907
  if (existing) {
@@ -8252,7 +8273,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
8252
8273
  clearBackgroundTasks(key);
8253
8274
  }
8254
8275
  }
8255
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl, configDir) {
8276
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl, configDir, enableTodoTools) {
8256
8277
  evictIfNeeded();
8257
8278
  log.info("spawning new claude process", {
8258
8279
  cliPath,
@@ -8261,13 +8282,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8261
8282
  sessionKey: sessionKey2,
8262
8283
  effort,
8263
8284
  promptCacheTtl,
8264
- configDir
8285
+ configDir,
8286
+ ...enableTodoTools ? { enableTodoTools } : {}
8265
8287
  });
8266
8288
  const plan = planClaudeSpawn(cliPath, cliArgs);
8267
8289
  const proc = spawn(plan.file, plan.args, {
8268
8290
  cwd,
8269
8291
  stdio: ["pipe", "pipe", "pipe"],
8270
- env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl, configDir }),
8292
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl, configDir, enableTodoTools }),
8271
8293
  windowsVerbatimArguments: plan.windowsVerbatimArguments
8272
8294
  });
8273
8295
  const lineEmitter = new EventEmitter3();
@@ -8279,6 +8301,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8279
8301
  systemPromptFile,
8280
8302
  effort,
8281
8303
  promptCacheTtl,
8304
+ enableTodoTools,
8282
8305
  startedAt: Date.now(),
8283
8306
  cliPath,
8284
8307
  cliArgs: [...cliArgs],
@@ -8410,7 +8433,8 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
8410
8433
  ignoreAnthropicApiKey,
8411
8434
  old.effort,
8412
8435
  old.promptCacheTtl,
8413
- old.configDir
8436
+ old.configDir,
8437
+ old.enableTodoTools
8414
8438
  );
8415
8439
  replacement.mcpServers = old.mcpServers;
8416
8440
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
@@ -9122,6 +9146,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
9122
9146
  "no configured account could take this turn; it ends here",
9123
9147
  "no proxyTools entry was recognised; nothing will be proxied this turn",
9124
9148
  "no stdout after envelope write; respawning claude process to resume conversation",
9149
+ "not replaying what this claude session missed: the conversation continued on an account in another group",
9125
9150
  "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
9126
9151
  "opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
9127
9152
  "pending proxy call arrived after stream close; rejecting",
@@ -9259,6 +9284,7 @@ var BUNDLE_DATA_ALLOWLIST = {
9259
9284
  historyLength: "count",
9260
9285
  matchedMessages: "count",
9261
9286
  messages: "count",
9287
+ userMessages: "count",
9262
9288
  inputTokens: "count",
9263
9289
  lastTextLength: "count",
9264
9290
  lines: "count",
@@ -11111,6 +11137,11 @@ function formatDoctorReport(report) {
11111
11137
  `| subagentDispatch | ${report.subagentDispatch}${report.subagentDispatch === "ask" && report.subagentDispatchCrossGroup ? " (cross-group accounts allowed)" : ""} |`
11112
11138
  );
11113
11139
  lines.push(`| turnStats | ${report.turnStats} |`);
11140
+ if (report.todoTools) {
11141
+ lines.push(
11142
+ `| enableTodoTools | ${report.todoTools.option}${report.todoTools.envSet ? " (CLAUDE_CODE_ENABLE_TODO_TOOLS is set in opencode's environment and decides)" : ""} |`
11143
+ );
11144
+ }
11114
11145
  lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
11115
11146
  const overriding = report.permissionPresets.filter((row) => row.overrides.length > 0);
11116
11147
  if (overriding.length > 0) {
@@ -11542,6 +11573,10 @@ async function gatherDoctorReport(options) {
11542
11573
  subagentDispatch: base.subagentDispatch,
11543
11574
  subagentDispatchCrossGroup: base.subagentDispatchCrossGroup,
11544
11575
  turnStats: options.turnStats,
11576
+ todoTools: {
11577
+ option: options.enableTodoTools === true,
11578
+ envSet: process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS !== void 0
11579
+ },
11545
11580
  anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
11546
11581
  processes,
11547
11582
  pendingCalls: snapshotPendingProxyCalls(),
@@ -18019,6 +18054,7 @@ var ClaudeCodeLanguageModel = class {
18019
18054
  cliPath,
18020
18055
  interactive: !!useInteractive,
18021
18056
  turnStats: this.config.turnStats === true,
18057
+ enableTodoTools: this.config.enableTodoTools === true,
18022
18058
  ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey === true,
18023
18059
  // Where the plugin applies the account itself rather than through a
18024
18060
  // wrapper (`accountInProcess`, Windows), the `/cost` spawn needs the
@@ -18427,30 +18463,45 @@ var ClaudeCodeLanguageModel = class {
18427
18463
  carried: conversationOnTarget
18428
18464
  });
18429
18465
  }
18430
- let accountGroupNote = null;
18431
- if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
18432
- const foreign = findForeignAccountSibling({
18466
+ let crossGroupAccountRead = false;
18467
+ let crossGroupAccountValue;
18468
+ const crossGroupAccount = () => {
18469
+ if (crossGroupAccountRead) return crossGroupAccountValue;
18470
+ crossGroupAccountRead = true;
18471
+ if (compactionMode || !groups || accountProviders.size === 0) return void 0;
18472
+ if (failoverAnswer?.kind === "switch") return void 0;
18473
+ crossGroupAccountValue = findForeignAccountSibling({
18433
18474
  sessionKey: sk,
18434
18475
  accountProviders,
18435
18476
  // The live process's own view, so the guard holds with
18436
18477
  // `resumeAfterRestart: false`, where the store is never written.
18437
- extraKeys: listClaudeSessionKeys()
18478
+ extraKeys: listClaudeSessionKeys(),
18479
+ only: (account) => !accountsShareGroup(sourceAccount, account, groups)
18480
+ })?.account;
18481
+ return crossGroupAccountValue;
18482
+ };
18483
+ const accountGroupNoteFor = (from) => formatAccountGroupNote({
18484
+ sourceAccount: from,
18485
+ sourceGroup: accountGroup(from, groups),
18486
+ targetAccount: sourceAccount,
18487
+ targetGroup: accountGroup(sourceAccount, groups)
18488
+ });
18489
+ const logAnotherAccountGroup = (from) => {
18490
+ log.notice("starting fresh on this account: it is in another account group", {
18491
+ sessionKey: sk,
18492
+ reason: "another-account-group",
18493
+ account: sourceAccount,
18494
+ from,
18495
+ group: accountGroup(sourceAccount, groups)
18438
18496
  });
18439
- if (foreign && !accountsShareGroup(sourceAccount, foreign.account, groups)) {
18497
+ };
18498
+ let accountGroupNote = null;
18499
+ if (includeHistoryContext) {
18500
+ const from = crossGroupAccount();
18501
+ if (from) {
18440
18502
  includeHistoryContext = false;
18441
- accountGroupNote = formatAccountGroupNote({
18442
- sourceAccount: foreign.account,
18443
- sourceGroup: accountGroup(foreign.account, groups),
18444
- targetAccount: sourceAccount,
18445
- targetGroup: accountGroup(sourceAccount, groups)
18446
- });
18447
- log.notice("starting fresh on this account: it is in another account group", {
18448
- sessionKey: sk,
18449
- reason: "another-account-group",
18450
- account: sourceAccount,
18451
- from: foreign.account,
18452
- group: accountGroup(sourceAccount, groups)
18453
- });
18503
+ accountGroupNote = accountGroupNoteFor(from);
18504
+ logAnotherAccountGroup(from);
18454
18505
  }
18455
18506
  }
18456
18507
  let forkFromClaudeSessionId;
@@ -18600,28 +18651,52 @@ var ClaudeCodeLanguageModel = class {
18600
18651
  });
18601
18652
  }
18602
18653
  const conversationChange = !compactionMode && !includeHistoryContext && failoverAnswer?.kind !== "switch" && !accountGroupNote && (!!getClaudeSessionId(sk) || !!getActiveProcess(sk)) ? classifyConversationChange(sk, filterSideQuestionHistory(options.prompt)) : null;
18603
- const missed = conversationChange?.kind === "missed" ? conversationChange : null;
18604
- if (missed) {
18605
- log.notice("replaying the messages this claude session missed: the conversation continued under another session key", {
18606
- sessionKey: sk,
18607
- messages: missed.messages.length,
18608
- userMessages: missed.userMessages
18609
- });
18654
+ let missed = null;
18655
+ if (conversationChange?.kind === "missed") {
18656
+ const from = crossGroupAccount();
18657
+ if (from) {
18658
+ log.notice(
18659
+ "not replaying what this claude session missed: the conversation continued on an account in another group",
18660
+ {
18661
+ sessionKey: sk,
18662
+ reason: "another-account-group",
18663
+ account: sourceAccount,
18664
+ from,
18665
+ group: accountGroup(sourceAccount, groups),
18666
+ messages: conversationChange.messages.length,
18667
+ userMessages: conversationChange.userMessages
18668
+ }
18669
+ );
18670
+ } else {
18671
+ missed = conversationChange;
18672
+ log.notice("replaying the messages this claude session missed: the conversation continued under another session key", {
18673
+ sessionKey: sk,
18674
+ messages: missed.messages.length,
18675
+ userMessages: missed.userMessages
18676
+ });
18677
+ }
18610
18678
  }
18611
18679
  let rewoundNote = null;
18612
18680
  if (conversationChange?.kind === "rewound") {
18613
18681
  await deleteActiveProcessAndWait(sk);
18614
18682
  deleteClaudeSessionId(sk);
18615
- includeHistoryContext = true;
18616
- replayReason = "history-rewound";
18617
- log.notice("replaying the conversation as text: the thread was edited or undone", {
18618
- sessionKey: sk,
18619
- reason: "history-rewound",
18620
- sharedMessages: conversationChange.sharedMessages,
18621
- removedMessages: conversationChange.removedMessages,
18622
- removedUserMessages: conversationChange.removedUserMessages
18623
- });
18624
- if (mode !== "generate") rewoundNote = formatConversationRewoundNote();
18683
+ const from = crossGroupAccount();
18684
+ if (from) {
18685
+ includeHistoryContext = false;
18686
+ accountGroupNote = accountGroupNote ?? accountGroupNoteFor(from);
18687
+ logAnotherAccountGroup(from);
18688
+ } else {
18689
+ includeHistoryContext = true;
18690
+ replayReason = "history-rewound";
18691
+ log.notice("replaying the conversation as text: the thread was edited or undone", {
18692
+ sessionKey: sk,
18693
+ reason: "history-rewound",
18694
+ sharedMessages: conversationChange.sharedMessages,
18695
+ removedMessages: conversationChange.removedMessages,
18696
+ removedUserMessages: conversationChange.removedUserMessages
18697
+ });
18698
+ if (mode !== "generate") rewoundNote = formatConversationRewoundNote();
18699
+ }
18625
18700
  }
18626
18701
  if (this.config.forkSessions === true && !compactionMode) {
18627
18702
  recordForkFingerprint(sk, options.prompt, cliPath);
@@ -19219,7 +19294,8 @@ var ClaudeCodeLanguageModel = class {
19219
19294
  claudeSpawnEnv({
19220
19295
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
19221
19296
  effort: reasoningEffort,
19222
- promptCacheTtl
19297
+ promptCacheTtl,
19298
+ enableTodoTools: self.config.enableTodoTools === true
19223
19299
  })
19224
19300
  )
19225
19301
  });
@@ -19337,7 +19413,9 @@ var ClaudeCodeLanguageModel = class {
19337
19413
  self.config.ignoreAnthropicApiKey,
19338
19414
  reasoningEffort,
19339
19415
  promptCacheTtl,
19340
- spawnConfigDir
19416
+ spawnConfigDir,
19417
+ // A compaction spawn runs no tools, so it never asks for them.
19418
+ self.config.enableTodoTools === true && !compactionMode
19341
19419
  );
19342
19420
  ap.mcpServers = spawnMcpServers;
19343
19421
  state.proc = ap.proc;
@@ -21070,6 +21148,7 @@ function createClaudeCode(settings = {}) {
21070
21148
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
21071
21149
  bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
21072
21150
  turnStats: settings.turnStats === true,
21151
+ enableTodoTools: settings.enableTodoTools === true,
21073
21152
  forkSessions: settings.forkSessions === true,
21074
21153
  resumeAfterRestart: settings.resumeAfterRestart !== false,
21075
21154
  resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,