zidane 6.4.4 → 6.5.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.
Files changed (138) hide show
  1. package/README.md +1 -0
  2. package/dist/acp-cli.js +7 -6
  3. package/dist/acp-cli.js.map +1 -1
  4. package/dist/{acp-BLO58K3F.js → acp-o-LQAjFw.js} +4 -5
  5. package/dist/{acp-BLO58K3F.js.map → acp-o-LQAjFw.js.map} +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-8sUadp5M.js → agent-BVNkC1h4.js} +886 -113
  9. package/dist/agent-BVNkC1h4.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -2
  12. package/dist/{anthropic-D3lkw5mv.js → anthropic-Dov2mC5e.js} +3 -3
  13. package/dist/{anthropic-D3lkw5mv.js.map → anthropic-Dov2mC5e.js.map} +1 -1
  14. package/dist/{auth-BzYbbdN_.js → auth-B8ZuBcKC.js} +5 -5
  15. package/dist/{auth-BzYbbdN_.js.map → auth-B8ZuBcKC.js.map} +1 -1
  16. package/dist/chat/pure.d.ts +3 -3
  17. package/dist/chat/pure.js +2 -2
  18. package/dist/chat.d.ts +13 -44
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +8 -7
  21. package/dist/chat.js.map +1 -1
  22. package/dist/{edit-utils-EGosADZq.js → context-breakdown-BLF10tOO.js} +154 -2
  23. package/dist/context-breakdown-BLF10tOO.js.map +1 -0
  24. package/dist/{context-transport-D29WvHcm.js → context-transport-D-daWOmp.js} +3 -3
  25. package/dist/{context-transport-D29WvHcm.js.map → context-transport-D-daWOmp.js.map} +1 -1
  26. package/dist/contexts/daytona.d.ts +1 -1
  27. package/dist/contexts/e2b.d.ts +1 -1
  28. package/dist/eval.d.ts +1 -1
  29. package/dist/eval.js +1 -1
  30. package/dist/extensions.d.ts +2 -2
  31. package/dist/extensions.js +2 -2
  32. package/dist/headless.d.ts +1 -1
  33. package/dist/headless.js +3 -3
  34. package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
  35. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +13 -13
  38. package/dist/{login-CMVVnAHS.js → login-CR3fDHXh.js} +3 -3
  39. package/dist/{login-CMVVnAHS.js.map → login-CR3fDHXh.js.map} +1 -1
  40. package/dist/{mcp-3GXUBwur.js → mcp-C6IkXNOS.js} +87 -6
  41. package/dist/mcp-C6IkXNOS.js.map +1 -0
  42. package/dist/mcp.d.ts +2 -2
  43. package/dist/mcp.js +3 -2
  44. package/dist/{messages-C01kKGj0.js → messages-C_H9soi5.js} +2 -3
  45. package/dist/{messages-C01kKGj0.js.map → messages-C_H9soi5.js.map} +1 -1
  46. package/dist/{openai-compat-B3c_hzwf.js → openai-compat-CnHeo4Lz.js} +216 -3
  47. package/dist/openai-compat-CnHeo4Lz.js.map +1 -0
  48. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  49. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  50. package/dist/output/stream-json.d.ts +1 -1
  51. package/dist/output/terminal.d.ts +1 -1
  52. package/dist/plugins-2PMTVcp-.js +1174 -0
  53. package/dist/plugins-2PMTVcp-.js.map +1 -0
  54. package/dist/plugins.d.ts +2 -0
  55. package/dist/plugins.js +2 -0
  56. package/dist/presets.d.ts +1 -1
  57. package/dist/presets.js +1 -1
  58. package/dist/providers/anthropic.d.ts +1 -1
  59. package/dist/providers/anthropic.js +1 -1
  60. package/dist/providers/arcee.d.ts +1 -1
  61. package/dist/providers/arcee.js +1 -1
  62. package/dist/providers/baseten.d.ts +1 -1
  63. package/dist/providers/baseten.js +1 -1
  64. package/dist/providers/cerebras.d.ts +1 -1
  65. package/dist/providers/cerebras.js +1 -1
  66. package/dist/providers/local.d.ts +1 -1
  67. package/dist/providers/local.js +1 -1
  68. package/dist/providers/openai-compat.d.ts +1 -1
  69. package/dist/providers/openai-compat.js +1 -1
  70. package/dist/providers/openai.d.ts +1 -1
  71. package/dist/providers/openai.js +2 -2
  72. package/dist/providers/openrouter.d.ts +1 -1
  73. package/dist/providers/openrouter.js +1 -1
  74. package/dist/providers/xai.d.ts +1 -1
  75. package/dist/providers/xai.js +1 -1
  76. package/dist/{providers-z1vU0b2Q.js → providers-B6mFe7OD.js} +3 -3
  77. package/dist/{providers-z1vU0b2Q.js.map → providers-B6mFe7OD.js.map} +1 -1
  78. package/dist/providers.d.ts +1 -1
  79. package/dist/providers.js +4 -4
  80. package/dist/restate.d.ts +178 -2
  81. package/dist/restate.d.ts.map +1 -1
  82. package/dist/restate.js +463 -7
  83. package/dist/restate.js.map +1 -1
  84. package/dist/session/sqlite.d.ts +1 -1
  85. package/dist/{session-cr9VdoEO.js → session-VpLrNaFQ.js} +2 -2
  86. package/dist/{session-cr9VdoEO.js.map → session-VpLrNaFQ.js.map} +1 -1
  87. package/dist/session.d.ts +1 -1
  88. package/dist/session.js +2 -2
  89. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  90. package/dist/skills-Cp9vnBwd.js.map +1 -0
  91. package/dist/skills.d.ts +1 -1
  92. package/dist/skills.js +1 -2
  93. package/dist/{stdio-BvL8HGYW.js → stdio-DzUUkkhS.js} +2 -2
  94. package/dist/{stdio-BvL8HGYW.js.map → stdio-DzUUkkhS.js.map} +1 -1
  95. package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BILyUGmi.js} +2 -2
  96. package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BILyUGmi.js.map} +1 -1
  97. package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  98. package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  99. package/dist/tools/fetch-url.d.ts +1 -1
  100. package/dist/tools/web-search.d.ts +1 -1
  101. package/dist/tools.d.ts +1 -1
  102. package/dist/tools.js +1 -2
  103. package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-DEIr0tJC.js} +44 -7
  104. package/dist/transcript-anchors-DEIr0tJC.js.map +1 -0
  105. package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  106. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  107. package/dist/tui.d.ts +38 -4
  108. package/dist/tui.d.ts.map +1 -1
  109. package/dist/tui.js +843 -109
  110. package/dist/tui.js.map +1 -1
  111. package/dist/{turn-operations-CAvi8NYt.js → turn-operations-C3IijXMW.js} +2 -2
  112. package/dist/{turn-operations-CAvi8NYt.js.map → turn-operations-C3IijXMW.js.map} +1 -1
  113. package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  114. package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  115. package/dist/types-BIarq1qE.js.map +1 -1
  116. package/dist/types.d.ts +1 -1
  117. package/dist/{xai-BPwMQSuF.js → xai-Du6Wfnlb.js} +2 -2
  118. package/dist/{xai-BPwMQSuF.js.map → xai-Du6Wfnlb.js.map} +1 -1
  119. package/docs/PLUGINS.md +340 -0
  120. package/docs/RESTATE.md +168 -3
  121. package/docs/SKILL.md +24 -8
  122. package/package.json +6 -1
  123. package/dist/agent-8sUadp5M.js.map +0 -1
  124. package/dist/aliasing-lVFbm81n.js +0 -217
  125. package/dist/aliasing-lVFbm81n.js.map +0 -1
  126. package/dist/context-breakdown-Dzo25N45.js +0 -155
  127. package/dist/context-breakdown-Dzo25N45.js.map +0 -1
  128. package/dist/edit-utils-EGosADZq.js.map +0 -1
  129. package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
  130. package/dist/mcp-3GXUBwur.js.map +0 -1
  131. package/dist/openai-compat-B3c_hzwf.js.map +0 -1
  132. package/dist/resolve-CZJxds85.js.map +0 -1
  133. package/dist/skills-Dd6XCi4s.js +0 -420
  134. package/dist/skills-Dd6XCi4s.js.map +0 -1
  135. package/dist/tool-state-DTs-3ZHH.js +0 -653
  136. package/dist/tool-state-DTs-3ZHH.js.map +0 -1
  137. package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
  138. package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
