opencode-acp 1.18.1-pr.416.178 → 1.18.1-pr.432.180

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
@@ -2479,163 +2479,6 @@ import { existsSync as existsSync2 } from "fs";
2479
2479
  import { homedir as homedir2 } from "os";
2480
2480
  import { isAbsolute, join as join2 } from "path";
2481
2481
 
2482
- // lib/message-ids.ts
2483
- var MESSAGE_REF_REGEX = /^m(\d{4,5})$/;
2484
- var BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
2485
- var MESSAGE_ID_TAG_NAME = "dcp-message-id";
2486
- var MESSAGE_REF_WIDTH = 5;
2487
- var MESSAGE_REF_MIN_INDEX = 1;
2488
- var MESSAGE_REF_MAX_INDEX = 99999;
2489
- function formatMessageRef(index) {
2490
- if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
2491
- throw new Error(
2492
- `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
2493
- );
2494
- }
2495
- return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
2496
- }
2497
- function formatBlockRef(blockId) {
2498
- if (!Number.isInteger(blockId) || blockId < 1) {
2499
- throw new Error(`Invalid block ID: ${blockId}`);
2500
- }
2501
- return `b${blockId}`;
2502
- }
2503
- function parseMessageRef(ref) {
2504
- const normalized = ref.trim().toLowerCase();
2505
- const match = normalized.match(MESSAGE_REF_REGEX);
2506
- if (!match) {
2507
- return null;
2508
- }
2509
- const index = Number.parseInt(match[1], 10);
2510
- if (!Number.isInteger(index)) {
2511
- return null;
2512
- }
2513
- if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
2514
- return null;
2515
- }
2516
- return index;
2517
- }
2518
- function migrateMessageRef(ref) {
2519
- const index = parseMessageRef(ref);
2520
- return index === null ? ref : formatMessageRef(index);
2521
- }
2522
- function parseBlockRef(ref) {
2523
- const normalized = ref.trim().toLowerCase();
2524
- const match = normalized.match(BLOCK_REF_REGEX);
2525
- if (!match) {
2526
- return null;
2527
- }
2528
- const id = Number.parseInt(match[1], 10);
2529
- return Number.isInteger(id) ? id : null;
2530
- }
2531
- function parseBoundaryId(id) {
2532
- const normalized = id.trim().toLowerCase();
2533
- const messageIndex = parseMessageRef(normalized);
2534
- if (messageIndex !== null) {
2535
- return {
2536
- kind: "message",
2537
- ref: formatMessageRef(messageIndex),
2538
- index: messageIndex
2539
- };
2540
- }
2541
- const blockId = parseBlockRef(normalized);
2542
- if (blockId !== null) {
2543
- return {
2544
- kind: "compressed-block",
2545
- ref: formatBlockRef(blockId),
2546
- blockId
2547
- };
2548
- }
2549
- return null;
2550
- }
2551
- function escapeXmlAttribute(value) {
2552
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2553
- }
2554
- function formatMessageIdTag(ref, attributes) {
2555
- const serializedAttributes = Object.entries(attributes || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => {
2556
- if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
2557
- return "";
2558
- }
2559
- return ` ${name}="${escapeXmlAttribute(value)}"`;
2560
- }).join("");
2561
- return `
2562
- <${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`;
2563
- }
2564
- function formatTokenSize(tokens) {
2565
- if (tokens < 1e3) return String(tokens);
2566
- if (tokens < 1e4) return `${(tokens / 1e3).toFixed(1)}K`;
2567
- return `${Math.round(tokens / 1e3)}K`;
2568
- }
2569
- function classifyMessageType(parts) {
2570
- let hasTool = false;
2571
- let hasText = false;
2572
- let hasReasoning = false;
2573
- const toolNames = [];
2574
- for (const part of parts) {
2575
- if (part.type === "tool") {
2576
- hasTool = true;
2577
- if (typeof part.tool === "string" && !toolNames.includes(part.tool)) {
2578
- toolNames.push(part.tool);
2579
- }
2580
- } else if (part.type === "text") {
2581
- hasText = true;
2582
- } else if (part.type === "reasoning") {
2583
- hasReasoning = true;
2584
- }
2585
- }
2586
- if (hasTool) {
2587
- return toolNames.length > 0 ? `tool:${toolNames.join(",")}` : "tool";
2588
- }
2589
- if (hasReasoning && !hasText) return "reasoning";
2590
- return "text";
2591
- }
2592
- function assignMessageRefs(state, messages) {
2593
- let assigned = 0;
2594
- let skippedSubAgentPrompt = false;
2595
- for (const message of messages) {
2596
- if (isIgnoredUserMessage(message)) {
2597
- continue;
2598
- }
2599
- if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
2600
- skippedSubAgentPrompt = true;
2601
- continue;
2602
- }
2603
- const rawMessageId = message.info.id;
2604
- if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
2605
- continue;
2606
- }
2607
- if (rawMessageId.startsWith("msg_dcp_summary_") || rawMessageId.startsWith("msg_dcp_text_")) {
2608
- continue;
2609
- }
2610
- const existingRef = state.messageIds.byRawId.get(rawMessageId);
2611
- if (existingRef) {
2612
- if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
2613
- state.messageIds.byRef.set(existingRef, rawMessageId);
2614
- }
2615
- continue;
2616
- }
2617
- const ref = allocateNextMessageRef(state);
2618
- state.messageIds.byRawId.set(rawMessageId, ref);
2619
- state.messageIds.byRef.set(ref, rawMessageId);
2620
- assigned++;
2621
- }
2622
- return assigned;
2623
- }
2624
- function allocateNextMessageRef(state) {
2625
- let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
2626
- while (candidate <= MESSAGE_REF_MAX_INDEX) {
2627
- const ref = formatMessageRef(candidate);
2628
- if (!state.messageIds.byRef.has(ref)) {
2629
- state.messageIds.nextRef = candidate + 1;
2630
- return ref;
2631
- }
2632
- candidate++;
2633
- }
2634
- throw new Error(
2635
- `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
2636
- );
2637
- }
2638
-
2639
2482
  // lib/state/utils.ts
2640
2483
  var isMessageCompacted = (state, msg) => {
2641
2484
  if (!isMessageWithInfo(msg)) {
@@ -2787,10 +2630,8 @@ function loadPruneMessagesState(persisted) {
2787
2630
  mode: block.mode === "range" || block.mode === "message" ? block.mode : void 0,
2788
2631
  topic: typeof block.topic === "string" ? block.topic : "",
2789
2632
  batchTopic: typeof block.batchTopic === "string" ? block.batchTopic : typeof block.topic === "string" ? block.topic : "",
2790
- // [Issue #415] Migrate legacy 4-digit boundary refs (pre-1.1.0 state)
2791
- // to canonical 5-digit form, matching the messageIds ref migration.
2792
- startId: typeof block.startId === "string" ? migrateMessageRef(block.startId) : "",
2793
- endId: typeof block.endId === "string" ? migrateMessageRef(block.endId) : "",
2633
+ startId: typeof block.startId === "string" ? block.startId : "",
2634
+ endId: typeof block.endId === "string" ? block.endId : "",
2794
2635
  anchorMessageId: typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
2795
2636
  compressMessageId: typeof block.compressMessageId === "string" ? block.compressMessageId : "",
2796
2637
  compressCallId: typeof block.compressCallId === "string" ? block.compressCallId : void 0,
@@ -3020,8 +2861,8 @@ function saveSessionState(sessionState, logger, sessionName) {
3020
2861
  queue = { pending: [], waiters: [], draining: false };
3021
2862
  saveQueues.set(key, queue);
3022
2863
  }
3023
- const promise = new Promise((resolve3, reject) => {
3024
- queue.waiters.push({ resolve: resolve3, reject });
2864
+ const promise = new Promise((resolve2, reject) => {
2865
+ queue.waiters.push({ resolve: resolve2, reject });
3025
2866
  });
3026
2867
  queue.pending.push({
3027
2868
  sessionId: sessionState.sessionId,
@@ -3145,6 +2986,159 @@ import { existsSync as existsSync3 } from "fs";
3145
2986
  import { join as join3 } from "path";
3146
2987
  import { cwd } from "process";
3147
2988
 
2989
+ // lib/message-ids.ts
2990
+ var MESSAGE_REF_REGEX = /^m(\d{4,5})$/;
2991
+ var BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
2992
+ var MESSAGE_ID_TAG_NAME = "dcp-message-id";
2993
+ var MESSAGE_REF_WIDTH = 5;
2994
+ var MESSAGE_REF_MIN_INDEX = 1;
2995
+ var MESSAGE_REF_MAX_INDEX = 99999;
2996
+ function formatMessageRef(index) {
2997
+ if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
2998
+ throw new Error(
2999
+ `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
3000
+ );
3001
+ }
3002
+ return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
3003
+ }
3004
+ function formatBlockRef(blockId) {
3005
+ if (!Number.isInteger(blockId) || blockId < 1) {
3006
+ throw new Error(`Invalid block ID: ${blockId}`);
3007
+ }
3008
+ return `b${blockId}`;
3009
+ }
3010
+ function parseMessageRef(ref) {
3011
+ const normalized = ref.trim().toLowerCase();
3012
+ const match = normalized.match(MESSAGE_REF_REGEX);
3013
+ if (!match) {
3014
+ return null;
3015
+ }
3016
+ const index = Number.parseInt(match[1], 10);
3017
+ if (!Number.isInteger(index)) {
3018
+ return null;
3019
+ }
3020
+ if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
3021
+ return null;
3022
+ }
3023
+ return index;
3024
+ }
3025
+ function parseBlockRef(ref) {
3026
+ const normalized = ref.trim().toLowerCase();
3027
+ const match = normalized.match(BLOCK_REF_REGEX);
3028
+ if (!match) {
3029
+ return null;
3030
+ }
3031
+ const id = Number.parseInt(match[1], 10);
3032
+ return Number.isInteger(id) ? id : null;
3033
+ }
3034
+ function parseBoundaryId(id) {
3035
+ const normalized = id.trim().toLowerCase();
3036
+ const messageIndex = parseMessageRef(normalized);
3037
+ if (messageIndex !== null) {
3038
+ return {
3039
+ kind: "message",
3040
+ ref: formatMessageRef(messageIndex),
3041
+ index: messageIndex
3042
+ };
3043
+ }
3044
+ const blockId = parseBlockRef(normalized);
3045
+ if (blockId !== null) {
3046
+ return {
3047
+ kind: "compressed-block",
3048
+ ref: formatBlockRef(blockId),
3049
+ blockId
3050
+ };
3051
+ }
3052
+ return null;
3053
+ }
3054
+ function escapeXmlAttribute(value) {
3055
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3056
+ }
3057
+ function formatMessageIdTag(ref, attributes) {
3058
+ const serializedAttributes = Object.entries(attributes || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => {
3059
+ if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
3060
+ return "";
3061
+ }
3062
+ return ` ${name}="${escapeXmlAttribute(value)}"`;
3063
+ }).join("");
3064
+ return `
3065
+ <${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`;
3066
+ }
3067
+ function formatTokenSize(tokens) {
3068
+ if (tokens < 1e3) return String(tokens);
3069
+ if (tokens < 1e4) return `${(tokens / 1e3).toFixed(1)}K`;
3070
+ return `${Math.round(tokens / 1e3)}K`;
3071
+ }
3072
+ function classifyMessageType(parts) {
3073
+ let hasTool = false;
3074
+ let hasText = false;
3075
+ let hasReasoning = false;
3076
+ const toolNames = [];
3077
+ for (const part of parts) {
3078
+ if (part.type === "tool") {
3079
+ hasTool = true;
3080
+ if (typeof part.tool === "string" && !toolNames.includes(part.tool)) {
3081
+ toolNames.push(part.tool);
3082
+ }
3083
+ } else if (part.type === "text") {
3084
+ hasText = true;
3085
+ } else if (part.type === "reasoning") {
3086
+ hasReasoning = true;
3087
+ }
3088
+ }
3089
+ if (hasTool) {
3090
+ return toolNames.length > 0 ? `tool:${toolNames.join(",")}` : "tool";
3091
+ }
3092
+ if (hasReasoning && !hasText) return "reasoning";
3093
+ return "text";
3094
+ }
3095
+ function assignMessageRefs(state, messages) {
3096
+ let assigned = 0;
3097
+ let skippedSubAgentPrompt = false;
3098
+ for (const message of messages) {
3099
+ if (isIgnoredUserMessage(message)) {
3100
+ continue;
3101
+ }
3102
+ if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
3103
+ skippedSubAgentPrompt = true;
3104
+ continue;
3105
+ }
3106
+ const rawMessageId = message.info.id;
3107
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
3108
+ continue;
3109
+ }
3110
+ if (rawMessageId.startsWith("msg_dcp_summary_") || rawMessageId.startsWith("msg_dcp_text_")) {
3111
+ continue;
3112
+ }
3113
+ const existingRef = state.messageIds.byRawId.get(rawMessageId);
3114
+ if (existingRef) {
3115
+ if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
3116
+ state.messageIds.byRef.set(existingRef, rawMessageId);
3117
+ }
3118
+ continue;
3119
+ }
3120
+ const ref = allocateNextMessageRef(state);
3121
+ state.messageIds.byRawId.set(rawMessageId, ref);
3122
+ state.messageIds.byRef.set(ref, rawMessageId);
3123
+ assigned++;
3124
+ }
3125
+ return assigned;
3126
+ }
3127
+ function allocateNextMessageRef(state) {
3128
+ let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
3129
+ while (candidate <= MESSAGE_REF_MAX_INDEX) {
3130
+ const ref = formatMessageRef(candidate);
3131
+ if (!state.messageIds.byRef.has(ref)) {
3132
+ state.messageIds.nextRef = candidate + 1;
3133
+ return ref;
3134
+ }
3135
+ candidate++;
3136
+ }
3137
+ throw new Error(
3138
+ `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
3139
+ );
3140
+ }
3141
+
3148
3142
  // lib/compress/state.ts
3149
3143
  var DEFAULT_PROMOTION_THRESHOLD = 5;
3150
3144
  var COMPRESSED_BLOCK_HEADER = "[Compressed conversation section]";
@@ -4616,11 +4610,7 @@ function restoreForkCompressionState(state, forkMessages, parent, parentMessages
4616
4610
  directMessageIds,
4617
4611
  directToolIds,
4618
4612
  effectiveMessageIds,
4619
- effectiveToolIds,
4620
- // [Issue #415] The parent record is raw persisted JSON; legacy
4621
- // 4-digit boundary refs must be normalized like every other ref.
4622
- startId: typeof block.startId === "string" ? migrateMessageRef(block.startId) : block.startId,
4623
- endId: typeof block.endId === "string" ? migrateMessageRef(block.endId) : block.endId
4613
+ effectiveToolIds
4624
4614
  });
4625
4615
  }
4626
4616
  const activeParentIds = new Set(parent.prune.messages.activeBlockIds);
@@ -6832,6 +6822,31 @@ function buildToolIdList(state, messages) {
6832
6822
  var stripHallucinationsFromString = (text) => {
6833
6823
  return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
6834
6824
  };
6825
+ var LEAKED_TRAILING_REF_REGEX = /(?:^|\n)[ \t]*(?:m(\d{4,5})|b([1-9]\d*))\b/g;
6826
+ var stripLeakedTrailingRefs = (text, bounds) => {
6827
+ if (typeof text !== "string" || text.length === 0) {
6828
+ return text;
6829
+ }
6830
+ const hasMessageBound = typeof bounds.nextMessageRef === "number" && Number.isInteger(bounds.nextMessageRef);
6831
+ const hasBlockBound = typeof bounds.nextBlockRef === "number" && Number.isInteger(bounds.nextBlockRef);
6832
+ if (!hasMessageBound && !hasBlockBound) {
6833
+ return text;
6834
+ }
6835
+ const pattern = new RegExp(LEAKED_TRAILING_REF_REGEX.source, "g");
6836
+ let match;
6837
+ while ((match = pattern.exec(text)) !== null) {
6838
+ if (match[1] !== void 0) {
6839
+ if (hasMessageBound && Number(match[1]) >= bounds.nextMessageRef) {
6840
+ return text.slice(0, match.index).replace(/[ \t\r\n]*$/, "");
6841
+ }
6842
+ } else if (match[2] !== void 0) {
6843
+ if (hasBlockBound && Number(match[2]) >= bounds.nextBlockRef) {
6844
+ return text.slice(0, match.index).replace(/[ \t\r\n]*$/, "");
6845
+ }
6846
+ }
6847
+ }
6848
+ return text;
6849
+ };
6835
6850
  var stripHallucinations = (messages) => {
6836
6851
  for (const message of messages) {
6837
6852
  for (const part of message.parts) {
@@ -9598,102 +9613,6 @@ function extractTextContent(msg) {
9598
9613
  return textParts.join(" ").replace(/\s+/g, " ").trim();
9599
9614
  }
9600
9615
 
9601
- // lib/compress/tofile-target.ts
9602
- import * as fsp from "fs/promises";
9603
- import { homedir as homedir3, tmpdir } from "os";
9604
- import { dirname as dirname2, join as join4, relative, resolve } from "path";
9605
- async function resolveSafeToFileTarget(targetPath, options) {
9606
- const osTmp = tmpdir();
9607
- const allowedDirs = options?.allowedDirs ?? [osTmp, join4(homedir3(), ".cache", "opencode")];
9608
- const dirList = allowedDirs.join(" or ");
9609
- if (typeof targetPath !== "string" || targetPath.length === 0) {
9610
- return {
9611
- ok: false,
9612
- error: `Error: toFile path must be under ${dirList}. Got: ${targetPath}`
9613
- };
9614
- }
9615
- const resolvedPath = resolve(targetPath);
9616
- const lexicallyAllowed = allowedDirs.some((dir) => {
9617
- const rel = relative(resolve(dir), resolvedPath);
9618
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !isWinAbsolute(rel);
9619
- });
9620
- if (!lexicallyAllowed) {
9621
- return {
9622
- ok: false,
9623
- error: `Error: toFile path must be under ${dirList}. Got: ${targetPath}`
9624
- };
9625
- }
9626
- let realAncestor = null;
9627
- const tail = [];
9628
- let current = resolvedPath;
9629
- while (true) {
9630
- try {
9631
- realAncestor = await fsp.realpath(current);
9632
- break;
9633
- } catch (err) {
9634
- const code = err.code;
9635
- if (code !== "ENOENT" && code !== "ENOTDIR") {
9636
- return {
9637
- ok: false,
9638
- error: `Error: cannot validate toFile path: ${err.message}`
9639
- };
9640
- }
9641
- const parent = dirname2(current);
9642
- if (parent === current) {
9643
- return {
9644
- ok: false,
9645
- error: `Error: cannot validate toFile path: no existing ancestor found`
9646
- };
9647
- }
9648
- tail.unshift(basename(current));
9649
- current = parent;
9650
- }
9651
- }
9652
- const physicalPath = tail.length > 0 ? join4(realAncestor, ...tail) : realAncestor;
9653
- const realAllowedDirs = [];
9654
- for (const dir of allowedDirs) {
9655
- try {
9656
- realAllowedDirs.push(await fsp.realpath(resolve(dir)));
9657
- } catch {
9658
- }
9659
- }
9660
- const physicallyAllowed = realAllowedDirs.some((dir) => containsOrIs(dir, physicalPath));
9661
- if (!physicallyAllowed) {
9662
- return {
9663
- ok: false,
9664
- error: `Error: toFile path resolves outside the allowed directories via a symlink. Got: ${targetPath}`
9665
- };
9666
- }
9667
- try {
9668
- const stat = await fsp.lstat(resolvedPath);
9669
- if (stat.isSymbolicLink()) {
9670
- return {
9671
- ok: false,
9672
- error: `Error: toFile target exists as a symlink and will not be followed. Got: ${targetPath}`
9673
- };
9674
- }
9675
- } catch (err) {
9676
- const code = err.code;
9677
- if (code !== "ENOENT") {
9678
- return {
9679
- ok: false,
9680
- error: `Error: cannot validate toFile path: ${err.message}`
9681
- };
9682
- }
9683
- }
9684
- return { ok: true, filePath: resolvedPath };
9685
- }
9686
- function isWinAbsolute(p) {
9687
- return /^[a-zA-Z]:[\\/]/.test(p);
9688
- }
9689
- function basename(p) {
9690
- return p.split(/[\\/]/).filter(Boolean).pop() ?? p;
9691
- }
9692
- function containsOrIs(root, p) {
9693
- const rel = relative(root, p);
9694
- return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !isWinAbsolute(rel);
9695
- }
9696
-
9697
9616
  // lib/compress/decompress.ts
9698
9617
  async function prepareDecompressSession(ctx, toolCtx) {
9699
9618
  await toolCtx.ask({
@@ -9727,13 +9646,7 @@ function resolveTargets(args, state, rawMessages, logger) {
9727
9646
  if (mode.mode === "block") {
9728
9647
  return resolveSingleBlockTarget(messagesState, args.blockId);
9729
9648
  }
9730
- return resolveRangeTarget(
9731
- state,
9732
- rawMessages,
9733
- args.startId,
9734
- args.endId,
9735
- logger
9736
- );
9649
+ return resolveRangeTarget(state, rawMessages, args.startId, args.endId, logger);
9737
9650
  }
9738
9651
  function resolveSingleBlockTarget(messagesState, blockIdArg) {
9739
9652
  const targetBlockId = parseBlockIdArg(blockIdArg);
@@ -9845,21 +9758,11 @@ IMPORTANT:
9845
9758
  - Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
9846
9759
  function buildSchema2() {
9847
9760
  return {
9848
- blockId: tool3.schema.string().optional().describe(
9849
- 'Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'
9850
- ),
9851
- startId: tool3.schema.string().optional().describe(
9852
- 'Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'
9853
- ),
9854
- endId: tool3.schema.string().optional().describe(
9855
- 'Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'
9856
- ),
9857
- toFile: tool3.schema.string().optional().describe(
9858
- "If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"
9859
- ),
9860
- full: tool3.schema.boolean().optional().describe(
9861
- "If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it."
9862
- )
9761
+ blockId: tool3.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
9762
+ startId: tool3.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
9763
+ endId: tool3.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
9764
+ toFile: tool3.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"),
9765
+ full: tool3.schema.boolean().optional().describe("If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it.")
9863
9766
  };
9864
9767
  }
9865
9768
  function extractMessageId(m) {
@@ -9883,12 +9786,7 @@ function createDecompressTool(factoryCtx) {
9883
9786
  const contextUsageBefore = effectiveLimitBefore ? Math.round(
9884
9787
  getCurrentTokenUsage(ctx.state, rawMessages) / effectiveLimitBefore.limit * 100
9885
9788
  ) : void 0;
9886
- const resolved = resolveTargets(
9887
- args,
9888
- ctx.state,
9889
- rawMessages,
9890
- ctx.logger
9891
- );
9789
+ const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
9892
9790
  if (!resolved.ok) {
9893
9791
  return resolved.error;
9894
9792
  }
@@ -9904,9 +9802,19 @@ function createDecompressTool(factoryCtx) {
9904
9802
  }
9905
9803
  if (args.toFile) {
9906
9804
  const targetPath = args.toFile;
9907
- const safe = await resolveSafeToFileTarget(targetPath);
9908
- if (!safe.ok) {
9909
- return safe.error;
9805
+ const os = await import("os");
9806
+ const path = await import("path");
9807
+ const allowedDirs = [
9808
+ os.tmpdir() + "/",
9809
+ path.join(os.homedir(), ".cache", "opencode") + "/"
9810
+ ];
9811
+ const resolvedPath = path.resolve(targetPath);
9812
+ const isAllowed = allowedDirs.some((dir) => {
9813
+ const rel = path.relative(dir, resolvedPath);
9814
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
9815
+ });
9816
+ if (!isAllowed) {
9817
+ return `Error: toFile path must be under ${os.tmpdir()} or ~/.cache/opencode/. Got: ${targetPath}`;
9910
9818
  }
9911
9819
  const msgIdSet = /* @__PURE__ */ new Set();
9912
9820
  for (const block of activeBlocks) {
@@ -9916,22 +9824,11 @@ function createDecompressTool(factoryCtx) {
9916
9824
  }
9917
9825
  const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
9918
9826
  const lines2 = blockMessages.map(extractMessageText2);
9919
- const fsp2 = await import("fs/promises");
9920
- const { constants } = await import("fs");
9827
+ const { writeFile: writeFile4 } = await import("fs/promises");
9921
9828
  const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : targets[0]?.blocks[0]?.summary ?? "(no content available)";
9922
- const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW);
9923
- try {
9924
- const handle = await fsp2.open(safe.filePath, flags, 384);
9925
- try {
9926
- await handle.write(Buffer.from(fileContent, "utf-8"));
9927
- } finally {
9928
- await handle.close();
9929
- }
9930
- } catch (err) {
9931
- return `Error: toFile write failed: ${err.message}`;
9932
- }
9829
+ await writeFile4(targetPath, fileContent, "utf-8");
9933
9830
  const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
9934
- return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${safe.filePath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
9831
+ return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
9935
9832
  }
9936
9833
  const activeMessagesBefore = snapshotActiveMessages(messagesState);
9937
9834
  const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
@@ -10006,9 +9903,7 @@ function isLiveBlock(block) {
10006
9903
  return block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep;
10007
9904
  }
10008
9905
  function rangeKey(startId, endId) {
10009
- const s = typeof startId === "string" ? migrateMessageRef(startId) : startId;
10010
- const e = typeof endId === "string" ? migrateMessageRef(endId) : endId;
10011
- return `${s}::${e}`;
9906
+ return `${startId}::${endId}`;
10012
9907
  }
10013
9908
  function rewriteCompressInput(part, liveKeys) {
10014
9909
  if (part.type !== "tool") return null;
@@ -10671,10 +10566,10 @@ function hideFailedCompressCalls(messages) {
10671
10566
 
10672
10567
  // lib/logger.ts
10673
10568
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10674
- import { join as join5 } from "path";
10569
+ import { join as join4 } from "path";
10675
10570
  import { existsSync as existsSync4 } from "fs";
10676
- import { homedir as homedir4 } from "os";
10677
- var LOG_VERSION = true ? "1.18.1-pr.416.178" : "dev";
10571
+ import { homedir as homedir3 } from "os";
10572
+ var LOG_VERSION = true ? "1.18.1-pr.432.180" : "dev";
10678
10573
  var LEVEL_RANK = {
10679
10574
  debug: 10,
10680
10575
  info: 20,
@@ -10694,8 +10589,8 @@ var Logger = class {
10694
10589
  constructor(enabled, level) {
10695
10590
  const resolved = level !== void 0 && LEVEL_RANK[level] !== void 0 ? level : void 0;
10696
10591
  this.level = resolved ?? (enabled ? "debug" : "warn");
10697
- const configHome = process.env.XDG_CONFIG_HOME || join5(homedir4(), ".config");
10698
- this.logDir = join5(configHome, "opencode", "logs", "acp");
10592
+ const configHome = process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config");
10593
+ this.logDir = join4(configHome, "opencode", "logs", "acp");
10699
10594
  }
10700
10595
  /** Back-compat view of the old flag: true only at full debug verbosity. */
10701
10596
  get enabled() {
@@ -10757,11 +10652,11 @@ var Logger = class {
10757
10652
  const dataStr = this.formatData(data);
10758
10653
  const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""} | v=${LOG_VERSION}
10759
10654
  `;
10760
- const dailyLogDir = join5(this.logDir, "daily");
10655
+ const dailyLogDir = join4(this.logDir, "daily");
10761
10656
  if (!existsSync4(dailyLogDir)) {
10762
10657
  await mkdir2(dailyLogDir, { recursive: true });
10763
10658
  }
10764
- const logFile = join5(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
10659
+ const logFile = join4(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
10765
10660
  await writeFile2(logFile, logLine, { flag: "a" });
10766
10661
  } catch (error) {
10767
10662
  }
@@ -10872,7 +10767,7 @@ var Logger = class {
10872
10767
  async saveContext(sessionId, messages) {
10873
10768
  if (this.level !== "debug") return;
10874
10769
  try {
10875
- const contextDir = join5(this.logDir, "context", sessionId);
10770
+ const contextDir = join4(this.logDir, "context", sessionId);
10876
10771
  if (!existsSync4(contextDir)) {
10877
10772
  await mkdir2(contextDir, { recursive: true });
10878
10773
  }
@@ -10880,9 +10775,9 @@ var Logger = class {
10880
10775
  (msg) => msg.parts && msg.parts.length > 0
10881
10776
  );
10882
10777
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10883
- const contextFile = join5(contextDir, `${timestamp}.json`);
10778
+ const contextFile = join4(contextDir, `${timestamp}.json`);
10884
10779
  await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
10885
- const versionFile = join5(contextDir, "_version");
10780
+ const versionFile = join4(contextDir, "_version");
10886
10781
  if (!existsSync4(versionFile)) {
10887
10782
  await writeFile2(versionFile, LOG_VERSION);
10888
10783
  }
@@ -10893,8 +10788,8 @@ var Logger = class {
10893
10788
 
10894
10789
  // lib/prompts/store.ts
10895
10790
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2 } from "fs";
10896
- import { join as join6, dirname as dirname3 } from "path";
10897
- import { homedir as homedir5 } from "os";
10791
+ import { join as join5, dirname as dirname2 } from "path";
10792
+ import { homedir as homedir4 } from "os";
10898
10793
 
10899
10794
  // lib/prompts/system.ts
10900
10795
  var ACP_STATUS_TOOL_LINE_RANGES = `- \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.`;
@@ -11201,7 +11096,7 @@ function createBundledRuntimePrompts(candidatesEnabled) {
11201
11096
  function findOpencodeDir2(startDir) {
11202
11097
  let current = startDir;
11203
11098
  while (current !== "/") {
11204
- const candidate = join6(current, ".opencode");
11099
+ const candidate = join5(current, ".opencode");
11205
11100
  if (existsSync5(candidate)) {
11206
11101
  try {
11207
11102
  if (statSync2(candidate).isDirectory()) {
@@ -11210,7 +11105,7 @@ function findOpencodeDir2(startDir) {
11210
11105
  } catch {
11211
11106
  }
11212
11107
  }
11213
- const parent = dirname3(current);
11108
+ const parent = dirname2(current);
11214
11109
  if (parent === current) {
11215
11110
  break;
11216
11111
  }
@@ -11219,13 +11114,13 @@ function findOpencodeDir2(startDir) {
11219
11114
  return null;
11220
11115
  }
11221
11116
  function resolvePromptPaths(workingDirectory) {
11222
- const configHome = process.env.XDG_CONFIG_HOME || join6(homedir5(), ".config");
11223
- const globalRoot = join6(configHome, "opencode", "acp-prompts");
11224
- const defaultsDir = join6(globalRoot, "defaults");
11225
- const globalOverridesDir = join6(globalRoot, "overrides");
11226
- const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join6(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
11117
+ const configHome = process.env.XDG_CONFIG_HOME || join5(homedir4(), ".config");
11118
+ const globalRoot = join5(configHome, "opencode", "acp-prompts");
11119
+ const defaultsDir = join5(globalRoot, "defaults");
11120
+ const globalOverridesDir = join5(globalRoot, "overrides");
11121
+ const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join5(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
11227
11122
  const opencodeDir = findOpencodeDir2(workingDirectory);
11228
- const projectOverridesDir = opencodeDir ? join6(opencodeDir, "acp-prompts", "overrides") : null;
11123
+ const projectOverridesDir = opencodeDir ? join5(opencodeDir, "acp-prompts", "overrides") : null;
11229
11124
  return {
11230
11125
  defaultsDir,
11231
11126
  globalOverridesDir,
@@ -11398,16 +11293,16 @@ var PromptStore = class {
11398
11293
  const candidates = [];
11399
11294
  if (this.paths.projectOverridesDir) {
11400
11295
  candidates.push({
11401
- path: join6(this.paths.projectOverridesDir, fileName)
11296
+ path: join5(this.paths.projectOverridesDir, fileName)
11402
11297
  });
11403
11298
  }
11404
11299
  if (this.paths.configDirOverridesDir) {
11405
11300
  candidates.push({
11406
- path: join6(this.paths.configDirOverridesDir, fileName)
11301
+ path: join5(this.paths.configDirOverridesDir, fileName)
11407
11302
  });
11408
11303
  }
11409
11304
  candidates.push({
11410
- path: join6(this.paths.globalOverridesDir, fileName)
11305
+ path: join5(this.paths.globalOverridesDir, fileName)
11411
11306
  });
11412
11307
  return candidates;
11413
11308
  }
@@ -11431,7 +11326,7 @@ var PromptStore = class {
11431
11326
  const managedContent = buildDefaultPromptFileContent(
11432
11327
  bundledEditable || bundledPrompts[definition.runtimeField]
11433
11328
  );
11434
- const filePath = join6(this.paths.defaultsDir, definition.fileName);
11329
+ const filePath = join5(this.paths.defaultsDir, definition.fileName);
11435
11330
  try {
11436
11331
  const existing = readFileIfExists(filePath);
11437
11332
  if (existing === managedContent) {
@@ -11445,7 +11340,7 @@ var PromptStore = class {
11445
11340
  });
11446
11341
  }
11447
11342
  }
11448
- const readmePath = join6(this.paths.defaultsDir, DEFAULTS_README_FILE);
11343
+ const readmePath = join5(this.paths.defaultsDir, DEFAULTS_README_FILE);
11449
11344
  const readmeContent = buildDefaultsReadmeContent();
11450
11345
  try {
11451
11346
  const existing = readFileIfExists(readmePath);
@@ -11910,7 +11805,7 @@ ${report}`;
11910
11805
  // lib/commands/export.ts
11911
11806
  import * as fs2 from "fs/promises";
11912
11807
  import { existsSync as existsSync6 } from "fs";
11913
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve2 } from "path";
11808
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join6, resolve } from "path";
11914
11809
  var ALL_TIERS = [1, 2, 3];
11915
11810
  var TIER_NAMES = {
11916
11811
  1: "Tier 1 \u2014 Capture",
@@ -12023,7 +11918,7 @@ function parseTiers(raw) {
12023
11918
  }
12024
11919
  function resolveDefaultOutputPath(sessionId, cwd2) {
12025
11920
  const short = (sessionId || "session").slice(0, 8);
12026
- return join7(cwd2, ".opencode", `acp-export-${short}.md`);
11921
+ return join6(cwd2, ".opencode", `acp-export-${short}.md`);
12027
11922
  }
12028
11923
  function formatTokens2(n) {
12029
11924
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -12151,9 +12046,9 @@ async function handleExportCommand(ctx, args) {
12151
12046
  return;
12152
12047
  }
12153
12048
  const cwd2 = ctx.workingDirectory || process.cwd();
12154
- const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve2(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
12049
+ const targetPath = options.outputPath !== "" ? isAbsolute2(options.outputPath) ? options.outputPath : resolve(cwd2, options.outputPath) : resolveDefaultOutputPath(sessionId, cwd2);
12155
12050
  try {
12156
- const dir = dirname4(targetPath);
12051
+ const dir = dirname3(targetPath);
12157
12052
  if (!existsSync6(dir)) {
12158
12053
  await fs2.mkdir(dir, { recursive: true });
12159
12054
  }
@@ -13014,9 +12909,29 @@ function createCommandExecuteHandler(client, registry4, logger, config, workingD
13014
12909
  }
13015
12910
  };
13016
12911
  }
13017
- function createTextCompleteHandler() {
13018
- return async (_input, output) => {
13019
- output.text = stripHallucinationsFromString(output.text);
12912
+ function createTextCompleteHandler(registry4, logger) {
12913
+ return async (input, output) => {
12914
+ if (typeof output.text !== "string" || output.text.length === 0) {
12915
+ return;
12916
+ }
12917
+ let text = stripHallucinationsFromString(output.text);
12918
+ const state = typeof input.sessionID === "string" ? registry4.get(input.sessionID) : void 0;
12919
+ if (state) {
12920
+ const stripped = stripLeakedTrailingRefs(text, {
12921
+ nextMessageRef: Number.isInteger(state.messageIds.nextRef) ? state.messageIds.nextRef : null,
12922
+ nextBlockRef: Number.isInteger(state.prune?.messages?.nextBlockId) ? state.prune.messages.nextBlockId : null
12923
+ });
12924
+ if (stripped !== text) {
12925
+ logger.warn("Stripped leaked trailing ACP ref from assistant output", {
12926
+ sessionID: input.sessionID,
12927
+ messageID: input.messageID,
12928
+ removedChars: text.length - stripped.length,
12929
+ removedTailPreview: text.slice(stripped.length, stripped.length + 200)
12930
+ });
12931
+ text = stripped;
12932
+ }
12933
+ }
12934
+ output.text = text;
13020
12935
  };
13021
12936
  }
13022
12937
  function createEventHandler(registry4, logger) {
@@ -13146,7 +13061,7 @@ function findBiliProxyProviders(provider) {
13146
13061
 
13147
13062
  // lib/update.ts
13148
13063
  import { readFile as readFile2, rm } from "fs/promises";
13149
- import { basename as basename2, dirname as dirname5, join as join8 } from "path";
13064
+ import { basename, dirname as dirname4, join as join7 } from "path";
13150
13065
  import { fileURLToPath } from "url";
13151
13066
  var PACKAGE_NAME = "opencode-acp";
13152
13067
  function startAutoUpdate(ctx, enabled, logger) {
@@ -13193,7 +13108,7 @@ async function checkAutoUpdate(signal, logger) {
13193
13108
  logger?.info("Auto-update skipped: package dir not found");
13194
13109
  return { updated: false };
13195
13110
  }
13196
- const pkg = await readPackageJson(join8(packageDir, "package.json"));
13111
+ const pkg = await readPackageJson(join7(packageDir, "package.json"));
13197
13112
  if (!pkg?.name || !pkg.version) {
13198
13113
  logger?.info("Auto-update skipped: package.json unreadable", { packageDir });
13199
13114
  return { updated: false };
@@ -13232,21 +13147,21 @@ async function checkAutoUpdate(signal, logger) {
13232
13147
  return { updated: true, name: pkg.name, current: pkg.version, latest };
13233
13148
  }
13234
13149
  async function findPackageDir(name) {
13235
- let dir = dirname5(fileURLToPath(import.meta.url));
13150
+ let dir = dirname4(fileURLToPath(import.meta.url));
13236
13151
  for (; ; ) {
13237
- const pkg = await readPackageJson(join8(dir, "package.json"));
13152
+ const pkg = await readPackageJson(join7(dir, "package.json"));
13238
13153
  if (pkg?.name === name) return dir;
13239
- const parent = dirname5(dir);
13154
+ const parent = dirname4(dir);
13240
13155
  if (parent === dir) return void 0;
13241
13156
  dir = parent;
13242
13157
  }
13243
13158
  }
13244
13159
  async function updateTarget(packageDir, name) {
13245
- const packageParent = dirname5(packageDir);
13246
- const nodeModulesDir = basename2(packageParent).startsWith("@") ? dirname5(packageParent) : packageParent;
13247
- if (basename2(nodeModulesDir) !== "node_modules") return void 0;
13248
- const wrapperDir = dirname5(nodeModulesDir);
13249
- const wrapperPkg = await readPackageJson(join8(wrapperDir, "package.json"));
13160
+ const packageParent = dirname4(packageDir);
13161
+ const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname4(packageParent) : packageParent;
13162
+ if (basename(nodeModulesDir) !== "node_modules") return void 0;
13163
+ const wrapperDir = dirname4(nodeModulesDir);
13164
+ const wrapperPkg = await readPackageJson(join7(wrapperDir, "package.json"));
13250
13165
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
13251
13166
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
13252
13167
  return { removeDir: wrapperDir, spec };
@@ -13254,13 +13169,13 @@ async function updateTarget(packageDir, name) {
13254
13169
  function wrapperSpec(wrapperDir, name) {
13255
13170
  if (name.startsWith("@")) {
13256
13171
  const [scope, pkg] = name.split("/");
13257
- if (!scope || !pkg || basename2(dirname5(wrapperDir)) !== scope) return void 0;
13172
+ if (!scope || !pkg || basename(dirname4(wrapperDir)) !== scope) return void 0;
13258
13173
  const prefix2 = `${pkg}@`;
13259
- const base2 = basename2(wrapperDir);
13174
+ const base2 = basename(wrapperDir);
13260
13175
  return base2.startsWith(prefix2) ? base2.slice(prefix2.length) : void 0;
13261
13176
  }
13262
13177
  const prefix = `${name}@`;
13263
- const base = basename2(wrapperDir);
13178
+ const base = basename(wrapperDir);
13264
13179
  return base.startsWith(prefix) ? base.slice(prefix.length) : void 0;
13265
13180
  }
13266
13181
  function isAutoUpdatableSpec(spec) {
@@ -13358,7 +13273,7 @@ var server = (async (ctx) => {
13358
13273
  }
13359
13274
  const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel);
13360
13275
  logger.info("ACP plugin initialized", {
13361
- version: true ? "1.18.1-pr.416.178" : "dev",
13276
+ version: true ? "1.18.1-pr.432.180" : "dev",
13362
13277
  workspace: ctx.directory,
13363
13278
  logLevel: logger.level,
13364
13279
  debug: config.debug,
@@ -13423,7 +13338,7 @@ var server = (async (ctx) => {
13423
13338
  hostPermissions
13424
13339
  )
13425
13340
  ),
13426
- "experimental.text.complete": guard(createTextCompleteHandler()),
13341
+ "experimental.text.complete": guard(createTextCompleteHandler(registry4, logger)),
13427
13342
  "command.execute.before": guard(
13428
13343
  createCommandExecuteHandler(
13429
13344
  ctx.client,