claude-code-cache-fix 3.0.5 → 3.1.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.
@@ -0,0 +1,89 @@
1
+ const CONTINUE_TRAILER_TEXT = "Continue from where you left off.";
2
+
3
+ const REMINDER_WRAP_REGEX = /^<system-reminder>\n([\s\S]*?)\n<\/system-reminder>\s*$/;
4
+ const BOOKKEEPING_PATTERNS = [
5
+ /^Token usage: \d+\/\d+; \d+ remaining\s*$/,
6
+ /^Output tokens — turn: [^\n]+ · session: [^\n]+\s*$/,
7
+ /^USD budget: \$[\d.]+\/\$[\d.]+; \$[\d.]+ remaining\s*$/,
8
+ /^The task tools haven't been used recently\./,
9
+ /^The TodoWrite tool hasn't been used recently\./,
10
+ /^Remaining conversation turns: /,
11
+ /^Messages? until auto-compact: /,
12
+ ];
13
+
14
+ function isContinueTrailerBlock(block) {
15
+ return (
16
+ !!block &&
17
+ typeof block === "object" &&
18
+ block.type === "text" &&
19
+ block.text === CONTINUE_TRAILER_TEXT
20
+ );
21
+ }
22
+
23
+ function isBookkeepingReminder(text) {
24
+ if (typeof text !== "string") return false;
25
+ const m = text.match(REMINDER_WRAP_REGEX);
26
+ if (!m) return false;
27
+ const inner = m[1];
28
+ for (const rx of BOOKKEEPING_PATTERNS) {
29
+ if (rx.test(inner)) return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ function stripContentBlocks(messages) {
35
+ if (!Array.isArray(messages)) return { messages, stats: null };
36
+
37
+ let trailerCount = 0;
38
+ let reminderCount = 0;
39
+
40
+ const result = messages.map((msg) => {
41
+ if (msg.role !== "user" || !Array.isArray(msg.content)) return msg;
42
+
43
+ let msgTrailers = 0;
44
+ let msgReminders = 0;
45
+
46
+ const kept = msg.content.filter((block) => {
47
+ if (isContinueTrailerBlock(block)) {
48
+ msgTrailers++;
49
+ return false;
50
+ }
51
+ if (block.type === "text" && isBookkeepingReminder(block.text)) {
52
+ msgReminders++;
53
+ return false;
54
+ }
55
+ return true;
56
+ });
57
+
58
+ if (kept.length === 0 || kept.length === msg.content.length) return msg;
59
+
60
+ trailerCount += msgTrailers;
61
+ reminderCount += msgReminders;
62
+ return { ...msg, content: kept };
63
+ });
64
+
65
+ const total = trailerCount + reminderCount;
66
+ return {
67
+ messages: total > 0 ? result : messages,
68
+ stats: total > 0 ? { trailerCount, reminderCount } : null,
69
+ };
70
+ }
71
+
72
+ export { isContinueTrailerBlock, isBookkeepingReminder, stripContentBlocks };
73
+
74
+ export default {
75
+ name: "content-strip",
76
+ description: "Strip continue trailers and bookkeeping system-reminders from user messages",
77
+ enabled: false,
78
+ order: 350,
79
+
80
+ async onRequest(ctx) {
81
+ if (!ctx.body.messages) return;
82
+
83
+ const { messages, stats } = stripContentBlocks(ctx.body.messages);
84
+ if (stats) {
85
+ ctx.body.messages = messages;
86
+ ctx.meta.contentStripStats = stats;
87
+ }
88
+ },
89
+ };
@@ -0,0 +1,361 @@
1
+ // deferred-tools-restore — preserve cache prefix across MCP reconnect race.
2
+ //
3
+ // PROBLEM (mirrored from preload.mjs ~429-518):
4
+ // On `claude --continue`, if MCP servers haven't finished reconnecting before
5
+ // CC fires the first post-resume request, the
6
+ // <system-reminder>The following deferred tools are now available via ToolSearch…</system-reminder>
7
+ // block at msg[0] (or wherever the attachment lands post-compaction) shrinks
8
+ // dramatically. ~40 tools collapse to a handful of CC built-ins, and CC
9
+ // injects a trailing
10
+ // "The following deferred tools are no longer available (their MCP server
11
+ // disconnected). Do not search for them — ToolSearch will return no match:"
12
+ // notice. That block change at the root of the message array busts the cache
13
+ // at the very top — the entire ~940K prompt re-caches.
14
+ //
15
+ // FIX: Persist the clean (no-UNAVAILABLE-marker) form of the block. On a
16
+ // subsequent request where the block has shrunk and contains the UNAVAILABLE
17
+ // marker, substitute the persisted full bytes. Restore only if the snapshot
18
+ // is STRICTLY LONGER than the current block — never downgrade to a stale
19
+ // shorter snapshot.
20
+ //
21
+ // SNAPSHOT KEY (proxy-specific adaptation):
22
+ // Preload uses process.cwd() because each preload runs in the CC process,
23
+ // where cwd identifies the project. The proxy is a long-lived daemon; its
24
+ // cwd is shared across all CC sessions on the host. To key per-project, the
25
+ // proxy parses the cwd OUT of the system prompt content (CC injects
26
+ // " - Primary working directory: <path>" in the # Environment section) and
27
+ // keys on sha1("cwd:" + that path). Verified empirically against CC v2.1.117.
28
+ //
29
+ // FAIL-OPEN: If the cwd marker is absent (CC format drift, missing system
30
+ // prompt), the extension no-ops the request entirely. Result: status quo
31
+ // cache-bust, never silently restores the wrong block.
32
+
33
+ import {
34
+ mkdir as _mkdir,
35
+ readFile as _readFile,
36
+ writeFile as _writeFile,
37
+ rename as _rename,
38
+ } from "node:fs/promises";
39
+ import { join } from "node:path";
40
+ import { homedir } from "node:os";
41
+ import { createHash } from "node:crypto";
42
+
43
+ const SKIP = process.env.CACHE_FIX_SKIP_DEFERRED_TOOLS_RESTORE === "1";
44
+ const DEBUG = process.env.CACHE_FIX_DEBUG === "1";
45
+
46
+ const AVAILABLE_MARKER =
47
+ "The following deferred tools are now available via ToolSearch";
48
+ const UNAVAILABLE_MARKER =
49
+ "The following deferred tools are no longer available";
50
+
51
+ const DEFAULT_FS = {
52
+ mkdir: _mkdir,
53
+ readFile: _readFile,
54
+ writeFile: _writeFile,
55
+ rename: _rename,
56
+ };
57
+
58
+ function getSnapshotDir() {
59
+ return join(homedir(), ".claude", "cache-fix-state");
60
+ }
61
+
62
+ function debug(msg) {
63
+ if (DEBUG) process.stderr.write(`[deferred-tools-restore] ${msg}\n`);
64
+ }
65
+
66
+ /**
67
+ * Extract the working-directory path from CC's system prompt.
68
+ * Returns the parsed path string, or null if the marker is not found OR
69
+ * the prompt structure is ambiguous (multiple valid env sections).
70
+ *
71
+ * STRICT STRUCTURAL PARSER (line-based, not regex-window):
72
+ *
73
+ * Recognizes a valid # Environment section ONLY when ALL of these hold:
74
+ * 1. A line that is exactly `# Environment` (whitespace-trimmed)
75
+ * 2. The next non-blank line is exactly the CC intro line:
76
+ * `You have been invoked in the following environment:`
77
+ * 3. The marker appears in a bullet line (`- Primary working directory: ...`)
78
+ * within the bullet list immediately following the intro line — bounded
79
+ * by the first blank line or first non-bullet line.
80
+ *
81
+ * Only the conjunction of all three rejects the false positives Codex flagged:
82
+ * - bare `# Environment` mention in narrative/code (fails rule 2)
83
+ * - code-fenced `Primary working directory:` example without the env header
84
+ * (fails rule 1)
85
+ * - a fake marker line elsewhere in the same block but not in a bullet
86
+ * list immediately following the intro (fails rule 3)
87
+ *
88
+ * AMBIGUITY GUARD: if MULTIPLE distinct valid env sections produce different
89
+ * cwd values (across blocks or within one block), refuse to pick — return
90
+ * null. The fail-open path (extension no-ops the request) is strictly safer
91
+ * than restoring with the wrong snapshot key.
92
+ *
93
+ * Accepts:
94
+ * - array of content blocks (CC's normal shape): walks .text fields in order
95
+ * - a single string (rare; older clients): scans directly
96
+ * - anything else: returns null
97
+ */
98
+ const ENV_HEADER_LINE = "# Environment";
99
+ const ENV_INTRO_LINE = "You have been invoked in the following environment:";
100
+ const CWD_BULLET_RE = /^-\s+Primary working directory:\s*(.+?)\s*$/;
101
+
102
+ function parseAllCwdsFromBlock(text) {
103
+ const found = [];
104
+ const lines = text.split("\n");
105
+ for (let i = 0; i < lines.length; i++) {
106
+ if (lines[i].trim() !== ENV_HEADER_LINE) continue;
107
+ // Skip blank lines after the header (CC emits exactly one intro line
108
+ // immediately following, but be tolerant of whitespace).
109
+ let j = i + 1;
110
+ while (j < lines.length && lines[j].trim() === "") j++;
111
+ if (j >= lines.length) continue;
112
+ if (lines[j].trim() !== ENV_INTRO_LINE) continue;
113
+ // Walk the bullet list following the intro line; first cwd bullet wins
114
+ // for this section. A blank line or non-bullet line ends the section.
115
+ for (let k = j + 1; k < lines.length; k++) {
116
+ const trimmed = lines[k].trimStart();
117
+ if (lines[k].trim() === "") break;
118
+ if (!trimmed.startsWith("-")) break;
119
+ const m = trimmed.match(CWD_BULLET_RE);
120
+ if (m && m[1]) {
121
+ found.push(m[1]);
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ return found;
127
+ }
128
+
129
+ function extractCwdFromSystem(system) {
130
+ if (!system) return null;
131
+ const texts = [];
132
+ if (typeof system === "string") {
133
+ texts.push(system);
134
+ } else if (Array.isArray(system)) {
135
+ for (const block of system) {
136
+ if (block && typeof block === "object" && typeof block.text === "string") {
137
+ texts.push(block.text);
138
+ }
139
+ }
140
+ } else {
141
+ return null;
142
+ }
143
+ const seen = new Set();
144
+ for (const t of texts) {
145
+ const matches = parseAllCwdsFromBlock(t);
146
+ for (const m of matches) {
147
+ seen.add(m);
148
+ if (seen.size > 1) return null; // ambiguous → no-op
149
+ }
150
+ }
151
+ if (seen.size === 1) return [...seen][0];
152
+ return null;
153
+ }
154
+
155
+ function deriveSnapshotKey(cwd) {
156
+ return createHash("sha1").update(`cwd:${cwd}`).digest("hex").slice(0, 16);
157
+ }
158
+
159
+ /**
160
+ * Locate the deferred-tools attachment block in body.messages.
161
+ * Only inspects user messages (skips assistant so the agent quoting the
162
+ * AVAILABLE marker verbatim doesn't trigger a false match).
163
+ * Returns { msgIdx, blockIdx, text } or null.
164
+ */
165
+ function findDeferredToolsBlockInBody(body) {
166
+ if (!body || !Array.isArray(body.messages)) return null;
167
+ for (let m = 0; m < body.messages.length; m++) {
168
+ const msg = body.messages[m];
169
+ if (!msg || msg.role !== "user" || !Array.isArray(msg.content)) continue;
170
+ for (let i = 0; i < msg.content.length; i++) {
171
+ const b = msg.content[i];
172
+ if (
173
+ b &&
174
+ b.type === "text" &&
175
+ typeof b.text === "string" &&
176
+ b.text.includes(AVAILABLE_MARKER)
177
+ ) {
178
+ return { msgIdx: m, blockIdx: i, text: b.text };
179
+ }
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+
185
+ // Atomic write (same lesson as prefix-diff): unique tmp per invocation so
186
+ // concurrent calls don't collide on a shared .tmp path.
187
+ async function atomicWriteText(finalPath, data, fs) {
188
+ const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.${Math.random()
189
+ .toString(36)
190
+ .slice(2, 10)}.tmp`;
191
+ await fs.writeFile(tmpPath, data);
192
+ await fs.rename(tmpPath, finalPath);
193
+ }
194
+
195
+ /**
196
+ * Persist a snapshot of the clean deferred-tools block.
197
+ *
198
+ * @param {string} text The full block text to persist.
199
+ * @param {object} options
200
+ * @param {string} options.dir Snapshot directory.
201
+ * @param {string} options.key Snapshot key (from deriveSnapshotKey).
202
+ * @param {object} [options.fs] fs/promises overrides for tests.
203
+ * @returns {Promise<{persisted: boolean, bytes: number, path: string}>}
204
+ */
205
+ async function persistDeferredTools(text, options) {
206
+ const dir = options.dir;
207
+ const key = options.key;
208
+ const fs = { ...DEFAULT_FS, ...(options.fs || {}) };
209
+ const path = join(dir, `deferred-tools-${key}.txt`);
210
+ try {
211
+ await fs.mkdir(dir, { recursive: true });
212
+ await atomicWriteText(path, text, fs);
213
+ return { persisted: true, bytes: Buffer.byteLength(text, "utf-8"), path };
214
+ } catch (err) {
215
+ debug(`persist failed at ${path}: ${err?.message ?? err}`);
216
+ return { persisted: false, bytes: 0, path };
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Read and validate a snapshot. Returns the snapshot text on success, null
222
+ * otherwise. Validation:
223
+ * - file exists and is readable
224
+ * - byte length >= AVAILABLE_MARKER length (sanity floor)
225
+ * - content contains the AVAILABLE marker (defense in depth against a
226
+ * truncated-but-readable file passing only the length check)
227
+ */
228
+ async function restoreDeferredTools(options) {
229
+ const dir = options.dir;
230
+ const key = options.key;
231
+ const fs = { ...DEFAULT_FS, ...(options.fs || {}) };
232
+ const path = join(dir, `deferred-tools-${key}.txt`);
233
+ let snapshot;
234
+ try {
235
+ snapshot = await fs.readFile(path, "utf-8");
236
+ } catch (err) {
237
+ if (err && err.code !== "ENOENT") {
238
+ debug(`snapshot read failed at ${path}: ${err?.message ?? err}`);
239
+ }
240
+ return null;
241
+ }
242
+ if (typeof snapshot !== "string") return null;
243
+ if (snapshot.length < AVAILABLE_MARKER.length) {
244
+ debug(`snapshot rejected (too short: ${snapshot.length} bytes) at ${path}`);
245
+ return null;
246
+ }
247
+ if (!snapshot.includes(AVAILABLE_MARKER)) {
248
+ debug(`snapshot rejected (missing AVAILABLE marker) at ${path}`);
249
+ return null;
250
+ }
251
+ // Defense in depth: persisted snapshots should be clean by construction
252
+ // (we only persist when !hasUnavail), but if a snapshot ever contains the
253
+ // UNAVAILABLE marker we refuse to restore it — restoring a "no longer
254
+ // available" block would be worse than no restore.
255
+ if (snapshot.includes(UNAVAILABLE_MARKER)) {
256
+ debug(`snapshot rejected (contains UNAVAILABLE marker, not a clean baseline) at ${path}`);
257
+ return null;
258
+ }
259
+ return snapshot;
260
+ }
261
+
262
+ export {
263
+ extractCwdFromSystem,
264
+ deriveSnapshotKey,
265
+ findDeferredToolsBlockInBody,
266
+ persistDeferredTools,
267
+ restoreDeferredTools,
268
+ AVAILABLE_MARKER,
269
+ UNAVAILABLE_MARKER,
270
+ };
271
+
272
+ export default {
273
+ name: "deferred-tools-restore",
274
+ description:
275
+ "Persist and restore the deferred-tools attachment block across sessions to prevent MCP-reconnect-race cache busts at resume time",
276
+ enabled: true,
277
+ order: 350,
278
+
279
+ async onRequest(ctx) {
280
+ if (SKIP) return;
281
+ if (!ctx || !ctx.body) return;
282
+ const body = ctx.body;
283
+
284
+ // 1. Parse cwd from system. No marker → no-op (honest degradation).
285
+ const cwd = extractCwdFromSystem(body.system);
286
+ if (!cwd) {
287
+ ctx.meta = ctx.meta || {};
288
+ ctx.meta.deferredToolsRestoreStats = { action: "skipped", reason: "no-cwd" };
289
+ return;
290
+ }
291
+ const key = deriveSnapshotKey(cwd);
292
+
293
+ // 2-4. Locate the deferred-tools block.
294
+ const found = findDeferredToolsBlockInBody(body);
295
+ if (!found) {
296
+ ctx.meta = ctx.meta || {};
297
+ ctx.meta.deferredToolsRestoreStats = { action: "skipped", reason: "no-block", key };
298
+ return;
299
+ }
300
+
301
+ const dir = getSnapshotDir();
302
+ const hasUnavail = found.text.includes(UNAVAILABLE_MARKER);
303
+
304
+ if (!hasUnavail) {
305
+ // 5. Clean baseline → persist.
306
+ const result = await persistDeferredTools(found.text, { dir, key });
307
+ ctx.meta = ctx.meta || {};
308
+ ctx.meta.deferredToolsRestoreStats = {
309
+ action: result.persisted ? "persisted" : "skipped",
310
+ bytes: result.bytes,
311
+ key,
312
+ };
313
+ if (result.persisted) {
314
+ process.stderr.write(
315
+ `[deferred-tools-restore] persisted ${result.bytes} bytes (key=${key})\n`,
316
+ );
317
+ }
318
+ return;
319
+ }
320
+
321
+ // 6. Block has UNAVAILABLE marker → attempt restore.
322
+ const snapshot = await restoreDeferredTools({ dir, key });
323
+ if (!snapshot) {
324
+ ctx.meta = ctx.meta || {};
325
+ ctx.meta.deferredToolsRestoreStats = { action: "skipped", reason: "no-snapshot", key };
326
+ return;
327
+ }
328
+
329
+ // Strictly-longer guard. Equal-length snapshots are not restored.
330
+ if (snapshot.length <= found.text.length) {
331
+ ctx.meta = ctx.meta || {};
332
+ ctx.meta.deferredToolsRestoreStats = {
333
+ action: "skipped",
334
+ reason: "snapshot-not-longer",
335
+ key,
336
+ snapshotBytes: snapshot.length,
337
+ currentBytes: found.text.length,
338
+ };
339
+ return;
340
+ }
341
+
342
+ // Substitute. Build new content array and new message; do not mutate
343
+ // the original arrays / objects.
344
+ const targetMsg = body.messages[found.msgIdx];
345
+ const newContent = targetMsg.content.slice();
346
+ newContent[found.blockIdx] = { ...newContent[found.blockIdx], text: snapshot };
347
+ body.messages[found.msgIdx] = { ...targetMsg, content: newContent };
348
+
349
+ ctx.meta = ctx.meta || {};
350
+ ctx.meta.deferredToolsRestoreStats = {
351
+ action: "restored",
352
+ bytes: snapshot.length,
353
+ previousBytes: found.text.length,
354
+ key,
355
+ };
356
+ process.stderr.write(
357
+ `[deferred-tools-restore] restored ${found.text.length}→${snapshot.length} bytes ` +
358
+ `at msg[${found.msgIdx}].content[${found.blockIdx}] (key=${key})\n`,
359
+ );
360
+ },
361
+ };
@@ -88,6 +88,8 @@ function stabilizeFingerprint(system, messages) {
88
88
  return { attrIdx, newText, oldFingerprint, stableFingerprint };
89
89
  }
90
90
 
91
+ export { computeFingerprint, extractRealUserMessageText, extractFirstMessageText, stabilizeFingerprint };
92
+
91
93
  export default {
92
94
  name: "fingerprint-strip",
93
95
  description: "Stabilize cc_version fingerprint in system prompt for cache prefix consistency",
@@ -86,6 +86,8 @@ function fixBlockText(blockType, text) {
86
86
  return pinBlockContent(blockType, fixed);
87
87
  }
88
88
 
89
+ export { isSystemReminder, isHooksBlock, isSkillsBlock, isDeferredToolsBlock, isMcpBlock, isRelocatableBlock, isClearArtifact, sortSkillsBlock, sortDeferredToolsBlock, stripSessionKnowledge, pinBlockContent, getBlockType, fixBlockText };
90
+
89
91
  export default {
90
92
  name: "fresh-session-sort",
91
93
  description: "Relocate scattered blocks to messages[0] in deterministic fresh-session order",
@@ -76,6 +76,8 @@ function isBookkeepingReminder(text) {
76
76
  return false;
77
77
  }
78
78
 
79
+ export { pinBlockContent, stripSessionKnowledge, normalizeSessionStartText, isContinueTrailerBlock, isBookkeepingReminder };
80
+
79
81
  export default {
80
82
  name: "identity-normalization",
81
83
  description: "Normalize volatile identity fields (SessionStart, Continue trailers, bookkeeping) for cache stability",
@@ -0,0 +1,83 @@
1
+ const KEEP_LAST = parseInt(process.env.CACHE_FIX_IMAGE_KEEP_LAST || "0", 10);
2
+ const PLACEHOLDER = "[image stripped from history — file may still be on disk]";
3
+
4
+ function stripOldToolResultImages(messages, keepLast) {
5
+ if (!keepLast || keepLast <= 0 || !Array.isArray(messages)) {
6
+ return { messages, stats: null };
7
+ }
8
+
9
+ const userMsgIndices = [];
10
+ for (let i = 0; i < messages.length; i++) {
11
+ if (messages[i].role === "user") userMsgIndices.push(i);
12
+ }
13
+
14
+ if (userMsgIndices.length <= keepLast) {
15
+ return { messages, stats: null };
16
+ }
17
+
18
+ const cutoffIdx = userMsgIndices[userMsgIndices.length - keepLast];
19
+
20
+ let strippedCount = 0;
21
+ let strippedBytes = 0;
22
+
23
+ const result = messages.map((msg, msgIdx) => {
24
+ if (msg.role !== "user" || msgIdx >= cutoffIdx || !Array.isArray(msg.content)) {
25
+ return msg;
26
+ }
27
+
28
+ let msgModified = false;
29
+ const newContent = msg.content.map((block) => {
30
+ if (block.type === "tool_result" && Array.isArray(block.content)) {
31
+ let toolModified = false;
32
+ const newToolContent = block.content.map((item) => {
33
+ if (item.type === "image") {
34
+ strippedCount++;
35
+ if (item.source?.data) {
36
+ strippedBytes += item.source.data.length;
37
+ }
38
+ toolModified = true;
39
+ return { type: "text", text: PLACEHOLDER };
40
+ }
41
+ return item;
42
+ });
43
+ if (toolModified) {
44
+ msgModified = true;
45
+ return { ...block, content: newToolContent };
46
+ }
47
+ }
48
+ return block;
49
+ });
50
+
51
+ return msgModified ? { ...msg, content: newContent } : msg;
52
+ });
53
+
54
+ const stats = strippedCount > 0
55
+ ? { strippedCount, strippedBytes, estimatedTokens: Math.ceil(strippedBytes * 0.125) }
56
+ : null;
57
+
58
+ return { messages: strippedCount > 0 ? result : messages, stats };
59
+ }
60
+
61
+ export { stripOldToolResultImages, PLACEHOLDER };
62
+
63
+ export default {
64
+ name: "image-strip",
65
+ description: "Strip base64 images from old tool results to reduce token waste",
66
+ enabled: false,
67
+ order: 150,
68
+
69
+ async onRequest(ctx) {
70
+ const keepLast = parseInt(ctx.meta.imageKeepLast ?? KEEP_LAST, 10);
71
+ if (!keepLast || keepLast <= 0) return;
72
+ if (!ctx.body.messages) return;
73
+
74
+ const { messages, stats } = stripOldToolResultImages(ctx.body.messages, keepLast);
75
+ if (stats) {
76
+ ctx.body.messages = messages;
77
+ ctx.meta.imageStripStats = stats;
78
+ process.stderr.write(
79
+ `[image-strip] stripped ${stats.strippedCount} images (~${stats.estimatedTokens} tokens saved)\n`
80
+ );
81
+ }
82
+ },
83
+ };
@@ -0,0 +1,64 @@
1
+ const SECTION_HEADER = "# Output efficiency";
2
+ const REPLACEMENT_RAW = process.env.CACHE_FIX_OUTPUT_EFFICIENCY_REPLACEMENT || "";
3
+
4
+ function normalizeReplacement(text) {
5
+ const trimmed = typeof text === "string" ? text.trim() : "";
6
+ if (!trimmed) return "";
7
+ return trimmed.startsWith(SECTION_HEADER) ? trimmed : `${SECTION_HEADER}\n\n${trimmed}`;
8
+ }
9
+
10
+ function replaceSection(text, replacement) {
11
+ const start = text.indexOf(SECTION_HEADER);
12
+ if (start === -1) return null;
13
+
14
+ const afterHeader = start + SECTION_HEADER.length;
15
+ const remainder = text.slice(afterHeader);
16
+ const nextHeadingMatch = remainder.match(/\n# [^\n]+/);
17
+
18
+ if (!nextHeadingMatch || nextHeadingMatch.index == null) {
19
+ return text.slice(0, start) + replacement;
20
+ }
21
+
22
+ const nextHeadingStart = afterHeader + nextHeadingMatch.index + 1;
23
+ return text.slice(0, start) + replacement + "\n\n" + text.slice(nextHeadingStart);
24
+ }
25
+
26
+ function rewriteOutputEfficiency(system, replacement) {
27
+ if (!Array.isArray(system) || !replacement) return null;
28
+
29
+ let changed = false;
30
+ const rewritten = system.map((block) => {
31
+ if (block?.type !== "text" || typeof block.text !== "string" || !block.text.includes(SECTION_HEADER)) {
32
+ return block;
33
+ }
34
+
35
+ const nextText = replaceSection(block.text, replacement);
36
+ if (!nextText || nextText === block.text) return block;
37
+
38
+ changed = true;
39
+ return { ...block, text: nextText };
40
+ });
41
+
42
+ return changed ? rewritten : null;
43
+ }
44
+
45
+ export { normalizeReplacement, replaceSection, rewriteOutputEfficiency, SECTION_HEADER };
46
+
47
+ export default {
48
+ name: "output-efficiency-rewrite",
49
+ description: "Replace Claude Code's # Output efficiency system prompt section with custom text",
50
+ enabled: false,
51
+ order: 90,
52
+
53
+ async onRequest(ctx) {
54
+ const raw = ctx.meta.outputEfficiencyReplacement ?? REPLACEMENT_RAW;
55
+ const replacement = normalizeReplacement(raw);
56
+ if (!replacement) return;
57
+ if (!ctx.body.system) return;
58
+
59
+ const result = rewriteOutputEfficiency(ctx.body.system, replacement);
60
+ if (result) {
61
+ ctx.body.system = result;
62
+ }
63
+ },
64
+ };