howtofixcli 0.7.13 → 0.7.15

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 (161) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-6FUWMLWO.js +1 -0
  5. package/dist/chunk-7CHDNTH6.js +1 -0
  6. package/dist/chunk-7DTQKDO4.js +2 -0
  7. package/dist/chunk-AGGIYXN7.js +2 -0
  8. package/dist/chunk-AUDMRGWF.js +189 -0
  9. package/dist/chunk-CPJKKHGH.js +1 -0
  10. package/dist/chunk-DAPXGSGD.js +12 -0
  11. package/dist/chunk-DOD7MUGX.js +1 -0
  12. package/dist/chunk-DV2BBBBJ.js +2 -0
  13. package/dist/chunk-EIBOS5KO.js +5 -0
  14. package/dist/chunk-EYWOVCSS.js +6 -0
  15. package/dist/chunk-FPXVDP32.js +8 -0
  16. package/dist/chunk-GXB7RZHH.js +1 -0
  17. package/dist/chunk-HT3MFLAV.js +34 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-JMKK26AT.js +2 -0
  21. package/dist/chunk-LAHVHRCS.js +2 -0
  22. package/dist/chunk-LOOFOBTS.js +1 -0
  23. package/dist/chunk-LSJC73DG.js +1 -0
  24. package/dist/chunk-MOKUBKE7.js +1 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-SX5VE6XA.js +110 -0
  30. package/dist/chunk-TSEKPGK4.js +1 -0
  31. package/dist/chunk-UNK5HRQR.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V2RPZMLK.js +1 -0
  34. package/dist/chunk-V3R4FYTJ.js +1 -0
  35. package/dist/chunk-V656XVXV.js +8 -0
  36. package/dist/chunk-VLMQSD25.js +1 -0
  37. package/dist/chunk-WKUJ7CFO.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-Y7YM5R75.js +5 -0
  42. package/dist/chunk-YW7NORH3.js +2 -0
  43. package/dist/main.js +99 -540
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/activity.js +0 -124
  48. package/dist/agent-defs.js +0 -237
  49. package/dist/agent-limiter.js +0 -68
  50. package/dist/agents.js +0 -554
  51. package/dist/anim.js +0 -361
  52. package/dist/args.js +0 -228
  53. package/dist/at-file.js +0 -28
  54. package/dist/brand-extension.js +0 -87
  55. package/dist/brand.js +0 -76
  56. package/dist/browser.js +0 -58
  57. package/dist/cache-report.js +0 -139
  58. package/dist/checkpoint.js +0 -437
  59. package/dist/claims.js +0 -167
  60. package/dist/cli-auth.js +0 -142
  61. package/dist/cli-checkin.js +0 -120
  62. package/dist/cli-credentials.js +0 -254
  63. package/dist/clipboard.js +0 -149
  64. package/dist/collapse.js +0 -129
  65. package/dist/commands.js +0 -294
  66. package/dist/compaction-guard.js +0 -81
  67. package/dist/context-extension.js +0 -17
  68. package/dist/dcp/commands.js +0 -172
  69. package/dist/dcp/compress-state.js +0 -139
  70. package/dist/dcp/compress.js +0 -329
  71. package/dist/dcp/dcp-extension.js +0 -295
  72. package/dist/dcp/dedup.js +0 -93
  73. package/dist/dcp/ids.js +0 -189
  74. package/dist/dcp/index.js +0 -25
  75. package/dist/dcp/middle-prune.js +0 -76
  76. package/dist/dcp/prompts.js +0 -68
  77. package/dist/dcp/protected-content.js +0 -221
  78. package/dist/dcp/protected.js +0 -161
  79. package/dist/dcp/prune.js +0 -418
  80. package/dist/dcp/state.js +0 -176
  81. package/dist/device-id.js +0 -90
  82. package/dist/device-id.test.js +0 -23
  83. package/dist/errors.js +0 -63
  84. package/dist/extensions.js +0 -48
  85. package/dist/fatal.js +0 -35
  86. package/dist/feed.js +0 -498
  87. package/dist/fs-atomic.js +0 -54
  88. package/dist/gateway-errors.js +0 -90
  89. package/dist/git.js +0 -230
  90. package/dist/goal-extension.js +0 -88
  91. package/dist/goal.js +0 -222
  92. package/dist/hooks-extension.js +0 -176
  93. package/dist/hooks.js +0 -443
  94. package/dist/howtofix-account.js +0 -735
  95. package/dist/hud-line.js +0 -153
  96. package/dist/hwid.js +0 -130
  97. package/dist/interrupt.js +0 -60
  98. package/dist/json-events.js +0 -50
  99. package/dist/level-style.js +0 -112
  100. package/dist/loop-guard.js +0 -95
  101. package/dist/lsp-tools.js +0 -32
  102. package/dist/lsp.js +0 -429
  103. package/dist/mcp-config.js +0 -85
  104. package/dist/mcp-lifecycle.js +0 -147
  105. package/dist/model-catalog.js +0 -217
  106. package/dist/model-facts.js +0 -35
  107. package/dist/model-resolve.js +0 -65
  108. package/dist/notes.js +0 -78
  109. package/dist/oauth-login.js +0 -568
  110. package/dist/paths.js +0 -61
  111. package/dist/permissions.js +0 -298
  112. package/dist/plan-mode.js +0 -363
  113. package/dist/preset-command.js +0 -227
  114. package/dist/preset-extension.js +0 -35
  115. package/dist/preset-mcp.js +0 -292
  116. package/dist/presets.js +0 -727
  117. package/dist/prompt-files.js +0 -103
  118. package/dist/prompt-text.js +0 -41
  119. package/dist/proxy.js +0 -137
  120. package/dist/quality/ast.js +0 -196
  121. package/dist/quality/cognitive.js +0 -83
  122. package/dist/quality/cyclomatic.js +0 -30
  123. package/dist/quality/halstead.js +0 -83
  124. package/dist/quality/index.js +0 -18
  125. package/dist/quality/length.js +0 -3
  126. package/dist/quality/maintainability.js +0 -15
  127. package/dist/quality/nesting.js +0 -57
  128. package/dist/quality/report.js +0 -803
  129. package/dist/quality/smells.js +0 -67
  130. package/dist/quality-tool.js +0 -63
  131. package/dist/read-only-commands.js +0 -597
  132. package/dist/repo-map.js +0 -292
  133. package/dist/rtk.js +0 -193
  134. package/dist/sandbox.js +0 -105
  135. package/dist/schema.js +0 -214
  136. package/dist/session-lifecycle.js +0 -19
  137. package/dist/session-retention.js +0 -111
  138. package/dist/session.js +0 -447
  139. package/dist/sessions.js +0 -100
  140. package/dist/settings.js +0 -148
  141. package/dist/skills.js +0 -35
  142. package/dist/slop-tool.js +0 -96
  143. package/dist/spend-cap.js +0 -43
  144. package/dist/subagent.js +0 -171
  145. package/dist/tar.js +0 -146
  146. package/dist/theme.js +0 -79
  147. package/dist/thinking.js +0 -45
  148. package/dist/todo.js +0 -114
  149. package/dist/tool-loadout.js +0 -97
  150. package/dist/tool-render.js +0 -769
  151. package/dist/tools.js +0 -176
  152. package/dist/ui.js +0 -4778
  153. package/dist/update.js +0 -551
  154. package/dist/usage-segment.js +0 -69
  155. package/dist/web-tools.js +0 -46
  156. package/dist/web.js +0 -265
  157. package/dist/welcome.js +0 -179
  158. package/dist/workflow-engine.js +0 -246
  159. package/dist/workflow-progress.js +0 -446
  160. package/dist/workflow-view.js +0 -195
  161. package/dist/workflow.js +0 -349
