howtofixcli 0.7.7 → 0.7.9

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 (152) 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-63ZCQDXH.js +6 -0
  5. package/dist/chunk-6FUWMLWO.js +1 -0
  6. package/dist/chunk-7CHDNTH6.js +1 -0
  7. package/dist/chunk-ACOMNDUL.js +185 -0
  8. package/dist/chunk-AEVPET7X.js +2 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-BHVBO4SC.js +1 -0
  11. package/dist/chunk-FPXVDP32.js +8 -0
  12. package/dist/chunk-GIUYIKH3.js +109 -0
  13. package/dist/chunk-GQCAJ56I.js +5 -0
  14. package/dist/chunk-GXB7RZHH.js +1 -0
  15. package/dist/chunk-IGEY3AWF.js +1 -0
  16. package/dist/chunk-ILDDDLRC.js +1 -0
  17. package/dist/chunk-JFYVRWJI.js +2 -0
  18. package/dist/chunk-MVLCGU3G.js +1 -0
  19. package/dist/chunk-N345KWWK.js +1 -0
  20. package/dist/chunk-NBQFPCRT.js +1 -0
  21. package/dist/chunk-OIPSLIZE.js +5 -0
  22. package/dist/chunk-ORJ2QZBG.js +3 -0
  23. package/dist/chunk-PCALBZOO.js +2 -0
  24. package/dist/chunk-PDNBGUYP.js +2 -0
  25. package/dist/chunk-S656RE4A.js +1 -0
  26. package/dist/chunk-TSEKPGK4.js +1 -0
  27. package/dist/chunk-U6CI6KXN.js +34 -0
  28. package/dist/chunk-UTYFLLSM.js +1 -0
  29. package/dist/chunk-V3R4FYTJ.js +1 -0
  30. package/dist/chunk-V656XVXV.js +8 -0
  31. package/dist/chunk-WKUJ7CFO.js +1 -0
  32. package/dist/chunk-WOT6VMZA.js +1 -0
  33. package/dist/chunk-WUPU6LTD.js +4 -0
  34. package/dist/chunk-WX3V5P7U.js +5 -0
  35. package/dist/chunk-ZKZ6WCI3.js +2 -0
  36. package/dist/main.js +94 -536
  37. package/package.json +15 -62
  38. package/dist/account-credential.js +0 -136
  39. package/dist/account-credential.test.js +0 -38
  40. package/dist/activity.js +0 -124
  41. package/dist/agent-defs.js +0 -237
  42. package/dist/agent-limiter.js +0 -68
  43. package/dist/agents.js +0 -554
  44. package/dist/anim.js +0 -361
  45. package/dist/args.js +0 -228
  46. package/dist/at-file.js +0 -28
  47. package/dist/brand-extension.js +0 -87
  48. package/dist/brand.js +0 -76
  49. package/dist/browser.js +0 -58
  50. package/dist/cache-report.js +0 -139
  51. package/dist/checkpoint.js +0 -437
  52. package/dist/claims.js +0 -167
  53. package/dist/cli-auth.js +0 -142
  54. package/dist/cli-checkin.js +0 -116
  55. package/dist/cli-credentials.js +0 -254
  56. package/dist/clipboard.js +0 -149
  57. package/dist/collapse.js +0 -129
  58. package/dist/commands.js +0 -301
  59. package/dist/compaction-guard.js +0 -81
  60. package/dist/context-extension.js +0 -17
  61. package/dist/dcp/commands.js +0 -172
  62. package/dist/dcp/compress-state.js +0 -139
  63. package/dist/dcp/compress.js +0 -329
  64. package/dist/dcp/dcp-extension.js +0 -288
  65. package/dist/dcp/dedup.js +0 -93
  66. package/dist/dcp/ids.js +0 -189
  67. package/dist/dcp/index.js +0 -25
  68. package/dist/dcp/prompts.js +0 -68
  69. package/dist/dcp/protected-content.js +0 -221
  70. package/dist/dcp/protected.js +0 -161
  71. package/dist/dcp/prune.js +0 -418
  72. package/dist/dcp/state.js +0 -176
  73. package/dist/device-id.js +0 -90
  74. package/dist/device-id.test.js +0 -23
  75. package/dist/errors.js +0 -63
  76. package/dist/extensions.js +0 -48
  77. package/dist/fatal.js +0 -35
  78. package/dist/feed.js +0 -498
  79. package/dist/fs-atomic.js +0 -54
  80. package/dist/gateway-errors.js +0 -90
  81. package/dist/git.js +0 -230
  82. package/dist/goal-extension.js +0 -88
  83. package/dist/goal.js +0 -222
  84. package/dist/hooks-extension.js +0 -176
  85. package/dist/hooks.js +0 -443
  86. package/dist/howtofix-account.js +0 -715
  87. package/dist/hud-line.js +0 -153
  88. package/dist/hwid.js +0 -130
  89. package/dist/interrupt.js +0 -60
  90. package/dist/json-events.js +0 -50
  91. package/dist/level-style.js +0 -112
  92. package/dist/loop-guard.js +0 -95
  93. package/dist/lsp-tools.js +0 -32
  94. package/dist/lsp.js +0 -347
  95. package/dist/mcp-config.js +0 -85
  96. package/dist/mcp-lifecycle.js +0 -147
  97. package/dist/model-catalog.js +0 -217
  98. package/dist/model-resolve.js +0 -65
  99. package/dist/notes.js +0 -78
  100. package/dist/oauth-login.js +0 -568
  101. package/dist/paths.js +0 -61
  102. package/dist/permissions.js +0 -298
  103. package/dist/plan-mode.js +0 -363
  104. package/dist/preset-command.js +0 -227
  105. package/dist/preset-extension.js +0 -35
  106. package/dist/preset-mcp.js +0 -292
  107. package/dist/presets.js +0 -727
  108. package/dist/prompt-files.js +0 -103
  109. package/dist/prompt-text.js +0 -41
  110. package/dist/proxy.js +0 -137
  111. package/dist/quality/ast.js +0 -196
  112. package/dist/quality/cognitive.js +0 -83
  113. package/dist/quality/cyclomatic.js +0 -30
  114. package/dist/quality/halstead.js +0 -83
  115. package/dist/quality/index.js +0 -18
  116. package/dist/quality/length.js +0 -3
  117. package/dist/quality/maintainability.js +0 -15
  118. package/dist/quality/nesting.js +0 -57
  119. package/dist/quality/report.js +0 -803
  120. package/dist/quality/smells.js +0 -67
  121. package/dist/quality-tool.js +0 -63
  122. package/dist/read-only-commands.js +0 -597
  123. package/dist/repo-map.js +0 -292
  124. package/dist/rtk.js +0 -193
  125. package/dist/sandbox.js +0 -105
  126. package/dist/schema.js +0 -214
  127. package/dist/session-lifecycle.js +0 -19
  128. package/dist/session-retention.js +0 -111
  129. package/dist/session.js +0 -446
  130. package/dist/sessions.js +0 -100
  131. package/dist/settings.js +0 -148
  132. package/dist/skills.js +0 -35
  133. package/dist/slop-tool.js +0 -96
  134. package/dist/spend-cap.js +0 -43
  135. package/dist/subagent.js +0 -171
  136. package/dist/tar.js +0 -146
  137. package/dist/theme.js +0 -79
  138. package/dist/thinking.js +0 -45
  139. package/dist/todo.js +0 -114
  140. package/dist/tool-loadout.js +0 -97
  141. package/dist/tool-render.js +0 -769
  142. package/dist/tools.js +0 -176
  143. package/dist/ui.js +0 -4751
  144. package/dist/update.js +0 -533
  145. package/dist/usage-segment.js +0 -69
  146. package/dist/web-tools.js +0 -46
  147. package/dist/web.js +0 -265
  148. package/dist/welcome.js +0 -179
  149. package/dist/workflow-engine.js +0 -246
  150. package/dist/workflow-progress.js +0 -446
  151. package/dist/workflow-view.js +0 -195
  152. package/dist/workflow.js +0 -349