@@ -1,217 +0,0 @@
1
- //#region src/hash.ts
2
- /**
3
- * FNV-1a (32-bit) rendered in base36 — a tiny, dependency-free, deterministic
4
- * hash.
5
- *
6
- * Used to disambiguate sanitized identifiers: distinct inputs that would
7
- * otherwise collapse to the same sanitized string (e.g. `a.b` and `a/b` both
8
- * → `a_b`, or two over-long names truncated to the same prefix) keep a stable,
9
- * unique suffix. Determinism matters because the suffix must be identical
10
- * across runs and across a `tool_use` / `tool_result` pair derived from the
11
- * same original value.
12
- */
13
- function fnv1aBase36(value) {
14
- let hash = 2166136261;
15
- for (let i = 0; i < value.length; i++) {
16
- hash ^= value.charCodeAt(i);
17
- hash = Math.imul(hash, 16777619);
18
- }
19
- return (hash >>> 0).toString(36);
20
- }
21
- //#endregion
22
- //#region src/aliasing.ts
23
- /**
24
- * Build alias lookup maps from a `toolAliases` record.
25
- *
26
- * Validates:
27
- * - No two canonical names map to the same alias (collision).
28
- * - No alias collides with another canonical tool name (would shadow).
29
- *
30
- * Silently ignores alias entries whose canonical name isn't in `canonicalNames` —
31
- * preset/agent authors can declare aliases for tools that may be added later via MCP.
32
- *
33
- * @param aliases - The `toolAliases` map from the agent options.
34
- * @param canonicalNames - All tool canonical names currently in scope (agent + MCP).
35
- */
36
- function buildAliasMaps(aliases, canonicalNames) {
37
- const aliasByCanonical = /* @__PURE__ */ new Map();
38
- const canonicalByAlias = /* @__PURE__ */ new Map();
39
- if (!aliases) return {
40
- aliasByCanonical,
41
- canonicalByAlias
42
- };
43
- const canonicalSet = new Set(canonicalNames);
44
- function isRemappedAway(canonical) {
45
- const mapped = aliases[canonical];
46
- return typeof mapped === "string" && mapped.length > 0 && mapped !== canonical;
47
- }
48
- for (const [canonical, alias] of Object.entries(aliases)) {
49
- if (typeof alias !== "string" || alias.length === 0) throw new Error(`Tool alias for "${canonical}" must be a non-empty string`);
50
- if (alias === canonical) continue;
51
- if (!canonicalSet.has(canonical)) continue;
52
- if (canonicalSet.has(alias) && !isRemappedAway(alias)) throw new Error(`Tool alias "${canonical}" -> "${alias}" collides with an existing canonical tool name`);
53
- const existingCanonical = canonicalByAlias.get(alias);
54
- if (existingCanonical && existingCanonical !== canonical) throw new Error(`Tool alias collision: both "${existingCanonical}" and "${canonical}" map to alias "${alias}"`);
55
- aliasByCanonical.set(canonical, alias);
56
- canonicalByAlias.set(alias, canonical);
57
- }
58
- return {
59
- aliasByCanonical,
60
- canonicalByAlias
61
- };
62
- }
63
- /** Return the alias for a canonical name, falling back to the canonical name itself. */
64
- function toWireName(canonical, maps) {
65
- return maps.aliasByCanonical.get(canonical) ?? canonical;
66
- }
67
- /** Return the canonical name for a wire name, falling back to the wire name itself. */
68
- function toCanonicalName(wire, maps) {
69
- return maps.canonicalByAlias.get(wire) ?? wire;
70
- }
71
- /**
72
- * Tool names must match `^[a-zA-Z0-9_-]{1,N}$`. The charset is universal; the
73
- * length cap is taken as the STRICTEST across providers (OpenAI/-compat: 64;
74
- * Anthropic allows 128) so a sanitized name is valid on EVERY provider — and so
75
- * a mid-session model switch can never make the provider-level drop guard
76
- * (`sanitizeToolSpecs`) discard a tool we could have kept.
77
- *
78
- * MCP tool names are minted verbatim as `mcp_<server>_<tool>` (see
79
- * `connectMcpServers`), so a server configured as `SSH Ipseity` yields
80
- * `mcp_SSH Ipseity_exec` — the space (and `.`/`:`/`/`, unicode, or a >64-char
81
- * join) 400s the request the instant that tool lands in the `tools` array:
82
- * `tools.N.custom.name: String should match pattern ...`.
83
- */
84
- const WIRE_NAME_MAX = 64;
85
- const WIRE_NAME_OK = /^[\w-]{1,64}$/;
86
- const WIRE_NAME_BAD_CHAR = /[^\w-]/g;
87
- /**
88
- * Build OUTBOUND wire aliases that coerce any tool name violating the provider
89
- * name constraint into a valid one. Only offenders get an entry; clean setups
90
- * return `undefined` (no alias map churn, prompt cache untouched).
91
- *
92
- * Returned as a `Record` so it merges into `effectiveToolAliases` upstream of
93
- * BOTH wire-name computations — the `tool_search` catalog and the `tools` array
94
- * — keeping them in lockstep. Canonical names (dispatch, hooks, persisted
95
- * turns) never change; only the bytes the model sees do.
96
- *
97
- * Scoped to MCP names (the only third-party-controlled, thus only realistically
98
- * invalid, source). The hash suffix keeps distinct originals distinct even when
99
- * they collapse to the same replacement; `taken` guards the residual collision.
100
- */
101
- function buildSanitizedWireAliases(names, existing) {
102
- const wireOf = (name) => {
103
- const alias = existing?.[name];
104
- return typeof alias === "string" && alias.length > 0 ? alias : name;
105
- };
106
- const taken = /* @__PURE__ */ new Set();
107
- const offenders = [];
108
- for (const name of names) {
109
- const wire = wireOf(name);
110
- if (WIRE_NAME_OK.test(wire)) taken.add(wire);
111
- else offenders.push(name);
112
- }
113
- if (offenders.length === 0) return void 0;
114
- const out = {};
115
- for (const name of offenders) {
116
- const sanitized = sanitizeWireName(wireOf(name), taken);
117
- out[name] = sanitized;
118
- taken.add(sanitized);
119
- }
120
- return out;
121
- }
122
- function sanitizeWireName(name, taken) {
123
- let base = name.replace(WIRE_NAME_BAD_CHAR, "_");
124
- if (base.length === 0) base = "tool";
125
- const hash = fnv1aBase36(name);
126
- const withSuffix = (suffix) => `${base.slice(0, Math.max(1, WIRE_NAME_MAX - suffix.length))}${suffix}`;
127
- let candidate = withSuffix(`_${hash}`);
128
- for (let n = 1; taken.has(candidate); n++) candidate = withSuffix(`_${hash}_${n}`);
129
- return candidate;
130
- }
131
- /**
132
- * Augment {@link AliasMaps} with INBOUND-ONLY aliases for the Claude Code
133
- * `mcp__server__tool` double-underscore naming convention. For every MCP
134
- * tool registered with the canonical `mcp_<server>_<tool>` form, add a
135
- * `canonicalByAlias` entry mapping the double-underscore variant to the
136
- * same canonical name. Outbound (`aliasByCanonical`) is left untouched —
137
- * the model still sees the single-underscore canonical on the wire, so
138
- * the tools-array byte count (and the provider prompt cache) is
139
- * unaffected.
140
- *
141
- * Why: SDK consumers and tool descriptions across the ecosystem
142
- * inconsistently reference one form or the other (Anthropic's docs use
143
- * double, zidane uses single). A Claude-Code-trained model emitting
144
- * `mcp__acme__apply_migration` against a server zidane registered as
145
- * `mcp_acme_apply_migration` would otherwise trip the `tool:unknown`
146
- * path and waste a turn on the correction.
147
- *
148
- * Idempotent: a canonical that already has a double-underscore alias
149
- * mapped (host's explicit `toolAliases` got there first) is left alone.
150
- * Tools whose canonical name DOESN'T start with `mcp_` (or that contain
151
- * no inner separator) are skipped — we don't want to invent aliases for
152
- * non-MCP tools or malformed names.
153
- *
154
- * Mutates `maps.canonicalByAlias` in place and returns the same object
155
- * for chaining.
156
- */
157
- function augmentMcpDoubleUnderscoreAliases(maps, canonicalNames) {
158
- for (const canonical of canonicalNames) {
159
- if (!canonical.startsWith("mcp_")) continue;
160
- const tail = canonical.slice(4);
161
- const sep = tail.indexOf("_");
162
- if (sep <= 0 || sep >= tail.length - 1) continue;
163
- const doubleForm = `mcp__${tail.slice(0, sep)}__${tail.slice(sep + 1)}`;
164
- if (doubleForm === canonical) continue;
165
- if (maps.canonicalByAlias.has(doubleForm)) continue;
166
- maps.canonicalByAlias.set(doubleForm, canonical);
167
- }
168
- return maps;
169
- }
170
- /**
171
- * Rewrite `tool_call` block names in a content array from canonical → wire for outbound
172
- * messages sent to the provider. Mutation is non-destructive (returns a new array).
173
- */
174
- function rewriteContentToWire(content, maps) {
175
- if (maps.aliasByCanonical.size === 0) return content;
176
- return content.map((block) => {
177
- if (block.type !== "tool_call") return block;
178
- const wire = maps.aliasByCanonical.get(block.name);
179
- if (!wire || wire === block.name) return block;
180
- return {
181
- ...block,
182
- name: wire
183
- };
184
- });
185
- }
186
- /**
187
- * Rewrite `tool_call` block names in a content array from wire → canonical for inbound
188
- * messages received from the provider. Non-destructive.
189
- */
190
- function rewriteContentToCanonical(content, maps) {
191
- if (maps.canonicalByAlias.size === 0) return content;
192
- return content.map((block) => {
193
- if (block.type !== "tool_call") return block;
194
- const canonical = maps.canonicalByAlias.get(block.name);
195
- if (!canonical || canonical === block.name) return block;
196
- return {
197
- ...block,
198
- name: canonical
199
- };
200
- });
201
- }
202
- /**
203
- * Rewrite every `SessionMessage.content` in an array from canonical → wire.
204
- * Returns a new array of new message objects — input messages are not mutated.
205
- * When the alias map is empty, returns the input by reference (no allocation).
206
- */
207
- function rewriteMessagesToWire(messages, maps) {
208
- if (maps.aliasByCanonical.size === 0) return messages;
209
- return messages.map((msg) => ({
210
- ...msg,
211
- content: rewriteContentToWire(msg.content, maps)
212
- }));
213
- }
214
- //#endregion
215
- export { rewriteMessagesToWire as a, fnv1aBase36 as c, rewriteContentToCanonical as i, buildAliasMaps as n, toCanonicalName as o, buildSanitizedWireAliases as r, toWireName as s, augmentMcpDoubleUnderscoreAliases as t };
216
-
217
- //# sourceMappingURL=aliasing-lVFbm81n.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"aliasing-lVFbm81n.js","names":[],"sources":["../src/hash.ts","../src/aliasing.ts"],"sourcesContent":["/**\n * FNV-1a (32-bit) rendered in base36 — a tiny, dependency-free, deterministic\n * hash.\n *\n * Used to disambiguate sanitized identifiers: distinct inputs that would\n * otherwise collapse to the same sanitized string (e.g. `a.b` and `a/b` both\n * → `a_b`, or two over-long names truncated to the same prefix) keep a stable,\n * unique suffix. Determinism matters because the suffix must be identical\n * across runs and across a `tool_use` / `tool_result` pair derived from the\n * same original value.\n */\nexport function fnv1aBase36(value: string): string {\n let hash = 0x811C9DC5\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return (hash >>> 0).toString(36)\n}\n","/**\n * Tool-name aliasing helpers.\n *\n * Aliasing is applied at the LLM boundary only: the provider sees the aliased\n * name, but hooks and persisted turns use canonical names. See `AgentOptions.toolAliases`.\n */\n\nimport type { SessionContentBlock, SessionMessage } from './types'\nimport { fnv1aBase36 } from './hash'\n\n/** Forward + reverse lookup between canonical and aliased tool names */\nexport interface AliasMaps {\n /** canonical → alias (only entries where alias !== canonical) */\n aliasByCanonical: Map<string, string>\n /** alias → canonical (only entries where alias !== canonical) */\n canonicalByAlias: Map<string, string>\n}\n\n/**\n * Build alias lookup maps from a `toolAliases` record.\n *\n * Validates:\n * - No two canonical names map to the same alias (collision).\n * - No alias collides with another canonical tool name (would shadow).\n *\n * Silently ignores alias entries whose canonical name isn't in `canonicalNames` —\n * preset/agent authors can declare aliases for tools that may be added later via MCP.\n *\n * @param aliases - The `toolAliases` map from the agent options.\n * @param canonicalNames - All tool canonical names currently in scope (agent + MCP).\n */\nexport function buildAliasMaps(\n aliases: Record<string, string> | undefined,\n canonicalNames: Iterable<string>,\n): AliasMaps {\n const aliasByCanonical = new Map<string, string>()\n const canonicalByAlias = new Map<string, string>()\n\n if (!aliases) {\n return { aliasByCanonical, canonicalByAlias }\n }\n\n const canonicalSet = new Set(canonicalNames)\n\n // A canonical name is \"genuinely remapped\" when it has an alias entry that is\n // both non-empty and different from itself. Identity aliases or missing keys\n // mean the tool still answers to its canonical name on the wire.\n function isRemappedAway(canonical: string): boolean {\n const mapped = aliases![canonical]\n return typeof mapped === 'string' && mapped.length > 0 && mapped !== canonical\n }\n\n for (const [canonical, alias] of Object.entries(aliases)) {\n if (typeof alias !== 'string' || alias.length === 0)\n throw new Error(`Tool alias for \"${canonical}\" must be a non-empty string`)\n\n if (alias === canonical)\n continue\n\n if (!canonicalSet.has(canonical))\n continue\n\n // When the alias matches an existing canonical tool name, only allow it if\n // that other canonical is itself being remapped away (i.e. a name swap).\n // Otherwise the wire name would shadow a real tool.\n if (canonicalSet.has(alias) && !isRemappedAway(alias)) {\n throw new Error(`Tool alias \"${canonical}\" -> \"${alias}\" collides with an existing canonical tool name`)\n }\n\n const existingCanonical = canonicalByAlias.get(alias)\n if (existingCanonical && existingCanonical !== canonical) {\n throw new Error(\n `Tool alias collision: both \"${existingCanonical}\" and \"${canonical}\" map to alias \"${alias}\"`,\n )\n }\n\n aliasByCanonical.set(canonical, alias)\n canonicalByAlias.set(alias, canonical)\n }\n\n return { aliasByCanonical, canonicalByAlias }\n}\n\n/** Return the alias for a canonical name, falling back to the canonical name itself. */\nexport function toWireName(canonical: string, maps: AliasMaps): string {\n return maps.aliasByCanonical.get(canonical) ?? canonical\n}\n\n/** Return the canonical name for a wire name, falling back to the wire name itself. */\nexport function toCanonicalName(wire: string, maps: AliasMaps): string {\n return maps.canonicalByAlias.get(wire) ?? wire\n}\n\n/**\n * Tool names must match `^[a-zA-Z0-9_-]{1,N}$`. The charset is universal; the\n * length cap is taken as the STRICTEST across providers (OpenAI/-compat: 64;\n * Anthropic allows 128) so a sanitized name is valid on EVERY provider — and so\n * a mid-session model switch can never make the provider-level drop guard\n * (`sanitizeToolSpecs`) discard a tool we could have kept.\n *\n * MCP tool names are minted verbatim as `mcp_<server>_<tool>` (see\n * `connectMcpServers`), so a server configured as `SSH Ipseity` yields\n * `mcp_SSH Ipseity_exec` — the space (and `.`/`:`/`/`, unicode, or a >64-char\n * join) 400s the request the instant that tool lands in the `tools` array:\n * `tools.N.custom.name: String should match pattern ...`.\n */\nconst WIRE_NAME_MAX = 64\nconst WIRE_NAME_OK = /^[\\w-]{1,64}$/\nconst WIRE_NAME_BAD_CHAR = /[^\\w-]/g\n\n/**\n * Build OUTBOUND wire aliases that coerce any tool name violating the provider\n * name constraint into a valid one. Only offenders get an entry; clean setups\n * return `undefined` (no alias map churn, prompt cache untouched).\n *\n * Returned as a `Record` so it merges into `effectiveToolAliases` upstream of\n * BOTH wire-name computations — the `tool_search` catalog and the `tools` array\n * — keeping them in lockstep. Canonical names (dispatch, hooks, persisted\n * turns) never change; only the bytes the model sees do.\n *\n * Scoped to MCP names (the only third-party-controlled, thus only realistically\n * invalid, source). The hash suffix keeps distinct originals distinct even when\n * they collapse to the same replacement; `taken` guards the residual collision.\n */\nexport function buildSanitizedWireAliases(\n names: Iterable<string>,\n existing: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const wireOf = (name: string): string => {\n const alias = existing?.[name]\n return typeof alias === 'string' && alias.length > 0 ? alias : name\n }\n\n // Already-valid wire names stay occupied so a freshly minted one can never\n // shadow another tool.\n const taken = new Set<string>()\n const offenders: string[] = []\n for (const name of names) {\n const wire = wireOf(name)\n if (WIRE_NAME_OK.test(wire))\n taken.add(wire)\n else\n offenders.push(name)\n }\n if (offenders.length === 0)\n return undefined\n\n const out: Record<string, string> = {}\n for (const name of offenders) {\n const sanitized = sanitizeWireName(wireOf(name), taken)\n out[name] = sanitized\n taken.add(sanitized)\n }\n return out\n}\n\nfunction sanitizeWireName(name: string, taken: ReadonlySet<string>): string {\n let base = name.replace(WIRE_NAME_BAD_CHAR, '_')\n if (base.length === 0)\n base = 'tool'\n // Hash the ORIGINAL so `a.b` vs `a/b` (both → `a_b`) and truncated long\n // names stay distinct and deterministic across runs.\n const hash = fnv1aBase36(name)\n const withSuffix = (suffix: string): string =>\n `${base.slice(0, Math.max(1, WIRE_NAME_MAX - suffix.length))}${suffix}`\n let candidate = withSuffix(`_${hash}`)\n for (let n = 1; taken.has(candidate); n++)\n candidate = withSuffix(`_${hash}_${n}`)\n return candidate\n}\n\n/**\n * Augment {@link AliasMaps} with INBOUND-ONLY aliases for the Claude Code\n * `mcp__server__tool` double-underscore naming convention. For every MCP\n * tool registered with the canonical `mcp_<server>_<tool>` form, add a\n * `canonicalByAlias` entry mapping the double-underscore variant to the\n * same canonical name. Outbound (`aliasByCanonical`) is left untouched —\n * the model still sees the single-underscore canonical on the wire, so\n * the tools-array byte count (and the provider prompt cache) is\n * unaffected.\n *\n * Why: SDK consumers and tool descriptions across the ecosystem\n * inconsistently reference one form or the other (Anthropic's docs use\n * double, zidane uses single). A Claude-Code-trained model emitting\n * `mcp__acme__apply_migration` against a server zidane registered as\n * `mcp_acme_apply_migration` would otherwise trip the `tool:unknown`\n * path and waste a turn on the correction.\n *\n * Idempotent: a canonical that already has a double-underscore alias\n * mapped (host's explicit `toolAliases` got there first) is left alone.\n * Tools whose canonical name DOESN'T start with `mcp_` (or that contain\n * no inner separator) are skipped — we don't want to invent aliases for\n * non-MCP tools or malformed names.\n *\n * Mutates `maps.canonicalByAlias` in place and returns the same object\n * for chaining.\n */\nexport function augmentMcpDoubleUnderscoreAliases(\n maps: AliasMaps,\n canonicalNames: Iterable<string>,\n): AliasMaps {\n for (const canonical of canonicalNames) {\n if (!canonical.startsWith('mcp_'))\n continue\n // Strip leading `mcp_`, then require AT LEAST one more `_` so the\n // remainder is `<server>_<tool>` (a degenerate `mcp_foo` with no\n // tool segment has nothing meaningful to double-underscore).\n const tail = canonical.slice(4)\n const sep = tail.indexOf('_')\n if (sep <= 0 || sep >= tail.length - 1)\n continue\n const server = tail.slice(0, sep)\n const tool = tail.slice(sep + 1)\n const doubleForm = `mcp__${server}__${tool}`\n if (doubleForm === canonical)\n continue // shouldn't happen but defensive\n if (maps.canonicalByAlias.has(doubleForm))\n continue // host alias or earlier pass got there first\n maps.canonicalByAlias.set(doubleForm, canonical)\n }\n return maps\n}\n\n/**\n * Rewrite `tool_call` block names in a content array from canonical → wire for outbound\n * messages sent to the provider. Mutation is non-destructive (returns a new array).\n */\nexport function rewriteContentToWire(\n content: SessionContentBlock[],\n maps: AliasMaps,\n): SessionContentBlock[] {\n if (maps.aliasByCanonical.size === 0)\n return content\n return content.map((block) => {\n if (block.type !== 'tool_call')\n return block\n const wire = maps.aliasByCanonical.get(block.name)\n if (!wire || wire === block.name)\n return block\n return { ...block, name: wire }\n })\n}\n\n/**\n * Rewrite `tool_call` block names in a content array from wire → canonical for inbound\n * messages received from the provider. Non-destructive.\n */\nexport function rewriteContentToCanonical(\n content: SessionContentBlock[],\n maps: AliasMaps,\n): SessionContentBlock[] {\n if (maps.canonicalByAlias.size === 0)\n return content\n return content.map((block) => {\n if (block.type !== 'tool_call')\n return block\n const canonical = maps.canonicalByAlias.get(block.name)\n if (!canonical || canonical === block.name)\n return block\n return { ...block, name: canonical }\n })\n}\n\n/**\n * Rewrite every `SessionMessage.content` in an array from canonical → wire.\n * Returns a new array of new message objects — input messages are not mutated.\n * When the alias map is empty, returns the input by reference (no allocation).\n */\nexport function rewriteMessagesToWire(\n messages: SessionMessage[],\n maps: AliasMaps,\n): SessionMessage[] {\n if (maps.aliasByCanonical.size === 0)\n return messages\n return messages.map(msg => ({ ...msg, content: rewriteContentToWire(msg.content, maps) }))\n}\n"],"mappings":"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"}
@@ -1,155 +0,0 @@
1
- import { n as estimateTokens } from "./utils-ngQzYzZD.js";
2
- //#region src/chat/context-breakdown.ts
3
- const LABELS = {
4
- systemPrompt: "System prompt",
5
- rules: "Rules (AGENTS.md)",
6
- tools: "Tool definitions",
7
- mcpTools: "MCP tools",
8
- mcpInstructions: "MCP instructions",
9
- skills: "Skills",
10
- subagentDefs: "Subagent definitions",
11
- conversation: "Conversation",
12
- mcpToolsDeferred: "MCP tools (deferred)",
13
- toolsDeferred: "System tools (deferred)",
14
- autocompactBuffer: "Autocompact buffer",
15
- freeSpace: "Free space"
16
- };
17
- /** Sum the heuristic token sizes of a list of strings. */
18
- function sumEstimate(parts) {
19
- let total = 0;
20
- for (const p of parts) total += estimateTokens(p);
21
- return total;
22
- }
23
- function mcpGroupTokens(group) {
24
- const items = group.tools.map((t) => ({
25
- id: `${group.server}:${t.name}`,
26
- label: t.name,
27
- tokens: estimateTokens(t.json),
28
- estimated: true
29
- }));
30
- let total = 0;
31
- for (const it of items) total += it.tokens;
32
- return {
33
- total,
34
- items
35
- };
36
- }
37
- /**
38
- * Shape a {@link ContextSnapshot} into a render-ready {@link ContextBreakdown}.
39
- *
40
- * Live categories (system prompt, tools, MCP tools/instructions, skills,
41
- * memory, subagent defs, conversation) sum to `snapshot.used` — `conversation`
42
- * absorbs the remainder so the bar matches the provider-reported total exactly.
43
- * Deferred + autocompact + free-space buckets follow.
44
- *
45
- * When `snapshot.exact` carries provider counts, the matching categories use
46
- * them (system, tools) and drop their `estimated` flag; everything else stays
47
- * on the heuristic.
48
- */
49
- function buildContextBreakdown(snapshot) {
50
- const exact = snapshot.exact;
51
- const systemExact = typeof exact?.system === "number";
52
- const rulesTokens = snapshot.rulesBlock ? estimateTokens(snapshot.rulesBlock) : 0;
53
- const rulesItems = (snapshot.rulesFiles ?? []).map((f) => ({
54
- id: f.path,
55
- label: f.path,
56
- tokens: 0,
57
- estimated: false
58
- }));
59
- const skillsTokens = snapshot.skillsCatalog ? estimateTokens(snapshot.skillsCatalog) : 0;
60
- const mcpInstructionsTokens = snapshot.mcpInstructions ? estimateTokens(snapshot.mcpInstructions) : 0;
61
- const subagentTokens = snapshot.subagentDefs ? estimateTokens(snapshot.subagentDefs) : 0;
62
- const embeddedSubsections = rulesTokens + skillsTokens + mcpInstructionsTokens + subagentTokens;
63
- const systemTotal = systemExact ? exact.system : estimateTokens(snapshot.system);
64
- const baseSystemTokens = Math.max(0, systemTotal - embeddedSubsections);
65
- const nativeToolsEst = sumEstimate(snapshot.toolsJson);
66
- const mcpDisclosed = snapshot.mcpGroups.map(mcpGroupTokens);
67
- const mcpToolsEst = mcpDisclosed.reduce((a, g) => a + g.total, 0);
68
- let nativeToolsTokens = nativeToolsEst;
69
- let mcpToolsTokens = mcpToolsEst;
70
- let toolsEstimated = true;
71
- if (systemExact && typeof exact?.systemAndTools === "number") {
72
- const exactToolsTotal = Math.max(0, exact.systemAndTools - exact.system);
73
- const estTotal = nativeToolsEst + mcpToolsEst;
74
- if (estTotal > 0) {
75
- nativeToolsTokens = Math.round(exactToolsTotal * (nativeToolsEst / estTotal));
76
- mcpToolsTokens = exactToolsTotal - nativeToolsTokens;
77
- } else {
78
- nativeToolsTokens = exactToolsTotal;
79
- mcpToolsTokens = 0;
80
- }
81
- toolsEstimated = false;
82
- }
83
- const live = [];
84
- const push = (id, tokens, estimated, items) => {
85
- if (tokens <= 0 && !items?.length) return;
86
- live.push({
87
- id,
88
- label: LABELS[id],
89
- tokens: Math.max(0, tokens),
90
- estimated,
91
- ...items?.length ? { items } : {}
92
- });
93
- };
94
- push("systemPrompt", baseSystemTokens, !systemExact);
95
- push("rules", rulesTokens, true, rulesItems);
96
- push("skills", skillsTokens, true);
97
- push("mcpInstructions", mcpInstructionsTokens, true);
98
- push("subagentDefs", subagentTokens, true);
99
- push("tools", nativeToolsTokens, toolsEstimated);
100
- push("mcpTools", mcpToolsTokens, toolsEstimated, mcpDisclosed.flatMap((g) => g.items));
101
- const accountedStatic = live.reduce((a, c) => a + c.tokens, 0);
102
- push("conversation", Math.max(0, snapshot.used - accountedStatic), !(systemExact && !toolsEstimated));
103
- const deferred = [];
104
- const deferredNativeTools = sumEstimate(snapshot.deferredToolsJson);
105
- if (deferredNativeTools > 0) deferred.push({
106
- id: "toolsDeferred",
107
- label: LABELS.toolsDeferred,
108
- tokens: deferredNativeTools,
109
- estimated: true,
110
- deferred: true
111
- });
112
- const deferredMcp = snapshot.deferredMcpGroups.map(mcpGroupTokens);
113
- const deferredMcpTotal = deferredMcp.reduce((a, g) => a + g.total, 0);
114
- if (deferredMcpTotal > 0) deferred.push({
115
- id: "mcpToolsDeferred",
116
- label: LABELS.mcpToolsDeferred,
117
- tokens: deferredMcpTotal,
118
- estimated: true,
119
- deferred: true,
120
- items: deferredMcp.flatMap((g) => g.items)
121
- });
122
- if (snapshot.autocompactBuffer > 0) deferred.push({
123
- id: "autocompactBuffer",
124
- label: LABELS.autocompactBuffer,
125
- tokens: snapshot.autocompactBuffer,
126
- estimated: false,
127
- deferred: true
128
- });
129
- const free = Math.max(0, snapshot.effectiveWindow - snapshot.used);
130
- deferred.push({
131
- id: "freeSpace",
132
- label: LABELS.freeSpace,
133
- tokens: free,
134
- estimated: false,
135
- deferred: true
136
- });
137
- const categories = [...live, ...deferred];
138
- const hasEstimates = live.some((c) => c.estimated && c.tokens > 0);
139
- const fraction = snapshot.effectiveWindow > 0 ? Math.max(0, Math.min(1, snapshot.used / snapshot.effectiveWindow)) : 0;
140
- return {
141
- modelId: snapshot.modelId,
142
- used: snapshot.used,
143
- effectiveWindow: snapshot.effectiveWindow,
144
- fraction,
145
- categories,
146
- hasEstimates,
147
- ...snapshot.usage ? { usage: snapshot.usage } : {},
148
- ...snapshot.extensionSections?.length ? { extensionSections: snapshot.extensionSections } : {},
149
- ...snapshot.activeSkills?.length ? { activeSkills: snapshot.activeSkills } : {}
150
- };
151
- }
152
- //#endregion
153
- export { buildContextBreakdown as t };
154
-
155
- //# sourceMappingURL=context-breakdown-Dzo25N45.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-breakdown-Dzo25N45.js","names":[],"sources":["../src/chat/context-breakdown.ts"],"sourcesContent":["/**\n * Context-usage breakdown — pure transforms.\n *\n * Turns a {@link ContextSnapshot} (the raw pieces the agent assembled for a\n * run: system prompt, wire tools, deferred tools, MCP tools/instructions,\n * skills catalog, ...) plus the real last-turn token total into a categorized\n * {@link ContextBreakdown} the TUI panel / GUI popover render.\n *\n * No React, no node — a browser-context renderer imports these via\n * `zidane/chat/pure`. The snapshot is captured main-side (TUI direct, GUI in\n * `main/chat`) because building it reads provider/agent state; only the pure\n * shaping lives here.\n *\n * Accuracy: per-category counts are estimates when no provider `countTokens`\n * is available — the heuristic ({@link estimateTokens}, ~4 bytes/token) sizes\n * each static segment and the bar is reconciled so it always sums to the real\n * last-turn total (`conversation = realTotal - sum(static)`). When a provider\n * exposes exact counts (Anthropic, OpenAI), the caller passes them in via\n * {@link ContextSnapshot.exact} and the corresponding categories drop their\n * `estimated` flag. Exact buckets (free space, autocompact buffer) are never\n * estimated.\n */\n\nimport type { SessionTurn } from '../types'\nimport { estimateTokens } from '../compact/utils'\n\n/**\n * Stable identifiers for each breakdown category. Drives swatch color +\n * ordering in both UIs. `*Deferred` buckets are tools/MCP that exist but are\n * NOT currently on the wire (lazy disclosure); `autocompactBuffer` is the\n * reserved output headroom; `freeSpace` is the remaining window.\n */\nexport type ContextCategoryId\n = | 'systemPrompt'\n | 'rules'\n | 'tools'\n | 'mcpTools'\n | 'mcpInstructions'\n | 'skills'\n | 'subagentDefs'\n | 'conversation'\n | 'mcpToolsDeferred'\n | 'toolsDeferred'\n | 'autocompactBuffer'\n | 'freeSpace'\n\n/**\n * Exact last-turn token usage split (provider-reported — always exact). Drives\n * the cache breakdown shown below the categories. `cacheRead + cacheCreation +\n * input` equals the live context total (`used`).\n */\nexport interface ContextUsageSplit {\n /** Fresh, uncached input tokens this turn. */\n input: number\n /** Tokens served from the prompt cache (the cached prefix). */\n cacheRead: number\n /** Tokens written to the cache this turn. */\n cacheCreation: number\n /** Assistant output tokens (not part of the input window; informational). */\n output: number\n}\n\n/** One display row contributed by an extension to the context panel. */\nexport interface ContextExtensionRow {\n label: string\n value: string\n}\n\n/** Extension-owned context panel section. Core treats rows as opaque display data. */\nexport interface ContextExtensionSection {\n id: string\n label: string\n /** Optional section-title foreground color. UIs fall back to their theme accent. */\n accentColor?: string\n rows: readonly ContextExtensionRow[]\n}\n\n/** Data exposed to extension context-breakdown contributors. */\nexport interface ContextBreakdownContributionContext {\n turns: readonly SessionTurn[]\n}\n\n/** Extension hook for adding opaque rows to the context panel. */\nexport type ContextBreakdownContributor\n = (ctx: ContextBreakdownContributionContext) =>\n | ContextExtensionSection\n | readonly ContextExtensionSection[]\n | null\n | undefined\n | Promise<ContextExtensionSection | readonly ContextExtensionSection[] | null | undefined>\n\n/** One expandable child row under a category (e.g. a single MCP tool / memory file). */\nexport interface ContextItem {\n /** Stable-ish key for rendering (tool name, file path). */\n id: string\n /** Display label. */\n label: string\n /** Token count for this item. */\n tokens: number\n /** True when `tokens` came from the heuristic rather than an exact count. */\n estimated: boolean\n}\n\n/** One category row in the breakdown. */\nexport interface ContextCategory {\n id: ContextCategoryId\n /** Display label (e.g. \"Tool definitions\"). */\n label: string\n /** Token count for the category. */\n tokens: number\n /** True when `tokens` came from the heuristic rather than an exact count. */\n estimated: boolean\n /**\n * True for buckets that are NOT counted against the live window total\n * (deferred tools, free space, autocompact buffer) — the UI renders these\n * dimmed / below a divider, mirroring Claude Code's layout.\n */\n deferred?: boolean\n /** Optional expandable child rows (per MCP tool, per memory file, ...). */\n items?: ContextItem[]\n}\n\n/** The shaped, render-ready breakdown. */\nexport interface ContextBreakdown {\n /** Model id the snapshot was built for. */\n modelId: string\n /**\n * Real last-turn context size (provider-reported `input + cacheRead +\n * cacheCreation`) — the SAME number the footer/orb shows. The non-deferred\n * categories always sum to this.\n */\n used: number\n /** Effective window (raw - output reserve), the bar's denominator. */\n effectiveWindow: number\n /** `used / effectiveWindow`, clamped to [0, 1]. */\n fraction: number\n /** Live categories (sum to `used`) followed by deferred + free-space buckets. */\n categories: ContextCategory[]\n /** True when ANY live category is estimated (drives the panel's footnote). */\n hasEstimates: boolean\n /**\n * Exact last-turn usage split (cache read / write / fresh input / output),\n * when the host supplied it. Always exact — rendered as an informational\n * section below the categories. Absent when no usage is available yet.\n */\n usage?: ContextUsageSplit\n /** Extension-owned context panel sections. */\n extensionSections?: readonly ContextExtensionSection[]\n /**\n * Names of currently-active skills. Informational — an activated skill's body\n * lands in the conversation (tool-results), so its tokens are already counted\n * under Conversation; this just surfaces \"which skills are loaded\". Empty/absent\n * when none are active.\n */\n activeSkills?: string[]\n}\n\n/** Host-supplied options for `agent.getContextBreakdown`. */\nexport interface ContextBreakdownOptions {\n /**\n * Effective context window (`rawWindow - outputReserve`) for the active\n * model, resolved by the host via the chat model registry. Drives free-space\n * + fraction. When omitted, defaults to `used` (no headroom shown).\n */\n effectiveWindow?: number\n /**\n * Active model id — used for the exact `countTokens` call + the header label.\n * Only consulted on the PRE-RUN path (no snapshot yet); once a run has fired,\n * the snapshot's own model wins. When omitted, the provider default is used.\n */\n model?: string\n /** Tokens reserved for output (autocompact buffer). Defaults to 0. */\n autocompactBuffer?: number\n /**\n * Auto-compact threshold as a fraction of the window (0–1), when enabled.\n * When set, the \"Autocompact buffer\" bucket represents the ENTIRE tail from\n * the threshold to the end of the window (`(1 − threshold) × window`) rather\n * than just the reserved-output tokens — so the bar/row show exactly where\n * compaction fires. When omitted, `autocompactBuffer` is used as-is.\n */\n compactThreshold?: number\n /** Abort signal forwarded to the provider's `countTokens`. */\n signal?: AbortSignal\n}\n\n/** Tools grouped by MCP server, for the expandable per-server rows. */\nexport interface ContextMcpGroup {\n server: string\n /** Wire tool specs for this server (their JSON size is measured). */\n tools: { name: string, json: string }[]\n}\n\n/**\n * Exact per-segment counts from a provider's `countTokens`, when available.\n * Each field is the count of the cumulative payload up to and including that\n * segment, so categories are derived by differencing (see\n * {@link buildContextBreakdown}). Any subset may be present; missing fields\n * fall back to the heuristic for that category.\n */\nexport interface ContextExactCounts {\n /** Tokens for system prompt alone. */\n system?: number\n /** Tokens for system + tools (so tools = systemAndTools - system). */\n systemAndTools?: number\n /** Tokens for the full payload (system + tools + messages) = the real total. */\n full?: number\n}\n\n/**\n * Raw inputs captured at agent prompt-assembly time. Everything here is what\n * actually went on (or was withheld from) the wire for the last run.\n */\nexport interface ContextSnapshot {\n modelId: string\n /** Full rendered system prompt (marker-stripped, as sent). */\n system: string\n /**\n * Host-composed base system BEFORE the agent appended skills / searchable /\n * MCP-instructions catalogs. Includes the rules block. The \"System prompt\"\n * (base doctrine) bucket = base − rules − env-noise. Defaults to `system`\n * when not provided (back-compat).\n */\n baseSystem?: string\n /** Rendered AGENTS.md/CLAUDE.md rules block (a substring of `baseSystem`). */\n rulesBlock?: string\n /** Source files behind the rules block, for per-file rows. */\n rulesFiles?: { path: string, source: string }[]\n /** Wire tool specs currently disclosed to the model, as JSON strings. */\n toolsJson: string[]\n /** Lazy tool entries NOT currently disclosed (deferred), as JSON strings. */\n deferredToolsJson: string[]\n /** MCP tools grouped by server (disclosed ones). */\n mcpGroups: ContextMcpGroup[]\n /** Deferred (undisclosed) MCP tools grouped by server. */\n deferredMcpGroups: ContextMcpGroup[]\n /** Rendered MCP instructions block, if any. */\n mcpInstructions?: string\n /** Rendered skills catalog text, if any. */\n skillsCatalog?: string\n /** Searchable / subagent (spawn) tool definitions text, if any. */\n subagentDefs?: string\n /** Memory / rules files in context. */\n /** Real last-turn total (input + cacheRead + cacheCreation). */\n used: number\n /** Effective window (raw - output reserve). */\n effectiveWindow: number\n /** Tokens reserved for output (autocompact buffer). */\n autocompactBuffer: number\n /** Exact per-segment counts from the provider, when available. */\n exact?: ContextExactCounts\n /** Exact last-turn usage split, when available (passed straight through). */\n usage?: ContextUsageSplit\n /** Extension-owned context panel sections. */\n extensionSections?: readonly ContextExtensionSection[]\n /** Names of currently-active skills (passed straight through). */\n activeSkills?: string[]\n}\n\nconst LABELS: Record<ContextCategoryId, string> = {\n systemPrompt: 'System prompt',\n rules: 'Rules (AGENTS.md)',\n tools: 'Tool definitions',\n mcpTools: 'MCP tools',\n mcpInstructions: 'MCP instructions',\n skills: 'Skills',\n subagentDefs: 'Subagent definitions',\n conversation: 'Conversation',\n mcpToolsDeferred: 'MCP tools (deferred)',\n toolsDeferred: 'System tools (deferred)',\n autocompactBuffer: 'Autocompact buffer',\n freeSpace: 'Free space',\n}\n\n/** Sum the heuristic token sizes of a list of strings. */\nfunction sumEstimate(parts: readonly string[]): number {\n let total = 0\n for (const p of parts)\n total += estimateTokens(p)\n return total\n}\n\nfunction mcpGroupTokens(group: ContextMcpGroup): { total: number, items: ContextItem[] } {\n const items: ContextItem[] = group.tools.map(t => ({\n id: `${group.server}:${t.name}`,\n label: t.name,\n tokens: estimateTokens(t.json),\n estimated: true,\n }))\n let total = 0\n for (const it of items)\n total += it.tokens\n return { total, items }\n}\n\n/**\n * Shape a {@link ContextSnapshot} into a render-ready {@link ContextBreakdown}.\n *\n * Live categories (system prompt, tools, MCP tools/instructions, skills,\n * memory, subagent defs, conversation) sum to `snapshot.used` — `conversation`\n * absorbs the remainder so the bar matches the provider-reported total exactly.\n * Deferred + autocompact + free-space buckets follow.\n *\n * When `snapshot.exact` carries provider counts, the matching categories use\n * them (system, tools) and drop their `estimated` flag; everything else stays\n * on the heuristic.\n */\nexport function buildContextBreakdown(snapshot: ContextSnapshot): ContextBreakdown {\n const exact = snapshot.exact\n const systemExact = typeof exact?.system === 'number'\n\n // --- System-embedded sub-sections (heuristic) ----------------------------\n // The exact `count_tokens` system number covers the WHOLE system text. The\n // appended catalogs (rules / skills / MCP instructions / subagent defs) all\n // live inside it, so we size each heuristically and let the \"System prompt\"\n // (base doctrine) bucket absorb the remainder — keeping the system group's\n // sum exact while still attributing each section.\n const rulesTokens = snapshot.rulesBlock ? estimateTokens(snapshot.rulesBlock) : 0\n const rulesItems: ContextItem[] = (snapshot.rulesFiles ?? []).map(f => ({\n id: f.path,\n label: f.path,\n // Per-file size isn't separable from the rendered block; show the path only\n // (tokens 0, not estimated — there's nothing to estimate) so the row lists\n // sources without implying a per-file count or a `~` marker.\n tokens: 0,\n estimated: false,\n }))\n const skillsTokens = snapshot.skillsCatalog ? estimateTokens(snapshot.skillsCatalog) : 0\n const mcpInstructionsTokens = snapshot.mcpInstructions ? estimateTokens(snapshot.mcpInstructions) : 0\n const subagentTokens = snapshot.subagentDefs ? estimateTokens(snapshot.subagentDefs) : 0\n const embeddedSubsections = rulesTokens + skillsTokens + mcpInstructionsTokens + subagentTokens\n\n // System TOTAL (whole system text) — exact when available.\n const systemTotal = systemExact ? exact!.system! : estimateTokens(snapshot.system)\n // Base doctrine = system total minus the attributed sub-sections.\n const baseSystemTokens = Math.max(0, systemTotal - embeddedSubsections)\n\n // --- Native tool definitions (non-MCP, disclosed) ------------------------\n // When exact `systemAndTools` is available it covers BOTH native + MCP tools,\n // so we split that combined number proportionally by heuristic weight rather\n // than claiming exactness per sub-bucket.\n const nativeToolsEst = sumEstimate(snapshot.toolsJson)\n const mcpDisclosed = snapshot.mcpGroups.map(mcpGroupTokens)\n const mcpToolsEst = mcpDisclosed.reduce((a, g) => a + g.total, 0)\n\n let nativeToolsTokens = nativeToolsEst\n let mcpToolsTokens = mcpToolsEst\n let toolsEstimated = true\n if (systemExact && typeof exact?.systemAndTools === 'number') {\n const exactToolsTotal = Math.max(0, exact.systemAndTools - exact.system!)\n const estTotal = nativeToolsEst + mcpToolsEst\n if (estTotal > 0) {\n nativeToolsTokens = Math.round(exactToolsTotal * (nativeToolsEst / estTotal))\n mcpToolsTokens = exactToolsTotal - nativeToolsTokens\n }\n else {\n nativeToolsTokens = exactToolsTotal\n mcpToolsTokens = 0\n }\n toolsEstimated = false\n }\n\n // --- Live categories -----------------------------------------------------\n // Detailed split. The system-embedded sub-sections (rules / skills / MCP\n // instructions / subagent defs) are heuristic; the \"System prompt\" base\n // bucket carries the exact remainder so the whole system group still sums to\n // the exact system count. Tools/MCP-tools/conversation are split as before.\n const live: ContextCategory[] = []\n const push = (id: ContextCategoryId, tokens: number, estimated: boolean, items?: ContextItem[]) => {\n if (tokens <= 0 && !items?.length)\n return\n live.push({ id, label: LABELS[id], tokens: Math.max(0, tokens), estimated, ...(items?.length ? { items } : {}) })\n }\n\n // System group: base doctrine (exact remainder) + attributed sub-sections.\n push('systemPrompt', baseSystemTokens, !systemExact)\n push('rules', rulesTokens, true, rulesItems)\n push('skills', skillsTokens, true)\n push('mcpInstructions', mcpInstructionsTokens, true)\n push('subagentDefs', subagentTokens, true)\n // Tools.\n push('tools', nativeToolsTokens, toolsEstimated)\n push('mcpTools', mcpToolsTokens, toolsEstimated, mcpDisclosed.flatMap(g => g.items))\n\n // --- Conversation = remainder (keeps the bar summing to `used`) ----------\n const accountedStatic = live.reduce((a, c) => a + c.tokens, 0)\n const conversationTokens = Math.max(0, snapshot.used - accountedStatic)\n // Exact when both system AND tools were exactly counted — then the remainder\n // (used − exactSystem − exactTools) is itself exact, since `used` is the\n // provider-reported total.\n const conversationEstimated = !(systemExact && !toolsEstimated)\n push('conversation', conversationTokens, conversationEstimated)\n\n // --- Deferred buckets (not counted against `used`) -----------------------\n const deferred: ContextCategory[] = []\n const deferredNativeTools = sumEstimate(snapshot.deferredToolsJson)\n if (deferredNativeTools > 0) {\n deferred.push({ id: 'toolsDeferred', label: LABELS.toolsDeferred, tokens: deferredNativeTools, estimated: true, deferred: true })\n }\n const deferredMcp = snapshot.deferredMcpGroups.map(mcpGroupTokens)\n const deferredMcpTotal = deferredMcp.reduce((a, g) => a + g.total, 0)\n if (deferredMcpTotal > 0) {\n deferred.push({\n id: 'mcpToolsDeferred',\n label: LABELS.mcpToolsDeferred,\n tokens: deferredMcpTotal,\n estimated: true,\n deferred: true,\n items: deferredMcp.flatMap(g => g.items),\n })\n }\n\n // --- Autocompact buffer + free space (exact) -----------------------------\n if (snapshot.autocompactBuffer > 0) {\n deferred.push({ id: 'autocompactBuffer', label: LABELS.autocompactBuffer, tokens: snapshot.autocompactBuffer, estimated: false, deferred: true })\n }\n const free = Math.max(0, snapshot.effectiveWindow - snapshot.used)\n deferred.push({ id: 'freeSpace', label: LABELS.freeSpace, tokens: free, estimated: false, deferred: true })\n\n const categories = [...live, ...deferred]\n const hasEstimates = live.some(c => c.estimated && c.tokens > 0)\n const fraction = snapshot.effectiveWindow > 0\n ? Math.max(0, Math.min(1, snapshot.used / snapshot.effectiveWindow))\n : 0\n\n return {\n modelId: snapshot.modelId,\n used: snapshot.used,\n effectiveWindow: snapshot.effectiveWindow,\n fraction,\n categories,\n hasEstimates,\n ...(snapshot.usage ? { usage: snapshot.usage } : {}),\n ...(snapshot.extensionSections?.length ? { extensionSections: snapshot.extensionSections } : {}),\n ...(snapshot.activeSkills?.length ? { activeSkills: snapshot.activeSkills } : {}),\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"edit-utils-EGosADZq.js","names":[],"sources":["../src/tools/edit-utils.ts"],"sourcesContent":["/**\n * Internal helpers shared between the `edit` and `multi_edit` tools.\n *\n * Not part of the public API — intentionally not re-exported from `tools/index.ts`\n * or the package barrel.\n */\n\n/**\n * Count exact (non-overlapping) occurrences of `needle` in `haystack`.\n * Returns 0 for an empty needle — both edit tools reject empty `old_string`\n * up front, so this branch is defensive rather than semantic.\n */\nexport function countExactMatches(haystack: string, needle: string): number {\n if (needle.length === 0)\n return 0\n let count = 0\n let idx = 0\n while (true) {\n const next = haystack.indexOf(needle, idx)\n if (next === -1)\n break\n count++\n idx = next + needle.length\n }\n return count\n}\n\n// ---------------------------------------------------------------------------\n// Curly-quote normalization + SDK-sanitization fallbacks\n// ---------------------------------------------------------------------------\n//\n// Models can't reliably emit Unicode curly quotes, and the Anthropic API\n// silently sanitizes a handful of XML-like tags before they hit the model\n// (`<name>` → `<n>`, etc.). When `old_string` doesn't match exactly, we retry\n// against the same content with both transforms applied so an \"old_string not\n// found\" failure that's only typographical resolves on the second pass.\n//\n// Mirrors the Claude Code Edit tool's recovery strategy. Verbatim port of the\n// quote constants and the desanitization table; the resolve loop below is\n// reorganized to share state between exact / quote-normalized / desanitized.\n\nexport const LEFT_SINGLE_CURLY_QUOTE = '\\u2018'\nexport const RIGHT_SINGLE_CURLY_QUOTE = '\\u2019'\nexport const LEFT_DOUBLE_CURLY_QUOTE = '\\u201C'\nexport const RIGHT_DOUBLE_CURLY_QUOTE = '\\u201D'\n\n/** Map curly quotes (any of the four) to their straight ASCII equivalents. */\nexport function normalizeQuotes(str: string): string {\n return str\n .replaceAll(LEFT_SINGLE_CURLY_QUOTE, '\\'')\n .replaceAll(RIGHT_SINGLE_CURLY_QUOTE, '\\'')\n .replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '\"')\n .replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '\"')\n}\n\n/**\n * Substitutions Anthropic's API applies to assistant output before the model\n * sees it. The model emits the sanitized form; the file on disk contains the\n * unsanitized form. We undo the substitutions on `old_string` so the search\n * lands on the actual file contents.\n *\n * Verbatim from `claude-code/tools/FileEditTool/utils.ts`.\n */\nconst DESANITIZATIONS: ReadonlyArray<readonly [string, string]> = [\n ['<fnr>', '<function_results>'],\n ['<n>', '<name>'],\n ['</n>', '</name>'],\n ['<o>', '<output>'],\n ['</o>', '</output>'],\n ['<e>', '<error>'],\n ['</e>', '</error>'],\n ['<s>', '<system>'],\n ['</s>', '</system>'],\n ['<r>', '<result>'],\n ['</r>', '</result>'],\n ['< META_START >', '<META_START>'],\n ['< META_END >', '<META_END>'],\n ['< EOT >', '<EOT>'],\n ['< META >', '<META>'],\n ['< SOS >', '<SOS>'],\n ['\\n\\nH:', '\\n\\nHuman:'],\n ['\\n\\nA:', '\\n\\nAssistant:'],\n]\n\n/**\n * Apply the SDK desanitization table to a string. Exported so the edit tools\n * can apply it to `new_string` whenever `old_string` matched via a\n * desanitize-class fallback — keeps the file's unsanitized form on disk\n * instead of writing the model's abbreviated form back.\n */\nexport function desanitize(s: string): string {\n let out = s\n for (const [from, to] of DESANITIZATIONS)\n out = out.replaceAll(from, to)\n return out\n}\n\n/**\n * Strip line-number prefixes from each line of a needle, used as a recovery\n * fallback when the model pastes a `read_file` chunk verbatim into\n * `old_string` — the on-disk file doesn't carry the metadata prefix.\n *\n * Accepts three separator characters so a model that learned on a different\n * agent stack still works here: `\\t` (Claude Code compact, our default),\n * `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one\n * of `\\t | →`. The 9-digit ceiling covers files up to ~1B lines without\n * overshooting into legitimate `\\d{N}<sep>` content like Markdown table\n * cells with long numeric IDs.\n */\nconst LINE_NUMBER_PREFIX_RE = /^[ \\t]*\\d{1,9}[\\t|\\u2192]/gm\nexport function stripLineNumberPrefixes(s: string): string {\n return s.replace(LINE_NUMBER_PREFIX_RE, '')\n}\n\n/**\n * Locate the actual substring in `haystack` that corresponds to `needle`,\n * recovering from typographical mismatch on six escalating fallbacks:\n *\n * 1. Exact substring match (the happy path — no transformation).\n * 2. Curly-quote normalization on both sides — when the model emits straight\n * quotes but the file has curly ones (or vice versa), the slice of the\n * file at the matched position carries the file's actual typography.\n * 3. Anthropic-sanitization undo on the needle — the model's `<n>` becomes\n * the file's `<name>`, etc.\n * 4. Combined: desanitize + quote normalization on the needle.\n * 5. Line-number-prefix strip — when the model pasted a numbered `read_file`\n * chunk verbatim into `old_string` and the file carries no such prefix.\n * 6. Combined: line-number strip + quote normalization — paste-back of a\n * numbered chunk against a file whose typography differs from the model's.\n *\n * Returns `{ actual, occurrences }` — `actual` is the string that exists in\n * the file (use this to do the actual replace; preserves the file's\n * typography). `occurrences` is the count of those matches in the file.\n *\n * Fallbacks 3-4 (desanitize-class) only fire when the transformed needle is\n * UNIQUE in the file — they rewrite the needle's meaning rather than its\n * typography, so an ambiguous match risks silently retargeting the wrong\n * text (see the inline comment at step 3).\n *\n * Returns `null` when no recovery worked.\n */\nexport interface ResolvedMatch {\n actual: string\n occurrences: number\n /** Recovery path — `'exact'` when no transformation was applied. */\n via: 'exact' | 'quotes' | 'desanitize' | 'quotes+desanitize' | 'line-numbers' | 'quotes+line-numbers'\n}\n\n/**\n * Search `target` in `normFile` and slice the matching span out of the\n * original `haystack`, counting all non-overlapping occurrences. `normFile`\n * is the haystack with whatever transform (quotes / desanitize / combined)\n * was applied to make the indices align — slicing the original haystack\n * preserves the file's actual typography so `replace_all` writes back the\n * file's form, not the model's.\n *\n * Pre-condition: `normFile.length === haystack.length` (every transform\n * we use is one-to-one). Returns null on miss.\n */\nfunction locateAndCount(\n haystack: string,\n normFile: string,\n target: string,\n via: ResolvedMatch['via'],\n): ResolvedMatch | null {\n const idx = normFile.indexOf(target)\n if (idx === -1)\n return null\n const actual = haystack.slice(idx, idx + target.length)\n let occ = 0\n let cursor = 0\n while (true) {\n const next = normFile.indexOf(target, cursor)\n if (next === -1)\n break\n occ++\n cursor = next + target.length\n }\n return { actual, occurrences: occ, via }\n}\n\nexport function resolveOldString(haystack: string, needle: string): ResolvedMatch | null {\n // 1. Exact match — happy path.\n const exact = countExactMatches(haystack, needle)\n if (exact > 0)\n return { actual: needle, occurrences: exact, via: 'exact' }\n\n // 2. Curly-quote normalization. Either side can carry curly quotes:\n // model emitted straight + file has curly, OR model emitted curly +\n // file has straight. Normalize both and look there. Slice the original\n // haystack to recover the actual file-side typography.\n const normNeedle = normalizeQuotes(needle)\n const normFile = normalizeQuotes(haystack)\n if (normNeedle !== needle || normFile !== haystack) {\n const m = locateAndCount(haystack, normFile, normNeedle, 'quotes')\n if (m)\n return m\n }\n\n // 3. Desanitize the needle — model-side substitution undo (`<n>` → `<name>`,\n // etc.) The file always has the unsanitized form; only the model emits\n // the abbreviated version.\n //\n // Desanitize-class fallbacks additionally require a UNIQUE transformed\n // match. Unlike the quote / line-number transforms (typography-only /\n // verbatim paste-back), this one rewrites the needle's meaning — a model\n // that genuinely meant the literal `<n>` against a file full of `<name>`\n // would otherwise silently retarget every occurrence under `replace_all`.\n // A unique hit bounds the blast radius to one visible replacement.\n const desan = desanitize(needle)\n if (desan !== needle) {\n const desanCount = countExactMatches(haystack, desan)\n if (desanCount === 1)\n return { actual: desan, occurrences: desanCount, via: 'desanitize' }\n }\n\n // 4. Combined: desanitize + quote normalization. Hit when the model\n // emitted both sanitized tokens and straight quotes, but the file has\n // the unsanitized form with curly quotes. Same uniqueness requirement\n // as #3.\n const combo = desanitize(normNeedle)\n if (combo !== needle) {\n const m = locateAndCount(haystack, normFile, combo, 'quotes+desanitize')\n if (m && m.occurrences === 1)\n return m\n }\n\n // 5. Line-number-prefix strip. When `read_file` returns line-numbered\n // output and the model pasted the chunk verbatim into `old_string`,\n // the on-disk file doesn't carry those prefixes. Strip them and\n // retry; the file-side `actual` is the prefix-free needle so the\n // replacement lands on real bytes.\n //\n // Guard against a needle that strips down to whitespace-only (e.g.\n // `1\\t\\n2\\t\\n3\\t` → `\\n\\n`). Searching for `\\n\\n` would match arbitrary\n // blank-line patterns anywhere in the file — a high false-positive risk.\n // Require the stripped needle to carry at least one non-whitespace\n // character before we trust the result.\n const stripped = stripLineNumberPrefixes(needle)\n if (stripped !== needle && stripped.trim().length > 0) {\n const count = countExactMatches(haystack, stripped)\n if (count > 0)\n return { actual: stripped, occurrences: count, via: 'line-numbers' }\n\n // 6. Combined: line-number strip + curly-quote normalization. The\n // typography mismatch may live in the file, the needle, or both.\n const strippedNorm = normalizeQuotes(stripped)\n if (strippedNorm !== stripped || normFile !== haystack) {\n const m = locateAndCount(haystack, normFile, strippedNorm, 'quotes+line-numbers')\n if (m)\n return m\n }\n }\n\n return null\n}\n\n/**\n * Short human-readable label for a recovery path, used by `edit` /\n * `multi_edit` to flag non-exact matches in their result messages — when a\n * fallback mistargets, the model (and the user reading the transcript) can\n * see WHICH transform fired instead of silently trusting the edit.\n */\nexport function describeVia(via: ResolvedMatch['via']): string {\n switch (via) {\n case 'exact': return 'exact match'\n case 'quotes': return 'curly-quote normalization'\n case 'desanitize': return 'sanitization recovery (e.g. <n> → <name>)'\n case 'quotes+desanitize': return 'sanitization recovery + curly-quote normalization'\n case 'line-numbers': return 'line-number-prefix stripping'\n case 'quotes+line-numbers': return 'line-number-prefix stripping + curly-quote normalization'\n }\n}\n\n/**\n * Apply the same recovery transforms used to find `old_string` to\n * `new_string`, so the file gets back its native form: desanitize when\n * the model emitted `<n>` for `<name>`, strip line-number prefixes when\n * the match required them, then re-curlify when the match required\n * quote normalization. Shared between `edit` and `multi_edit`.\n */\nexport function styleReplacementForVia(\n replacement: string,\n via: ResolvedMatch['via'],\n actual: string,\n): string {\n let out = replacement\n if (via === 'desanitize' || via === 'quotes+desanitize')\n out = desanitize(out)\n if (via === 'line-numbers' || via === 'quotes+line-numbers')\n out = stripLineNumberPrefixes(out)\n if (via === 'quotes' || via === 'quotes+desanitize' || via === 'quotes+line-numbers')\n out = preserveQuoteStyle(actual, out)\n return out\n}\n\n/**\n * When `old_string` matched via curly-quote normalization, re-style\n * `new_string` so the file's typography is preserved across the edit.\n * Detects whether the matched file region had curly singles, doubles, or\n * both, and applies the matching curlification to the replacement.\n *\n * Apostrophes in contractions (`don't`, `it's`) get the right-single curly\n * quote regardless of opening context — that's the canonical typographer's\n * convention for English. Other quotes use a simple\n * preceded-by-whitespace-or-opening-punctuation heuristic.\n */\nexport function preserveQuoteStyle(actual: string, replacement: string): string {\n const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE)\n const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE)\n if (!hasDouble && !hasSingle)\n return replacement\n\n let out = replacement\n if (hasDouble)\n out = applyCurly(out, '\"', LEFT_DOUBLE_CURLY_QUOTE, RIGHT_DOUBLE_CURLY_QUOTE, false)\n if (hasSingle)\n out = applyCurly(out, '\\'', LEFT_SINGLE_CURLY_QUOTE, RIGHT_SINGLE_CURLY_QUOTE, true)\n return out\n}\n\nfunction applyCurly(\n s: string,\n straight: string,\n left: string,\n right: string,\n contractionAware: boolean,\n): string {\n const chars = [...s]\n const result: string[] = []\n for (let i = 0; i < chars.length; i++) {\n if (chars[i] !== straight) {\n result.push(chars[i])\n continue\n }\n if (contractionAware) {\n const prev = i > 0 ? chars[i - 1] : ''\n const next = i < chars.length - 1 ? chars[i + 1] : ''\n // Letter-quote-letter is a contraction, not a quote — always right.\n if (/\\p{L}/u.test(prev) && /\\p{L}/u.test(next)) {\n result.push(right)\n continue\n }\n }\n result.push(isOpeningContext(chars, i) ? left : right)\n }\n return result.join('')\n}\n\nfunction isOpeningContext(chars: string[], i: number): boolean {\n if (i === 0)\n return true\n const prev = chars[i - 1]\n return prev === ' ' || prev === '\\t' || prev === '\\n' || prev === '\\r'\n || prev === '(' || prev === '[' || prev === '{'\n || prev === '\\u2014' || prev === '\\u2013' // em / en dash\n}\n"],"mappings":"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"}