@@ -1,295 +0,0 @@
1
- import { assignMessageRefs, formatMessageIdTag, formatMessageRef, refIndicesOf, stripDcpMarkers } from "./ids.js";
2
- import { applyPrune, filterCompressedRanges, planPrune } from "./prune.js";
3
- import { applyMiddlePrune } from "./middle-prune.js";
4
- import { activeCompressedRanges } from "./compress-state.js";
5
- import { rangePrompt } from "./prompts.js";
6
- import { enrichBlockSummary } from "./protected-content.js";
7
- import { getDcpState, saveDcpState } from "./state.js";
8
- import { readMessageText, writeMessageText } from "../prompt-text.js";
9
- /**
10
- * The ordered ref snapshot the compress tool validates against, with each row's
11
- * size estimate.
12
- *
13
- * `tokenCount` is not decoration: `applyCompressionState` sums it over the newly
14
- * covered refs to report `compressedTokens`, so a snapshot without it makes
15
- * every block claim to have saved nothing. The estimate is the same
16
- * length-over-four fallback the rest of this module uses, for the same reason -
17
- * DCP counts with a bundled tokenizer and this repo does not add a dependency
18
- * for one.
19
- */
20
- export function snapshotOf(refs, messages) {
21
- const rows = [];
22
- for (let position = 0; position < refs.length; position += 1) {
23
- const ref = refs[position];
24
- if (ref === null)
25
- continue;
26
- const message = messages[position];
27
- const text = message && "content" in message ? readMessageText(message.content) : "";
28
- rows.push({ index: ref, tokenCount: Math.round(text.length / 4) });
29
- }
30
- return rows;
31
- }
32
- /**
33
- * The selection a compress call covers, in ref space. Exported because the
34
- * command handlers and the compress tool both build it, and it must be built
35
- * the same way in both places or a range validated against one shape is applied
36
- * against another.
37
- */
38
- export function selectionFor(refs, startIndex, endIndex) {
39
- const indices = [];
40
- for (const ref of refs) {
41
- if (ref !== null && ref >= startIndex && ref <= endIndex)
42
- indices.push(ref);
43
- }
44
- return {
45
- startIdx: startIndex,
46
- endIdx: endIndex,
47
- indices,
48
- requiredBlockIds: [],
49
- tokenByIndex: new Map(),
50
- };
51
- }
52
- /**
53
- * The ref snapshot from the last prompt pass, keyed by session.
54
- *
55
- * The compress tool validates its boundaries against this, and it cannot build
56
- * it: all it sees is what the model passed in its arguments. The handler is the
57
- * one that knows the transcript, so it publishes the snapshot here and the tool
58
- * reads it. Process-wide and keyed by session for the same reason `state.ts` is
59
- * - one object per session, shared by the handler and every tool call.
60
- */
61
- /** Marks the compress instructions so a second pass does not append them twice. */
62
- const COMPRESS_PROMPT_MARK = "COMPRESSED BLOCK PLACEHOLDERS";
63
- const lastSnapshot = new Map();
64
- export function snapshotFor(sessionId) {
65
- return lastSnapshot.get(sessionId) ?? [];
66
- }
67
- /**
68
- * Forget a session's snapshot.
69
- *
70
- * `lastSnapshot` is process-wide and one entry per session, and the handler
71
- * only ever adds to it. A session that has been replaced has no reader left for
72
- * its snapshot, so keeping it holds a copy of that session's whole transcript
73
- * for the rest of the process.
74
- */
75
- export function dropSnapshot(sessionId) {
76
- lastSnapshot.delete(sessionId);
77
- }
78
- /**
79
- * The session the handler last ran for.
80
- *
81
- * The compress tool is registered once, at session creation, before any request
82
- * has happened - so it cannot capture a session id in a closure. It reads the
83
- * live one here instead. One process runs one main session, and `/new` or
84
- * `/resume` replaces it, so "the last one the handler saw" is the right answer
85
- * rather than a compromise.
86
- */
87
- let activeSessionId;
88
- export function activeDcpSession() {
89
- return activeSessionId;
90
- }
91
- export function createDcpExtension(options = {}) {
92
- return (hxf) => {
93
- hxf.on("context_with_system", async (event, ctx) => {
94
- // `ctx` carries a fresh manager per session, so the id is read here
95
- // rather than cached at registration: `/new` and `/resume` swap sessions
96
- // and each needs its own state file.
97
- //
98
- // A runner built without a session manager hands back `undefined` here,
99
- // and a throw inside this handler is swallowed by the SDK — the extension
100
- // then does nothing at all, silently, on every request. That is the one
101
- // failure mode worse than not being registered, because nothing reports
102
- // it. Falling back to a fixed key keeps the transform working; the state
103
- // is per process either way.
104
- const sessionId = ctx?.sessionManager?.getSessionId() ?? "dcp-sessionless";
105
- const state = getDcpState(sessionId);
106
- if (state.enabled === false)
107
- return undefined;
108
- const messages = event.messages;
109
- const head = messages[0];
110
- // The provider reads the prompt from index 0; without it there is nothing
111
- // to rewrite, and rewriting would reorder the transcript.
112
- if (!head || head.role !== "system")
113
- return undefined;
114
- // Refs first: every later step is expressed in ref space, and the
115
- // compress tool validates against these very numbers.
116
- assignMessageRefs(state.messageIds, messages);
117
- const refs = refIndicesOf(state.messageIds, messages);
118
- // Published before anything can fail below: the compress tool reads this
119
- // to resolve the boundaries the model quotes, and a stale snapshot is a
120
- // wrong boundary rather than a missing one.
121
- lastSnapshot.set(sessionId, snapshotOf(refs, messages));
122
- activeSessionId = sessionId;
123
- // Carrier blocks are dropped and their summaries put in place before any
124
- // pruning, so a pruned result can never be folded into a block.
125
- //
126
- // The model's own text is cleaned of any marker it imitated BEFORE the
127
- // summaries go in: a summary legitimately carries its own `<dcp-message-id>b1</dcp-message-id>`
128
- // and stripping after this point would delete the tag the block placeholders
129
- // resolve through. Cleaning here also stops the pattern being taught back to
130
- // the model on the next turn, which is how it spread in the first place.
131
- const authored = stripModelMarkers(messages);
132
- let working = filterCompressedRanges(authored, activeCompressedRanges(state), state.messageIds);
133
- // Big results lose their middle before anything is dropped whole: the top of
134
- // a build log names the command, the bottom carries the error, and only the
135
- // middle is noise. Pruning by age alone threw away both ends.
136
- const middle = applyMiddlePrune(working);
137
- if (middle.prunedResults > 0)
138
- working = [...middle.messages];
139
- const keep = options.keepSet?.() ?? { files: [], identifiers: [] };
140
- const plan = planPrune(working, keep);
141
- if (plan.ops.length > 0) {
142
- working = applyPrune(working, plan.ops);
143
- for (const op of plan.ops) {
144
- state.prune.tools[op.callId] = (state.prune.tools[op.callId] ?? 0) + op.savedChars;
145
- }
146
- state.stats.totalPruneTokens = Object.values(state.prune.tools).reduce((sum, saved) => sum + saved, 0);
147
- }
148
- // A block created by the compress tool stored only its summary. The
149
- // protected text - the user's own words, the <protect> captures, the
150
- // outputs of todo/goal/context/task - is layered on here, because the
151
- // handler is the only place that holds both the live transcript and the
152
- // ref state the block's coverage is expressed in. The tool cannot do it:
153
- // `CompressToolDeps.messages()` is a ref snapshot, not the transcript.
154
- //
155
- // It runs on every pass and is idempotent, so a block created mid-run is
156
- // enriched on the very next request without any hook back into the tool.
157
- for (const block of state.prune.messages.blocks) {
158
- if (block.active && !block.protectedEnriched) {
159
- enrichBlockSummary(state, block, messages);
160
- }
161
- }
162
- saveDcpState(sessionId);
163
- // The compress instructions go into the system prompt, which is where the
164
- // provider reads the prompt from. `prompts.ts` had no caller before this,
165
- // so the model was offered a compress tool without ever being told the
166
- // placeholder form `(bN)`, what to keep, or that user intent must be
167
- // quoted rather than summarised - the one rule the whole protected-content
168
- // path exists to serve.
169
- //
170
- // Appended to the carrier block's text, which changes only when a block is
171
- // created: the text is otherwise byte-identical between turns, so it does
172
- // not invalidate the cacheable prefix the way a counter would.
173
- const withPrompt = working.map((message, position) => {
174
- if (position !== 0 || !("content" in message))
175
- return message;
176
- const body = readMessageText(message.content);
177
- if (body.includes(COMPRESS_PROMPT_MARK))
178
- return message;
179
- return { ...message, content: writeMessageText(message.content, `${body}\n\n${rangePrompt()}`) };
180
- });
181
- return { messages: tagged(withPrompt, state.messageIds) };
182
- });
183
- };
184
- }
185
- /**
186
- * Remove any ref marker from text the model authored, before the prompt is built.
187
- *
188
- * The refs are how the model names a message in a `compress` call, and it copies
189
- * the pattern it sees. An imitated marker is a ref the model guessed, and it
190
- * survives into the next request and into the stored transcript, where it reads
191
- * as machinery. Cleaning the model's own text is safe at exactly this point in
192
- * the pipeline: the summaries that carry a legitimate block tag are inserted
193
- * afterwards, so their tags are never touched.
194
- */
195
- function stripModelMarkers(messages) {
196
- return messages.map((message) => {
197
- // Only the model's own output can contain an imitation. A user message is
198
- // the user's words, and a tool result is produced here.
199
- if (message.role !== "assistant" || !("content" in message))
200
- return message;
201
- const cleaned = withCleanedText(message.content);
202
- return cleaned === message.content ? message : { ...message, content: cleaned };
203
- });
204
- }
205
- /**
206
- * Strip markers from a message's content, preserving its shape.
207
- *
208
- * Returns `unknown` for the same reason `withTaggedText` does: the SDK's content
209
- * union is wider than the four roles this touches, so the value is built here
210
- * and asserted once at the call site rather than narrowed by hand.
211
- */
212
- function withCleanedText(content) {
213
- if (typeof content === "string")
214
- return stripDcpMarkers(content);
215
- if (!Array.isArray(content))
216
- return content;
217
- let changed = false;
218
- const parts = content.map((part) => {
219
- if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text")
220
- return part;
221
- const text = "text" in part && typeof part.text === "string" ? part.text : "";
222
- const cleaned = stripDcpMarkers(text);
223
- if (cleaned === text)
224
- return part;
225
- changed = true;
226
- return { ...part, text: cleaned };
227
- });
228
- return changed ? parts : content;
229
- }
230
- /**
231
- * Append each message's ref tag to its own content.
232
- *
233
- * The model has to see the refs to use them — `compress` takes `m0007` in its
234
- * arguments, and a number the prompt never showed cannot be quoted. The tag
235
- * rides the message it names rather than the system prompt, so it stays
236
- * attached through a rewind and does not invalidate the cacheable prompt
237
- * prefix on every turn.
238
- *
239
- * Unref'd positions (the system message, anything the SDK added) pass through
240
- * untouched. `working` may be shorter than the ref list after a filter, so the
241
- * tag is placed by walking `refs` against the array positions of the messages
242
- * that actually survived.
243
- */
244
- function tagged(messages, idState) {
245
- const refs = refIndicesOf(idState, messages);
246
- return messages.map((message, position) => {
247
- const ref = refs[position];
248
- if (ref === null || ref === undefined)
249
- return message;
250
- // `AgentMessage` includes an open `CustomAgentMessages` map, so the union
251
- // does not narrow to the four SDK roles on its own. Only those four carry
252
- // `content`, and they are the only ones `refIndicesOf` gave a ref to.
253
- if (!("content" in message))
254
- return message;
255
- const tag = formatMessageIdTag(formatMessageRef(ref));
256
- if (readMessageText(message.content).includes(tag))
257
- return message;
258
- return { ...message, content: withTaggedText(message.content, tag) };
259
- });
260
- }
261
- /**
262
- * Append the ref tag to EVERY text part of a message, so one ref maps to the
263
- * whole message.
264
- *
265
- * The spec states the reason: "the same ID appears in every part of the
266
- * message, so one ID = one whole message". Tagging only the last text part
267
- * leaves a `toolCall` part with no id at all, and a model reading the call
268
- * cannot tell which ref it belongs to. That matters here because `compress`
269
- * takes refs as its arguments: a ref the model cannot attribute to the call in
270
- * front of it is a ref it has to guess.
271
- *
272
- * A `toolCall` part is never rewritten - its fields are the provider's wire
273
- * contract. A message holding only a call therefore gains one extra text part
274
- * to carry the tag, which is the smallest change that keeps the mapping
275
- * visible.
276
- */
277
- function withTaggedText(content, tag) {
278
- if (typeof content === "string")
279
- return content + tag;
280
- if (!Array.isArray(content))
281
- return content;
282
- let taggedAny = false;
283
- const parts = content.map((part) => {
284
- if (!part || typeof part !== "object" || !("type" in part))
285
- return part;
286
- if (part.type !== "text")
287
- return part;
288
- taggedAny = true;
289
- // After the `in` guard TypeScript knows `type` exists; `text` is still
290
- // unknown, so it is read through a second guard rather than an assertion.
291
- const existing = "text" in part && typeof part.text === "string" ? part.text : "";
292
- return { ...part, text: existing + tag };
293
- });
294
- return taggedAny ? parts : [...parts, { type: "text", text: tag.trimStart() }];
295
- }
package/dist/dcp/dedup.js DELETED
@@ -1,93 +0,0 @@
1
- /**
2
- * Deduplication strategy: group tool calls by signature (tool name +
3
- * normalised parameters, keys sorted), keep the most recent occurrence of each
4
- * group, mark the rest as prunable.
5
- *
6
- * Ported from Opencode-DCP `lib/strategies/deduplication.ts`. The port is pure
7
- * over a message array: DCP mutated `state.prune.tools` in place, here the
8
- * marked tool call ids come back as a `Set` and the caller decides when to
9
- * apply them. Protected tools (hard-exception tools by default, plus an
10
- * optional protected file list such as the plan file) are never grouped, so
11
- * they can never be marked.
12
- *
13
- * Pure module: no I/O, no session state, never mutates `messages`.
14
- */
15
- import { HARD_EXCEPTION_TOOLS, isToolProtected } from "./protected.js";
16
- /**
17
- * Signature of a tool call: tool name plus parameters with `undefined`/`null`
18
- * fields dropped and all object keys sorted recursively, so key order never
19
- * affects the signature and a differing field always does.
20
- */
21
- export function createToolSignature(tool, parameters) {
22
- if (parameters === undefined || parameters === null)
23
- return tool;
24
- return `${tool}::${JSON.stringify(sortObjectKeys(normalizeParameters(parameters)))}`;
25
- }
26
- /** Drop `undefined`/`null` object fields (volatile noise); arrays pass through. */
27
- function normalizeParameters(params) {
28
- if (typeof params !== "object" || params === null)
29
- return params;
30
- if (Array.isArray(params))
31
- return params;
32
- const normalized = {};
33
- for (const [key, value] of Object.entries(params)) {
34
- if (value !== undefined && value !== null) {
35
- normalized[key] = value;
36
- }
37
- }
38
- return normalized;
39
- }
40
- /** Recursively sort object keys so JSON.stringify is order-independent. */
41
- function sortObjectKeys(value) {
42
- if (typeof value !== "object" || value === null)
43
- return value;
44
- if (Array.isArray(value))
45
- return value.map(sortObjectKeys);
46
- const sorted = {};
47
- for (const key of Object.keys(value).sort()) {
48
- sorted[key] = sortObjectKeys(value[key]);
49
- }
50
- return sorted;
51
- }
52
- /**
53
- * The tool call ids of every duplicate occurrence — all but the last of each
54
- * signature group, in transcript order. Calls without a tool result yet are
55
- * not considered (they are still in flight; DCP dedupes executed calls only).
56
- */
57
- export function markDuplicates(messages, options) {
58
- const protectedTools = [...(options?.protectedTools ?? HARD_EXCEPTION_TOOLS)];
59
- const protectedFiles = options?.protectedFiles ? [...options.protectedFiles] : [];
60
- // Only executed calls participate: a call counts once its tool result exists.
61
- const executed = new Set();
62
- for (const message of messages) {
63
- if (message.role === "toolResult")
64
- executed.add(message.toolCallId);
65
- }
66
- const groups = new Map();
67
- for (const message of messages) {
68
- if (message.role !== "assistant")
69
- continue;
70
- for (const part of message.content) {
71
- if (part.type !== "toolCall" || !executed.has(part.id))
72
- continue;
73
- if (isToolProtected(part.name, part.arguments, protectedTools, protectedFiles))
74
- continue;
75
- const signature = createToolSignature(part.name, part.arguments);
76
- const group = groups.get(signature);
77
- if (group) {
78
- group.push(part.id);
79
- }
80
- else {
81
- groups.set(signature, [part.id]);
82
- }
83
- }
84
- }
85
- const marks = new Set();
86
- for (const ids of groups.values()) {
87
- if (ids.length > 1) {
88
- for (const id of ids.slice(0, -1))
89
- marks.add(id);
90
- }
91
- }
92
- return marks;
93
- }
package/dist/dcp/ids.js DELETED
@@ -1,189 +0,0 @@
1
- /**
2
- * DCP message and block identity: `mNNNN` raw refs, `bN` block refs, both the
3
- * `xml` tag format and the `compact` `@7@` format, plus assignment of refs to
4
- * harness transcript messages.
5
- *
6
- * Ported from Opencode-DCP `lib/message-ids.ts`, adapted to one hard fact about
7
- * this host: harness messages carry no id. The system message and any non
8
- * (user | assistant | toolResult) role get no ref at all; every other message is
9
- * keyed by `toolResult` → toolCallId, `user`/`assistant` → `role:timestamp#n`
10
- * where `n` is how many messages with that same role+timestamp precede it in the
11
- * array. That key is stable across requests for an append-only transcript, so
12
- * refs assigned once stay attached to the same message.
13
- *
14
- * `refIndicesOf` is the single skip-rule source of truth: ref assignment, the
15
- * compress snapshot, and `filterCompressedRanges` all go through it, so the
16
- * three can never disagree about which message a ref number means.
17
- *
18
- * Pure module: no I/O, no session state, never mutates `messages`.
19
- */
20
- const MESSAGE_REF_REGEX = /^m(\d{4})$/;
21
- const BLOCK_REF_REGEX = /^b([1-9]\d*)$/;
22
- const COMPACT_MESSAGE_REGEX = /^@([1-9]\d*)@$/;
23
- const COMPACT_BLOCK_REGEX = /^@b([1-9]\d*)@$/;
24
- const MESSAGE_ID_TAG_NAME = "dcp-message-id";
25
- const MESSAGE_REF_WIDTH = 4;
26
- const MESSAGE_REF_MIN_INDEX = 1;
27
- export const MESSAGE_REF_MAX_INDEX = 9999;
28
- export function createMessageIdState() {
29
- return { nextRef: MESSAGE_REF_MIN_INDEX, keyToRef: {}, refToKey: {} };
30
- }
31
- export function formatMessageRef(index, format = "xml") {
32
- const max = format === "compact" ? Number.MAX_SAFE_INTEGER : MESSAGE_REF_MAX_INDEX;
33
- if (!Number.isSafeInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > max) {
34
- throw new Error(`Message ID index out of bounds: ${index}. Supported range is 1-${max}.`);
35
- }
36
- return format === "compact" ? `@${index}@` : `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
37
- }
38
- export function formatBlockRef(blockId, format = "xml") {
39
- if (!Number.isInteger(blockId) || blockId < 1) {
40
- throw new Error(`Invalid block ID: ${blockId}`);
41
- }
42
- return format === "compact" ? `@b${blockId}@` : `b${blockId}`;
43
- }
44
- export function parseMessageRef(ref, format = "xml") {
45
- const normalized = ref.trim().toLowerCase();
46
- const match = normalized.match(format === "compact" ? COMPACT_MESSAGE_REGEX : MESSAGE_REF_REGEX);
47
- if (!match)
48
- return null;
49
- const index = Number.parseInt(match[1], 10);
50
- if (!Number.isSafeInteger(index))
51
- return null;
52
- const max = format === "compact" ? Number.MAX_SAFE_INTEGER : MESSAGE_REF_MAX_INDEX;
53
- if (index < MESSAGE_REF_MIN_INDEX || index > max)
54
- return null;
55
- return index;
56
- }
57
- export function parseBlockRef(ref, format = "xml") {
58
- const normalized = ref.trim().toLowerCase();
59
- const match = normalized.match(format === "compact" ? COMPACT_BLOCK_REGEX : BLOCK_REF_REGEX);
60
- if (!match)
61
- return null;
62
- const id = Number.parseInt(match[1], 10);
63
- return Number.isSafeInteger(id) ? id : null;
64
- }
65
- /** Parse either a message ref or a block ref in the given format. */
66
- export function parseBoundaryId(id, format = "xml") {
67
- const normalized = id.trim().toLowerCase();
68
- const messageIndex = parseMessageRef(normalized, format);
69
- if (messageIndex !== null) {
70
- return { kind: "message", ref: formatMessageRef(messageIndex, format), index: messageIndex };
71
- }
72
- const blockId = parseBlockRef(normalized, format);
73
- if (blockId !== null) {
74
- return { kind: "compressed-block", ref: formatBlockRef(blockId, format), blockId };
75
- }
76
- return null;
77
- }
78
- /** Escape a value for use inside a double-quoted XML attribute. */
79
- export function escapeXmlAttribute(value) {
80
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
81
- }
82
- /**
83
- * Remove any ref marker a model wrote into its own text.
84
- *
85
- * The tags exist so the model can quote a ref in a `compress` call, and they are
86
- * added to a per-request copy of the transcript. A model that has read that
87
- * pattern can imitate it, and an imitated marker is wrong twice over: it names a
88
- * ref the model guessed, and it reaches the screen and the stored transcript,
89
- * where it reads as machinery the user was never meant to see. Observed in real
90
- * sessions as an answer ending in `<dcp-message-id>m0036</dcp-message-id>`.
91
- *
92
- * Only the xml form is stripped. The compact form is a bare number in `@` signs
93
- * and occurs in ordinary prose often enough that removing it would eat text.
94
- */
95
- export function stripDcpMarkers(text) {
96
- if (!text.includes("<dcp-message-id") && !text.includes("</dcp-message-id"))
97
- return text;
98
- return (text
99
- // A complete marker, with any attributes the writer invented.
100
- .replace(/<dcp-message-id(?:\s[^>]*)?>[\s\S]*?<\/dcp-message-id>/gi, "")
101
- // An opener left unpaired — a marker cut off mid-stream, or one whose closing
102
- // tag never arrived. The ref it carries goes with it; a bare `m0012` left on
103
- // screen names a message the user cannot act on and reads as a typo.
104
- .replace(/<dcp-message-id(?:\s[^>]*)?>[mb]?\d{0,6}/gi, "")
105
- // Whatever tag text is still standing, opener or closer, including a tag the
106
- // stream cut before its `>` arrived.
107
- .replace(/<\/?dcp-message-id(?:\s[^>]*)?>?/gi, ""));
108
- }
109
- /**
110
- * Render the id tag appended to a message's output.
111
- * xml: `\n<dcp-message-id k="v">m0007</dcp-message-id>` (attributes sorted)
112
- * compact: `\n@7@ [priority]`, or `\n@blocked@` for the literal "BLOCKED".
113
- */
114
- export function formatMessageIdTag(ref, attributes, format = "xml") {
115
- if (format === "compact") {
116
- if (ref === "BLOCKED")
117
- return "\n@blocked@";
118
- const priority = attributes?.priority;
119
- return `\n${ref}${priority ? ` [${priority}]` : ""}`;
120
- }
121
- const serializedAttributes = Object.entries(attributes || {})
122
- .sort(([left], [right]) => left.localeCompare(right))
123
- .map(([name, value]) => {
124
- if (name.trim().length === 0 || typeof value !== "string" || value.length === 0)
125
- return "";
126
- return ` ${name}="${escapeXmlAttribute(value)}"`;
127
- })
128
- .join("");
129
- return `\n<${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`;
130
- }
131
- /** Roles whose messages receive a ref. System and custom roles never do. */
132
- function isRefEligible(message) {
133
- return message.role === "user" || message.role === "assistant" || message.role === "toolResult";
134
- }
135
- /**
136
- * Compute the ref key for every position in the array in one pass: tool
137
- * results keyed by their unique call id, user/assistant messages by
138
- * `role:timestamp#occurrence` where `occurrence` counts earlier messages with
139
- * the same role+timestamp (harness messages have no id), `null` for roles that
140
- * never get a ref. Shared by `assignMessageRefs` and `refIndicesOf` so both
141
- * apply byte-identical keys.
142
- */
143
- function refKeysOf(messages) {
144
- const seen = new Map();
145
- return messages.map((message) => {
146
- if (!isRefEligible(message))
147
- return null;
148
- const base = message.role === "toolResult" ? `toolResult:${message.toolCallId}` : `${message.role}:${message.timestamp}`;
149
- const occurrence = seen.get(base) ?? 0;
150
- seen.set(base, occurrence + 1);
151
- return message.role === "toolResult" ? base : `${base}#${occurrence}`;
152
- });
153
- }
154
- /**
155
- * Assign a ref to every message that does not have one yet. Mutates `state`
156
- * only — never `messages` (tagging outputs is a separate, later step). Returns
157
- * how many refs were newly allocated.
158
- */
159
- export function assignMessageRefs(state, messages) {
160
- let assigned = 0;
161
- for (const key of refKeysOf(messages)) {
162
- if (key === null || state.keyToRef[key] !== undefined)
163
- continue;
164
- const ref = allocateNextRef(state);
165
- state.keyToRef[key] = ref;
166
- state.refToKey[String(ref)] = key;
167
- assigned++;
168
- }
169
- return assigned;
170
- }
171
- function allocateNextRef(state) {
172
- let candidate = Number.isInteger(state.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.nextRef) : MESSAGE_REF_MIN_INDEX;
173
- while (candidate <= MESSAGE_REF_MAX_INDEX) {
174
- if (state.refToKey[String(candidate)] === undefined) {
175
- state.nextRef = candidate + 1;
176
- return candidate;
177
- }
178
- candidate++;
179
- }
180
- throw new Error(`Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`);
181
- }
182
- /**
183
- * Ref number for every position in the array, `null` for unassigned or
184
- * ineligible messages. The one mapping used by ref assignment, the compress
185
- * snapshot, and `filterCompressedRanges`.
186
- */
187
- export function refIndicesOf(state, messages) {
188
- return refKeysOf(messages).map((key) => (key === null ? null : (state.keyToRef[key] ?? null)));
189
- }
package/dist/dcp/index.js DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * DCP's public surface, re-exported from one path.
3
- *
4
- * `session.ts` and the command handlers import from here so the module layout
5
- * stays free to change: the nine files under `src/dcp/` are an implementation
6
- * detail, this is the contract.
7
- *
8
- * DCP is dynamic context pruning. Before each LLM call it rebuilds the copy of
9
- * the transcript that goes to the provider: it tags every message with a stable
10
- * ref (`m0007`), replaces stale tool output with fixed placeholders, and folds
11
- * consumed ranges into compressed blocks. The session's own transcript is never
12
- * touched — every function here is pure over an array, and the handler returns
13
- * a new one.
14
- */
15
- export { MESSAGE_REF_MAX_INDEX, assignMessageRefs, createMessageIdState, escapeXmlAttribute, formatBlockRef, formatMessageIdTag, formatMessageRef, parseBlockRef, parseBoundaryId, parseMessageRef, refIndicesOf, } from "./ids.js";
16
- export { HARD_EXCEPTION_TOOLS, getFilePathsFromParameters, isFilePathProtected, isToolNameProtected, isToolProtected, matchesGlob, normalizePath } from "./protected.js";
17
- export { createToolSignature, markDuplicates } from "./dedup.js";
18
- export { BIG_OUTPUT_CHARS, PRUNED_QUESTION_INPUT_REPLACEMENT, PRUNED_TOOL_ERROR_INPUT_REPLACEMENT, PRUNED_TOOL_OUTPUT_REPLACEMENT, QUESTION_TOOLS, applyPrune, filterCompressedRanges, planPrune, pruneToolErrors, pruneToolInputs, pruneToolOutputs, } from "./prune.js";
19
- export { COMPRESSED_BLOCK_HEADER, activeCompressedRanges, allocateBlockId, allocateRunId, applyCompressionState, wrapCompressedSummary, } from "./compress-state.js";
20
- export { compressRanges, createCompressTool, parseBlockPlaceholders, resolveRanges, validateArgs, validateNonOverlapping, validateSummaryPlaceholders, } from "./compress.js";
21
- export { rangePrompt } from "./prompts.js";
22
- export { appendProtectedPromptInfo, appendProtectedTools, appendProtectedUserMessages, extractProtectedPromptInfo } from "./protected-content.js";
23
- export { createDcpState, dcpStatePath, dropDcpState, getDcpState, readConfigEnabled, resolveDcpEnabled, saveDcpState } from "./state.js";
24
- export { createDcpExtension, selectionFor, snapshotOf, snapshotFor, dropSnapshot, activeDcpSession } from "./dcp-extension.js";
25
- export { DCP_USAGE, runDcpCommand } from "./commands.js";