@@ -1,288 +0,0 @@
1
- import { assignMessageRefs, formatMessageIdTag, formatMessageRef, refIndicesOf, stripDcpMarkers } from "./ids.js";
2
- import { applyPrune, filterCompressedRanges, planPrune } from "./prune.js";
3
- import { activeCompressedRanges } from "./compress-state.js";
4
- import { rangePrompt } from "./prompts.js";
5
- import { enrichBlockSummary } from "./protected-content.js";
6
- import { getDcpState, saveDcpState } from "./state.js";
7
- import { readMessageText, writeMessageText } from "../prompt-text.js";
8
- /**
9
- * The ordered ref snapshot the compress tool validates against, with each row's
10
- * size estimate.
11
- *
12
- * `tokenCount` is not decoration: `applyCompressionState` sums it over the newly
13
- * covered refs to report `compressedTokens`, so a snapshot without it makes
14
- * every block claim to have saved nothing. The estimate is the same
15
- * length-over-four fallback the rest of this module uses, for the same reason -
16
- * DCP counts with a bundled tokenizer and this repo does not add a dependency
17
- * for one.
18
- */
19
- export function snapshotOf(refs, messages) {
20
- const rows = [];
21
- for (let position = 0; position < refs.length; position += 1) {
22
- const ref = refs[position];
23
- if (ref === null)
24
- continue;
25
- const message = messages[position];
26
- const text = message && "content" in message ? readMessageText(message.content) : "";
27
- rows.push({ index: ref, tokenCount: Math.round(text.length / 4) });
28
- }
29
- return rows;
30
- }
31
- /**
32
- * The selection a compress call covers, in ref space. Exported because the
33
- * command handlers and the compress tool both build it, and it must be built
34
- * the same way in both places or a range validated against one shape is applied
35
- * against another.
36
- */
37
- export function selectionFor(refs, startIndex, endIndex) {
38
- const indices = [];
39
- for (const ref of refs) {
40
- if (ref !== null && ref >= startIndex && ref <= endIndex)
41
- indices.push(ref);
42
- }
43
- return {
44
- startIdx: startIndex,
45
- endIdx: endIndex,
46
- indices,
47
- requiredBlockIds: [],
48
- tokenByIndex: new Map(),
49
- };
50
- }
51
- /**
52
- * The ref snapshot from the last prompt pass, keyed by session.
53
- *
54
- * The compress tool validates its boundaries against this, and it cannot build
55
- * it: all it sees is what the model passed in its arguments. The handler is the
56
- * one that knows the transcript, so it publishes the snapshot here and the tool
57
- * reads it. Process-wide and keyed by session for the same reason `state.ts` is
58
- * - one object per session, shared by the handler and every tool call.
59
- */
60
- /** Marks the compress instructions so a second pass does not append them twice. */
61
- const COMPRESS_PROMPT_MARK = "COMPRESSED BLOCK PLACEHOLDERS";
62
- const lastSnapshot = new Map();
63
- export function snapshotFor(sessionId) {
64
- return lastSnapshot.get(sessionId) ?? [];
65
- }
66
- /**
67
- * Forget a session's snapshot.
68
- *
69
- * `lastSnapshot` is process-wide and one entry per session, and the handler
70
- * only ever adds to it. A session that has been replaced has no reader left for
71
- * its snapshot, so keeping it holds a copy of that session's whole transcript
72
- * for the rest of the process.
73
- */
74
- export function dropSnapshot(sessionId) {
75
- lastSnapshot.delete(sessionId);
76
- }
77
- /**
78
- * The session the handler last ran for.
79
- *
80
- * The compress tool is registered once, at session creation, before any request
81
- * has happened - so it cannot capture a session id in a closure. It reads the
82
- * live one here instead. One process runs one main session, and `/new` or
83
- * `/resume` replaces it, so "the last one the handler saw" is the right answer
84
- * rather than a compromise.
85
- */
86
- let activeSessionId;
87
- export function activeDcpSession() {
88
- return activeSessionId;
89
- }
90
- export function createDcpExtension(options = {}) {
91
- return (hxf) => {
92
- hxf.on("context_with_system", async (event, ctx) => {
93
- // `ctx` carries a fresh manager per session, so the id is read here
94
- // rather than cached at registration: `/new` and `/resume` swap sessions
95
- // and each needs its own state file.
96
- //
97
- // A runner built without a session manager hands back `undefined` here,
98
- // and a throw inside this handler is swallowed by the SDK — the extension
99
- // then does nothing at all, silently, on every request. That is the one
100
- // failure mode worse than not being registered, because nothing reports
101
- // it. Falling back to a fixed key keeps the transform working; the state
102
- // is per process either way.
103
- const sessionId = ctx?.sessionManager?.getSessionId() ?? "dcp-sessionless";
104
- const state = getDcpState(sessionId);
105
- if (state.enabled === false)
106
- return undefined;
107
- const messages = event.messages;
108
- const head = messages[0];
109
- // The provider reads the prompt from index 0; without it there is nothing
110
- // to rewrite, and rewriting would reorder the transcript.
111
- if (!head || head.role !== "system")
112
- return undefined;
113
- // Refs first: every later step is expressed in ref space, and the
114
- // compress tool validates against these very numbers.
115
- assignMessageRefs(state.messageIds, messages);
116
- const refs = refIndicesOf(state.messageIds, messages);
117
- // Published before anything can fail below: the compress tool reads this
118
- // to resolve the boundaries the model quotes, and a stale snapshot is a
119
- // wrong boundary rather than a missing one.
120
- lastSnapshot.set(sessionId, snapshotOf(refs, messages));
121
- activeSessionId = sessionId;
122
- // Carrier blocks are dropped and their summaries put in place before any
123
- // pruning, so a pruned result can never be folded into a block.
124
- //
125
- // The model's own text is cleaned of any marker it imitated BEFORE the
126
- // summaries go in: a summary legitimately carries its own `<dcp-message-id>b1</dcp-message-id>`
127
- // and stripping after this point would delete the tag the block placeholders
128
- // resolve through. Cleaning here also stops the pattern being taught back to
129
- // the model on the next turn, which is how it spread in the first place.
130
- const authored = stripModelMarkers(messages);
131
- let working = filterCompressedRanges(authored, activeCompressedRanges(state), state.messageIds);
132
- const keep = options.keepSet?.() ?? { files: [], identifiers: [] };
133
- const plan = planPrune(working, keep);
134
- if (plan.ops.length > 0) {
135
- working = applyPrune(working, plan.ops);
136
- for (const op of plan.ops) {
137
- state.prune.tools[op.callId] = (state.prune.tools[op.callId] ?? 0) + op.savedChars;
138
- }
139
- state.stats.totalPruneTokens = Object.values(state.prune.tools).reduce((sum, saved) => sum + saved, 0);
140
- }
141
- // A block created by the compress tool stored only its summary. The
142
- // protected text - the user's own words, the <protect> captures, the
143
- // outputs of todo/goal/context/task - is layered on here, because the
144
- // handler is the only place that holds both the live transcript and the
145
- // ref state the block's coverage is expressed in. The tool cannot do it:
146
- // `CompressToolDeps.messages()` is a ref snapshot, not the transcript.
147
- //
148
- // It runs on every pass and is idempotent, so a block created mid-run is
149
- // enriched on the very next request without any hook back into the tool.
150
- for (const block of state.prune.messages.blocks) {
151
- if (block.active && !block.protectedEnriched) {
152
- enrichBlockSummary(state, block, messages);
153
- }
154
- }
155
- saveDcpState(sessionId);
156
- // The compress instructions go into the system prompt, which is where the
157
- // provider reads the prompt from. `prompts.ts` had no caller before this,
158
- // so the model was offered a compress tool without ever being told the
159
- // placeholder form `(bN)`, what to keep, or that user intent must be
160
- // quoted rather than summarised - the one rule the whole protected-content
161
- // path exists to serve.
162
- //
163
- // Appended to the carrier block's text, which changes only when a block is
164
- // created: the text is otherwise byte-identical between turns, so it does
165
- // not invalidate the cacheable prefix the way a counter would.
166
- const withPrompt = working.map((message, position) => {
167
- if (position !== 0 || !("content" in message))
168
- return message;
169
- const body = readMessageText(message.content);
170
- if (body.includes(COMPRESS_PROMPT_MARK))
171
- return message;
172
- return { ...message, content: writeMessageText(message.content, `${body}\n\n${rangePrompt()}`) };
173
- });
174
- return { messages: tagged(withPrompt, state.messageIds) };
175
- });
176
- };
177
- }
178
- /**
179
- * Remove any ref marker from text the model authored, before the prompt is built.
180
- *
181
- * The refs are how the model names a message in a `compress` call, and it copies
182
- * the pattern it sees. An imitated marker is a ref the model guessed, and it
183
- * survives into the next request and into the stored transcript, where it reads
184
- * as machinery. Cleaning the model's own text is safe at exactly this point in
185
- * the pipeline: the summaries that carry a legitimate block tag are inserted
186
- * afterwards, so their tags are never touched.
187
- */
188
- function stripModelMarkers(messages) {
189
- return messages.map((message) => {
190
- // Only the model's own output can contain an imitation. A user message is
191
- // the user's words, and a tool result is produced here.
192
- if (message.role !== "assistant" || !("content" in message))
193
- return message;
194
- const cleaned = withCleanedText(message.content);
195
- return cleaned === message.content ? message : { ...message, content: cleaned };
196
- });
197
- }
198
- /**
199
- * Strip markers from a message's content, preserving its shape.
200
- *
201
- * Returns `unknown` for the same reason `withTaggedText` does: the SDK's content
202
- * union is wider than the four roles this touches, so the value is built here
203
- * and asserted once at the call site rather than narrowed by hand.
204
- */
205
- function withCleanedText(content) {
206
- if (typeof content === "string")
207
- return stripDcpMarkers(content);
208
- if (!Array.isArray(content))
209
- return content;
210
- let changed = false;
211
- const parts = content.map((part) => {
212
- if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text")
213
- return part;
214
- const text = "text" in part && typeof part.text === "string" ? part.text : "";
215
- const cleaned = stripDcpMarkers(text);
216
- if (cleaned === text)
217
- return part;
218
- changed = true;
219
- return { ...part, text: cleaned };
220
- });
221
- return changed ? parts : content;
222
- }
223
- /**
224
- * Append each message's ref tag to its own content.
225
- *
226
- * The model has to see the refs to use them — `compress` takes `m0007` in its
227
- * arguments, and a number the prompt never showed cannot be quoted. The tag
228
- * rides the message it names rather than the system prompt, so it stays
229
- * attached through a rewind and does not invalidate the cacheable prompt
230
- * prefix on every turn.
231
- *
232
- * Unref'd positions (the system message, anything the SDK added) pass through
233
- * untouched. `working` may be shorter than the ref list after a filter, so the
234
- * tag is placed by walking `refs` against the array positions of the messages
235
- * that actually survived.
236
- */
237
- function tagged(messages, idState) {
238
- const refs = refIndicesOf(idState, messages);
239
- return messages.map((message, position) => {
240
- const ref = refs[position];
241
- if (ref === null || ref === undefined)
242
- return message;
243
- // `AgentMessage` includes an open `CustomAgentMessages` map, so the union
244
- // does not narrow to the four SDK roles on its own. Only those four carry
245
- // `content`, and they are the only ones `refIndicesOf` gave a ref to.
246
- if (!("content" in message))
247
- return message;
248
- const tag = formatMessageIdTag(formatMessageRef(ref));
249
- if (readMessageText(message.content).includes(tag))
250
- return message;
251
- return { ...message, content: withTaggedText(message.content, tag) };
252
- });
253
- }
254
- /**
255
- * Append the ref tag to EVERY text part of a message, so one ref maps to the
256
- * whole message.
257
- *
258
- * The spec states the reason: "the same ID appears in every part of the
259
- * message, so one ID = one whole message". Tagging only the last text part
260
- * leaves a `toolCall` part with no id at all, and a model reading the call
261
- * cannot tell which ref it belongs to. That matters here because `compress`
262
- * takes refs as its arguments: a ref the model cannot attribute to the call in
263
- * front of it is a ref it has to guess.
264
- *
265
- * A `toolCall` part is never rewritten - its fields are the provider's wire
266
- * contract. A message holding only a call therefore gains one extra text part
267
- * to carry the tag, which is the smallest change that keeps the mapping
268
- * visible.
269
- */
270
- function withTaggedText(content, tag) {
271
- if (typeof content === "string")
272
- return content + tag;
273
- if (!Array.isArray(content))
274
- return content;
275
- let taggedAny = false;
276
- const parts = content.map((part) => {
277
- if (!part || typeof part !== "object" || !("type" in part))
278
- return part;
279
- if (part.type !== "text")
280
- return part;
281
- taggedAny = true;
282
- // After the `in` guard TypeScript knows `type` exists; `text` is still
283
- // unknown, so it is read through a second guard rather than an assertion.
284
- const existing = "text" in part && typeof part.text === "string" ? part.text : "";
285
- return { ...part, text: existing + tag };
286
- });
287
- return taggedAny ? parts : [...parts, { type: "text", text: tag.trimStart() }];
288
- }
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";
@@ -1,68 +0,0 @@
1
- export function rangePrompt(format = "xml") {
2
- const compact = format === "compact";
3
- const message = compact ? "@4@" : "mNNNN";
4
- const block = compact ? "@b1@" : "bN";
5
- const placeholder = compact ? "@b1@" : "(bN)";
6
- return `COMPRESSING A RANGE OF THIS CONVERSATION
7
-
8
- This is what the \`compress\` tool is for, and how a range must be summarised when it is used. Reach for it when the conversation has grown long enough that its older turns cost more than they are worth - never as a way to answer the user, and never on a range they did not ask about. Nothing here is a standing instruction to summarise: it is the contract the tool's output is held to.
9
-
10
- THE SUMMARY
11
- Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings... EVERYTHING that maintains context integrity. This is not a brief note - it is an authoritative record so faithful that the original conversation adds no value.
12
-
13
- USER INTENT FIDELITY
14
- When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
15
- Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
16
-
17
- Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
18
-
19
- COMPRESSED BLOCK PLACEHOLDERS
20
- When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
21
-
22
- - \`${placeholder}\`
23
-
24
- Compressed block sections in context are clearly marked with a header:
25
-
26
- - \`[Compressed conversation section]\`
27
-
28
- ${compact ? "Compressed block IDs look like `@b1@`, distinct from raw message IDs like `@4@`." : "Compressed block IDs always use the `bN` form (never `mNNNN`) and are represented in the same XML metadata tag format."}
29
-
30
- Rules:
31
-
32
- - Include every required block placeholder exactly once.
33
- - Do not invent placeholders for blocks outside the selected range.
34
- - Treat \`${placeholder}\` placeholders as RESERVED TOKENS. Do not emit \`${placeholder}\` text anywhere except intentional placeholders.
35
- - If you need to mention a block in prose, use plain text like \`compressed ${compact ? "block 1" : "bN"}\` (not as a placeholder).
36
- - Preflight check before finalizing: the set of \`${placeholder}\` placeholders in your summary must exactly match the required set, with no duplicates.
37
-
38
- These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
39
-
40
- FLOW PRESERVATION WITH PLACEHOLDERS
41
- When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
42
-
43
- - Treat each placeholder as a stand-in for a full conversation segment, not a short label.
44
- - Ensure transitions before and after each placeholder preserve chronology and causality.
45
- - Do not write text that depends on the placeholder staying literal (for example, "as noted in \`${compact ? "@b2@" : "(b2)"}\`").
46
- - Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
47
-
48
- BOUNDARY IDS
49
- You specify boundaries by ID using the injected IDs visible in the conversation:
50
-
51
- - \`${message}\` IDs identify raw messages
52
- - \`${block}\` IDs identify previously compressed blocks
53
-
54
- ${compact ? "Each message has an ID like `@4@`. Copy the whole marker, including both `@` characters, into `startId` and `endId`." : "Each message has an ID inside XML metadata tags like `<dcp-message-id>...</dcp-message-id>`."}
55
- The same ID tag appears in every tool output of the message it belongs to — each unique ID identifies one complete message.
56
- Treat these tags as boundary metadata only, not as tool result content.
57
-
58
- Rules:
59
-
60
- - Pick \`startId\` and \`endId\` directly from injected IDs in context.
61
- - IDs must exist in the current visible context.
62
- - \`startId\` must appear before \`endId\`.
63
- - Do not invent IDs. Use only IDs that are present in context.
64
-
65
- BATCHING
66
- When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
67
- `;
68
- }