howtofixcli 0.7.14 → 0.7.16

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 (165) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-2XG6XQZR.js +1 -0
  3. package/dist/chunk-3FYCFLNK.js +1 -0
  4. package/dist/chunk-56SEAGAQ.js +1 -0
  5. package/dist/chunk-5HR4WOW7.js +2 -0
  6. package/dist/chunk-6FUWMLWO.js +1 -0
  7. package/dist/chunk-7CHDNTH6.js +1 -0
  8. package/dist/chunk-A5WOL324.js +34 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-AUDMRGWF.js +189 -0
  11. package/dist/chunk-BX5KIC4R.js +2 -0
  12. package/dist/chunk-DAPXGSGD.js +12 -0
  13. package/dist/chunk-DOD7MUGX.js +1 -0
  14. package/dist/chunk-DVYH677E.js +5 -0
  15. package/dist/chunk-FFNDHUCQ.js +5 -0
  16. package/dist/chunk-FPXVDP32.js +8 -0
  17. package/dist/chunk-GXB7RZHH.js +1 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-LAHVHRCS.js +2 -0
  21. package/dist/chunk-LJEKOS4S.js +6 -0
  22. package/dist/chunk-LSJC73DG.js +1 -0
  23. package/dist/chunk-MOKUBKE7.js +1 -0
  24. package/dist/chunk-MVTU4M3W.js +2 -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-Q7VRW74X.js +1 -0
  30. package/dist/chunk-QJ6MIM5Z.js +1 -0
  31. package/dist/chunk-TSEKPGK4.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V3R4FYTJ.js +1 -0
  34. package/dist/chunk-V656XVXV.js +8 -0
  35. package/dist/chunk-VLMQSD25.js +1 -0
  36. package/dist/chunk-WKUJ7CFO.js +1 -0
  37. package/dist/chunk-WLU3WFCK.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-YW7NORH3.js +2 -0
  42. package/dist/chunk-ZNS6H44M.js +110 -0
  43. package/dist/main.js +99 -551
  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/acp.js +0 -234
  48. package/dist/activity.js +0 -124
  49. package/dist/agent-defs.js +0 -237
  50. package/dist/agent-limiter.js +0 -68
  51. package/dist/agents.js +0 -554
  52. package/dist/anim.js +0 -361
  53. package/dist/args.js +0 -250
  54. package/dist/at-file.js +0 -28
  55. package/dist/brand-extension.js +0 -87
  56. package/dist/brand.js +0 -76
  57. package/dist/browser.js +0 -58
  58. package/dist/cache-report.js +0 -139
  59. package/dist/checkpoint.js +0 -437
  60. package/dist/claims.js +0 -167
  61. package/dist/cli-auth.js +0 -142
  62. package/dist/cli-checkin.js +0 -120
  63. package/dist/cli-credentials.js +0 -254
  64. package/dist/clipboard.js +0 -149
  65. package/dist/collapse.js +0 -129
  66. package/dist/commands.js +0 -294
  67. package/dist/compaction-guard.js +0 -81
  68. package/dist/context-extension.js +0 -17
  69. package/dist/dcp/commands.js +0 -172
  70. package/dist/dcp/compress-state.js +0 -139
  71. package/dist/dcp/compress.js +0 -329
  72. package/dist/dcp/dcp-extension.js +0 -295
  73. package/dist/dcp/dedup.js +0 -93
  74. package/dist/dcp/ids.js +0 -189
  75. package/dist/dcp/index.js +0 -25
  76. package/dist/dcp/middle-prune.js +0 -76
  77. package/dist/dcp/prompts.js +0 -68
  78. package/dist/dcp/protected-content.js +0 -221
  79. package/dist/dcp/protected.js +0 -161
  80. package/dist/dcp/prune.js +0 -418
  81. package/dist/dcp/state.js +0 -176
  82. package/dist/device-id.js +0 -90
  83. package/dist/device-id.test.js +0 -23
  84. package/dist/errors.js +0 -63
  85. package/dist/extensions.js +0 -48
  86. package/dist/fatal.js +0 -35
  87. package/dist/feed.js +0 -498
  88. package/dist/fs-atomic.js +0 -54
  89. package/dist/gateway-errors.js +0 -90
  90. package/dist/git.js +0 -230
  91. package/dist/goal-extension.js +0 -88
  92. package/dist/goal.js +0 -222
  93. package/dist/hooks-extension.js +0 -176
  94. package/dist/hooks.js +0 -443
  95. package/dist/howtofix-account.js +0 -735
  96. package/dist/hud-line.js +0 -153
  97. package/dist/hwid.js +0 -130
  98. package/dist/interrupt.js +0 -60
  99. package/dist/json-events.js +0 -50
  100. package/dist/level-style.js +0 -112
  101. package/dist/loop-guard.js +0 -95
  102. package/dist/lsp-tools.js +0 -32
  103. package/dist/lsp.js +0 -429
  104. package/dist/mcp-config.js +0 -85
  105. package/dist/mcp-lifecycle.js +0 -147
  106. package/dist/model-catalog.js +0 -217
  107. package/dist/model-facts.js +0 -35
  108. package/dist/model-resolve.js +0 -65
  109. package/dist/notes.js +0 -78
  110. package/dist/oauth-login.js +0 -568
  111. package/dist/paths.js +0 -61
  112. package/dist/permissions.js +0 -298
  113. package/dist/plan-mode.js +0 -363
  114. package/dist/preset-command.js +0 -227
  115. package/dist/preset-extension.js +0 -35
  116. package/dist/preset-mcp.js +0 -292
  117. package/dist/presets.js +0 -727
  118. package/dist/prompt-files.js +0 -103
  119. package/dist/prompt-text.js +0 -41
  120. package/dist/proxy.js +0 -137
  121. package/dist/quality/ast.js +0 -196
  122. package/dist/quality/cognitive.js +0 -83
  123. package/dist/quality/cyclomatic.js +0 -30
  124. package/dist/quality/halstead.js +0 -83
  125. package/dist/quality/index.js +0 -18
  126. package/dist/quality/length.js +0 -3
  127. package/dist/quality/maintainability.js +0 -15
  128. package/dist/quality/nesting.js +0 -57
  129. package/dist/quality/report.js +0 -803
  130. package/dist/quality/smells.js +0 -67
  131. package/dist/quality-tool.js +0 -63
  132. package/dist/read-only-commands.js +0 -597
  133. package/dist/repo-map.js +0 -292
  134. package/dist/roblox-connect.js +0 -125
  135. package/dist/rpc.js +0 -91
  136. package/dist/rtk.js +0 -193
  137. package/dist/sandbox.js +0 -105
  138. package/dist/schema.js +0 -214
  139. package/dist/serve.js +0 -50
  140. package/dist/session-lifecycle.js +0 -19
  141. package/dist/session-retention.js +0 -111
  142. package/dist/session.js +0 -447
  143. package/dist/sessions.js +0 -100
  144. package/dist/settings.js +0 -148
  145. package/dist/skills.js +0 -35
  146. package/dist/slop-tool.js +0 -96
  147. package/dist/spend-cap.js +0 -43
  148. package/dist/subagent.js +0 -171
  149. package/dist/tar.js +0 -146
  150. package/dist/theme.js +0 -79
  151. package/dist/thinking.js +0 -45
  152. package/dist/todo.js +0 -114
  153. package/dist/tool-loadout.js +0 -97
  154. package/dist/tool-render.js +0 -769
  155. package/dist/tools.js +0 -176
  156. package/dist/ui.js +0 -4814
  157. package/dist/update.js +0 -551
  158. package/dist/usage-segment.js +0 -69
  159. package/dist/web-tools.js +0 -46
  160. package/dist/web.js +0 -265
  161. package/dist/welcome.js +0 -179
  162. package/dist/workflow-engine.js +0 -246
  163. package/dist/workflow-progress.js +0 -446
  164. package/dist/workflow-view.js +0 -195
  165. package/dist/workflow.js +0 -349
package/dist/dcp/prune.js DELETED
@@ -1,418 +0,0 @@
1
- /**
2
- * The prune transform: replaces stale context with fixed placeholders in the
3
- * copy of the transcript that is sent to the LLM. The session itself is never
4
- * touched — every function here is pure over a message array and returns new
5
- * messages.
6
- *
7
- * Placeholder constants are verbatim from Opencode-DCP `lib/messages/prune.ts`,
8
- * and the three appliers (`pruneToolOutputs` / `pruneToolInputs` /
9
- * `pruneToolErrors`) plus `filterCompressedRanges` port DCP's mechanics to the harness's
10
- * message shape: outputs live on `toolResult` messages, inputs on the
11
- * `toolCall` parts inside assistant messages. On top of that sits `planPrune`,
12
- * this repo's keep-set-aware planner (spec §3.4): three rules, each op carrying
13
- * the reason it was planned.
14
- *
15
- * Hard exceptions, never planned or applied onto:
16
- * - the `todo` / `goal` / `context` / `task` tools (HARD_EXCEPTION_TOOLS)
17
- * - the plan file's results
18
- * - user messages, assistant text/thinking (appliers only touch toolCall args)
19
- * - the message at array index 0, whatever it says
20
- *
21
- * Safety rule: when the keep-set is empty, rule 3 (big unrelated output) is
22
- * OFF — structural pruning (rules 1 and 2, dedup, compressed ranges) still
23
- * runs, but nothing is dropped for being unrelated to a goal that does not
24
- * exist.
25
- *
26
- * Pipeline order (spec §3.4): filterCompressedRanges → pruneToolOutputs →
27
- * pruneToolInputs → pruneToolErrors.
28
- */
29
- import { refIndicesOf } from "./ids.js";
30
- import { HARD_EXCEPTION_TOOLS, getFilePathsFromParameters, normalizePath } from "./protected.js";
31
- import { createToolSignature } from "./dedup.js";
32
- export const PRUNED_TOOL_OUTPUT_REPLACEMENT = "[Output removed to save context - information superseded or no longer needed]";
33
- export const PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]";
34
- export const PRUNED_QUESTION_INPUT_REPLACEMENT = "[questions removed - see output for user's answers]";
35
- /** Rule 3 threshold: outputs longer than this may be pruned when unrelated. */
36
- export const BIG_OUTPUT_CHARS = 2000;
37
- /** Tools that ask the user questions: their output holds the answers, so only
38
- * their input is ever pruned (with PRUNED_QUESTION_INPUT_REPLACEMENT). */
39
- export const QUESTION_TOOLS = ["ask", "question"];
40
- /** Tools whose results feed rule 1 (superseded file reads). */
41
- const READ_TOOLS = ["read", "grep", "ls"];
42
- /** Tools whose success supersedes an earlier read of the same path (rule 1). */
43
- const MUTATING_TOOLS = ["edit", "write", "multiedit", "apply_patch", "patch"];
44
- function textOfResult(content) {
45
- return content
46
- .filter((part) => part.type === "text")
47
- .map((part) => part.text)
48
- .join("");
49
- }
50
- function stringArgTotal(args) {
51
- if (!args)
52
- return { total: 0, count: 0 };
53
- let total = 0;
54
- let count = 0;
55
- for (const value of Object.values(args)) {
56
- if (typeof value === "string") {
57
- total += value.length;
58
- count++;
59
- }
60
- }
61
- return { total, count };
62
- }
63
- function escapeRegExp(value) {
64
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65
- }
66
- /**
67
- * Plan the prune: returns which tool calls to replace and why, without
68
- * touching anything. Apply the result with `applyPrune`.
69
- */
70
- export function planPrune(messages, keep, options) {
71
- const bigOutputChars = options?.bigOutputChars ?? BIG_OUTPUT_CHARS;
72
- const calls = new Map();
73
- const results = [];
74
- for (let index = 0; index < messages.length; index++) {
75
- const message = messages[index];
76
- if (message.role === "assistant") {
77
- for (const part of message.content) {
78
- if (part.type === "toolCall") {
79
- calls.set(part.id, { name: part.name, args: part.arguments, callIndex: index });
80
- }
81
- }
82
- }
83
- else if (message.role === "toolResult") {
84
- const call = calls.get(message.toolCallId);
85
- const tool = message.toolName;
86
- results.push({
87
- index,
88
- callId: message.toolCallId,
89
- tool,
90
- isError: message.isError,
91
- text: textOfResult(message.content),
92
- paths: call ? getFilePathsFromParameters(tool, call.args) : [],
93
- signature: createToolSignature(tool, call?.args),
94
- callIndex: call?.callIndex ?? -1,
95
- });
96
- }
97
- }
98
- const planFile = keep.planFile ? normalizePath(keep.planFile) : undefined;
99
- const isOutputExempt = (r) => r.index === 0 || HARD_EXCEPTION_TOOLS.includes(r.tool) || (planFile !== undefined && r.paths.some((p) => normalizePath(p) === planFile));
100
- const isInputExempt = (r) => r.callIndex < 0 || r.callIndex === 0 || HARD_EXCEPTION_TOOLS.includes(r.tool) || (planFile !== undefined && r.paths.some((p) => normalizePath(p) === planFile));
101
- const ops = [];
102
- const emitted = new Set();
103
- const emit = (op) => {
104
- const key = `${op.kind}:${op.callId}`;
105
- if (emitted.has(key))
106
- return;
107
- emitted.add(key);
108
- ops.push(op);
109
- };
110
- // Rule 1 — a read/grep/ls result whose file was later edited or re-read.
111
- // The newest result for the path stays; every older read is superseded.
112
- const latestByPath = new Map();
113
- for (const r of results) {
114
- if (r.isError || (!READ_TOOLS.includes(r.tool) && !MUTATING_TOOLS.includes(r.tool)))
115
- continue;
116
- for (const path of r.paths) {
117
- const key = normalizePath(path);
118
- const previous = latestByPath.get(key);
119
- if (previous === undefined || r.index > previous)
120
- latestByPath.set(key, r.index);
121
- }
122
- }
123
- for (const r of results) {
124
- if (r.isError || !READ_TOOLS.includes(r.tool) || isOutputExempt(r))
125
- continue;
126
- // Already pruned: the paths are still attached, so without this the read
127
- // looks superseded on every pass and the plan is never empty. Skipping it
128
- // is what makes a second pass over a pruned transcript a no-op.
129
- if (r.text === PRUNED_TOOL_OUTPUT_REPLACEMENT)
130
- continue;
131
- const superseded = r.paths.some((path) => (latestByPath.get(normalizePath(path)) ?? -1) > r.index);
132
- if (superseded) {
133
- emit({
134
- kind: "output",
135
- reason: "superseded-read",
136
- callId: r.callId,
137
- messageIndex: r.index,
138
- savedChars: Math.max(0, r.text.length - PRUNED_TOOL_OUTPUT_REPLACEMENT.length),
139
- });
140
- }
141
- }
142
- // Rule 2 — an error result followed by an identical successful call: the
143
- // error output is superseded, and the failed call's arguments (the most
144
- // expensive part — a rejected patch) are useless now that the retry worked.
145
- const latestSuccessBySignature = new Map();
146
- for (const r of results) {
147
- if (r.isError)
148
- continue;
149
- const previous = latestSuccessBySignature.get(r.signature);
150
- if (previous === undefined || r.index > previous)
151
- latestSuccessBySignature.set(r.signature, r.index);
152
- }
153
- for (const r of results) {
154
- if (!r.isError || isOutputExempt(r))
155
- continue;
156
- const success = latestSuccessBySignature.get(r.signature);
157
- if (success === undefined || success <= r.index)
158
- continue;
159
- emit({
160
- kind: "output",
161
- reason: "error-then-success",
162
- callId: r.callId,
163
- messageIndex: r.index,
164
- savedChars: Math.max(0, r.text.length - PRUNED_TOOL_OUTPUT_REPLACEMENT.length),
165
- });
166
- if (!isInputExempt(r)) {
167
- const args = calls.get(r.callId)?.args;
168
- const { total, count } = stringArgTotal(args);
169
- if (count > 0) {
170
- emit({
171
- kind: "failed-input",
172
- reason: "error-then-success",
173
- callId: r.callId,
174
- messageIndex: r.callIndex,
175
- savedChars: Math.max(0, total - count * PRUNED_TOOL_ERROR_INPUT_REPLACEMENT.length),
176
- });
177
- }
178
- }
179
- }
180
- // Rule 3 — an output over the threshold that does not touch the keep-set.
181
- // OFF when the keep-set is empty: nothing is dropped for being unrelated to
182
- // a goal that does not exist.
183
- if (keep.files.length > 0 || keep.identifiers.length > 0) {
184
- const touchFiles = [...keep.files.map(normalizePath), ...(planFile ? [planFile] : [])];
185
- const basenames = touchFiles.map((file) => file.split("/").pop() ?? "").filter((name) => name.length > 0);
186
- const identifierPatterns = keep.identifiers.map((identifier) => new RegExp(`(?<![\\w.-])${escapeRegExp(identifier)}(?![\\w.-])`));
187
- const touchesKeep = (text) => {
188
- const normalized = normalizePath(text);
189
- if (touchFiles.some((file) => normalized.includes(file)))
190
- return true;
191
- if (basenames.some((name) => text.includes(name)))
192
- return true;
193
- return identifierPatterns.some((pattern) => pattern.test(text));
194
- };
195
- for (const r of results) {
196
- if (isOutputExempt(r) || QUESTION_TOOLS.includes(r.tool))
197
- continue;
198
- if (r.text.length <= bigOutputChars)
199
- continue;
200
- if (touchesKeep(r.text))
201
- continue;
202
- emit({
203
- kind: "output",
204
- reason: "unrelated-big-output",
205
- callId: r.callId,
206
- messageIndex: r.index,
207
- savedChars: Math.max(0, r.text.length - PRUNED_TOOL_OUTPUT_REPLACEMENT.length),
208
- });
209
- }
210
- }
211
- let savedChars = 0;
212
- for (const op of ops)
213
- savedChars += op.savedChars;
214
- return { ops, savedChars };
215
- }
216
- /** Apply planned ops to a copy of the transcript, in DCP's order:
217
- * outputs → question inputs → failed inputs. Pure; input is never mutated. */
218
- export function applyPrune(messages, ops) {
219
- const outputs = new Set();
220
- const questions = new Set();
221
- const failures = new Set();
222
- for (const op of ops) {
223
- if (op.kind === "output")
224
- outputs.add(op.callId);
225
- else if (op.kind === "question-input")
226
- questions.add(op.callId);
227
- else
228
- failures.add(op.callId);
229
- }
230
- return pruneToolErrors(pruneToolInputs(pruneToolOutputs(messages, outputs), questions), failures);
231
- }
232
- /** Replace the toolResult content of every marked call with the output
233
- placeholder. Question tools are skipped — their answers live in the output
234
- and their input is pruned instead. Never touches array index 0. */
235
- export function pruneToolOutputs(messages, marks) {
236
- if (marks.size === 0)
237
- return [...messages];
238
- return messages.map((message, index) => {
239
- if (index === 0 || message.role !== "toolResult" || !marks.has(message.toolCallId))
240
- return message;
241
- if (QUESTION_TOOLS.includes(message.toolName))
242
- return message;
243
- return { ...message, content: [{ type: "text", text: PRUNED_TOOL_OUTPUT_REPLACEMENT }] };
244
- });
245
- }
246
- /** For marked, successfully answered question tools (ask/question), replace
247
- the question-bearing input fields — the answers are in the output. */
248
- export function pruneToolInputs(messages, marks) {
249
- if (marks.size === 0)
250
- return [...messages];
251
- const answered = new Set();
252
- for (const message of messages) {
253
- if (message.role === "toolResult" && !message.isError && marks.has(message.toolCallId)) {
254
- answered.add(message.toolCallId);
255
- }
256
- }
257
- if (answered.size === 0)
258
- return [...messages];
259
- return messages.map((message, index) => {
260
- if (index === 0 || message.role !== "assistant")
261
- return message;
262
- let changed = false;
263
- const content = message.content.map((part) => {
264
- if (part.type !== "toolCall" || !answered.has(part.id) || !QUESTION_TOOLS.includes(part.name))
265
- return part;
266
- const args = { ...part.arguments };
267
- let touched = false;
268
- for (const field of ["question", "questions", "options"]) {
269
- if (field in args) {
270
- args[field] = PRUNED_QUESTION_INPUT_REPLACEMENT;
271
- touched = true;
272
- }
273
- }
274
- if (!touched)
275
- return part;
276
- changed = true;
277
- return { ...part, arguments: args };
278
- });
279
- return changed ? { ...message, content } : message;
280
- });
281
- }
282
- /** For marked calls whose tool result is an error, replace every string
283
- argument with the failed-call placeholder — a rejected patch's arguments are
284
- the most expensive dead weight in the transcript. */
285
- export function pruneToolErrors(messages, marks) {
286
- if (marks.size === 0)
287
- return [...messages];
288
- const failed = new Set();
289
- for (const message of messages) {
290
- if (message.role === "toolResult" && message.isError && marks.has(message.toolCallId)) {
291
- failed.add(message.toolCallId);
292
- }
293
- }
294
- if (failed.size === 0)
295
- return [...messages];
296
- return messages.map((message, index) => {
297
- if (index === 0 || message.role !== "assistant")
298
- return message;
299
- let changed = false;
300
- const content = message.content.map((part) => {
301
- if (part.type !== "toolCall" || !failed.has(part.id))
302
- return part;
303
- const args = {};
304
- let hasString = false;
305
- for (const [key, value] of Object.entries(part.arguments)) {
306
- if (typeof value === "string") {
307
- args[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT;
308
- hasString = true;
309
- }
310
- else {
311
- args[key] = value;
312
- }
313
- }
314
- if (!hasString)
315
- return part;
316
- changed = true;
317
- return { ...part, arguments: args };
318
- });
319
- return changed ? { ...message, content } : message;
320
- });
321
- }
322
- /** Drop every message covered by an active compressed range and insert the
323
- block's summary (a synthetic user message) at the anchor position. Unref'd
324
- positions — the system message above all — can never be covered. */
325
- export function filterCompressedRanges(messages, ranges, state) {
326
- if (ranges.length === 0)
327
- return [...messages];
328
- const refs = refIndicesOf(state, messages);
329
- const posByRef = new Map();
330
- for (let pos = 0; pos < refs.length; pos++) {
331
- const ref = refs[pos];
332
- if (ref !== null)
333
- posByRef.set(ref, pos);
334
- }
335
- const covered = new Set();
336
- const summaries = new Map();
337
- for (const range of ranges) {
338
- const positions = [];
339
- for (const coveredIndex of range.coveredIndices) {
340
- const pos = posByRef.get(coveredIndex);
341
- if (pos !== undefined) {
342
- covered.add(pos);
343
- positions.push(pos);
344
- }
345
- }
346
- const anchorPos = posByRef.get(range.anchorIndex) ?? (positions.length > 0 ? Math.min(...positions) : undefined);
347
- if (range.summary && anchorPos !== undefined) {
348
- const existing = summaries.get(anchorPos);
349
- if (existing)
350
- existing.push(range.summary);
351
- else
352
- summaries.set(anchorPos, [range.summary]);
353
- }
354
- }
355
- // A tool call and its result are one exchange, and a provider rejects a
356
- // transcript where they are separated: an assistant message calling a tool
357
- // whose result is absent is an invalid request, and that is what the 400 from
358
- // the provider was. So the covered set is closed over exchanges first - if
359
- // either half is covered, both are - before anything is dropped.
360
- //
361
- // Nothing before this noticed because every fixture compressed whole
362
- // exchanges; a range boundary landing between a call and its result is what
363
- // a real model produces.
364
- const callIdByPosition = new Map();
365
- const resultPositionByCallId = new Map();
366
- for (let position = 0; position < messages.length; position++) {
367
- const message = messages[position];
368
- if (!message)
369
- continue;
370
- if (message.role === "assistant" && Array.isArray(message.content)) {
371
- for (const part of message.content) {
372
- if (part.type === "toolCall")
373
- callIdByPosition.set(position, part.id);
374
- }
375
- }
376
- else if (message.role === "toolResult") {
377
- resultPositionByCallId.set(message.toolCallId, position);
378
- }
379
- }
380
- // One pass over a worklist rather than nested scans: a position pulled in can
381
- // pull in another exchange behind it.
382
- const queue = [...covered];
383
- while (queue.length > 0) {
384
- const position = queue.pop();
385
- const callId = callIdByPosition.get(position);
386
- if (callId !== undefined) {
387
- const resultPosition = resultPositionByCallId.get(callId);
388
- if (resultPosition !== undefined && !covered.has(resultPosition)) {
389
- covered.add(resultPosition);
390
- queue.push(resultPosition);
391
- }
392
- }
393
- const message = messages[position];
394
- if (message?.role === "toolResult") {
395
- for (const [callPosition, id] of callIdByPosition) {
396
- if (id === message.toolCallId && !covered.has(callPosition)) {
397
- covered.add(callPosition);
398
- queue.push(callPosition);
399
- }
400
- }
401
- }
402
- }
403
- const out = [];
404
- for (let index = 0; index < messages.length; index++) {
405
- const pending = summaries.get(index);
406
- if (pending) {
407
- const anchor = messages[index];
408
- for (const summary of pending) {
409
- const synthetic = { role: "user", content: summary, timestamp: anchor.timestamp };
410
- out.push(synthetic);
411
- }
412
- }
413
- if (covered.has(index))
414
- continue;
415
- out.push(messages[index]);
416
- }
417
- return out;
418
- }
package/dist/dcp/state.js DELETED
@@ -1,176 +0,0 @@
1
- /**
2
- * DCP session state: what has been pruned, what has been compressed, and the
3
- * three-layer switch that decides whether any of it runs.
4
- *
5
- * Two responsibilities live here deliberately:
6
- *
7
- * 1. **The on/off switch** (spec §2). `HOWTOFIX_DCP` (env, strongest) →
8
- * `enabled` in `<cwd>/.howtofix/dcp.json` (config) → the session's own
9
- * `/dcp on|off` override (weakest) → default on. Every resolution reports
10
- * WHICH layer decided, so `/dcp` can say "off (config)" instead of a bare
11
- * value the user cannot attribute — the pattern `antislopEnabled` in
12
- * `paths.ts` uses for the skill suite. The config layer is read once when
13
- * the extension is created (spec §2), so `resolveDcpEnabled` takes the
14
- * layers as plain values and stays pure; `readConfigEnabled` is the file
15
- * read the factory and the `/dcp` command each perform for themselves.
16
- *
17
- * 2. **The per-session state file** (spec §3.6): prune marks, compressed
18
- * blocks with their anchors, the block/run id counters, and the savings
19
- * stats. Plain JSON under `AGENT_DIR/dcp/<session>.json` — machine state,
20
- * not a project document, so it is never git-backed — written atomically
21
- * like every other persisted state file in this codebase. Loaded into a
22
- * per-session cache so the request handler and the slash commands mutate
23
- * one object instead of racing load-modify-save cycles.
24
- */
25
- import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
26
- import { dirname, join } from "node:path";
27
- import { writeFileAtomicSync } from "../fs-atomic.js";
28
- import { AGENT_DIR } from "../paths.js";
29
- import { createMessageIdState } from "./ids.js";
30
- /**
31
- * Read `enabled` from `<cwd>/.howtofix/dcp.json`.
32
- *
33
- * Returns `undefined` when the file is missing, unparseable, or holds no
34
- * boolean there: a broken config is "no decision", not an error, because the
35
- * next layer down must still get its say — same rule as `configuredAntislop`.
36
- */
37
- export function readConfigEnabled(cwd) {
38
- try {
39
- const file = join(cwd, ".howtofix", "dcp.json");
40
- if (!existsSync(file))
41
- return undefined;
42
- const parsed = JSON.parse(readFileSync(file, "utf8"));
43
- if (typeof parsed !== "object" || parsed === null)
44
- return undefined;
45
- const enabled = parsed.enabled;
46
- return typeof enabled === "boolean" ? enabled : undefined;
47
- }
48
- catch {
49
- return undefined;
50
- }
51
- }
52
- /**
53
- * The three-layer switch, strongest layer first (spec §2):
54
- *
55
- * 1. `HOWTOFIX_DCP` — `0`/`false` off, any other value on. Wins both ways,
56
- * because it is how one person disables pruning across every project at
57
- * once while chasing a suspected pruning bug.
58
- * 2. `enabled` in `.howtofix/dcp.json` — the per-project say.
59
- * 3. The session's `/dcp on|off` override — consulted only when neither
60
- * layer above decided, so it persists across a resume of the same session
61
- * without out-ranking the project's own config.
62
- * 4. Default: on.
63
- */
64
- export function resolveDcpEnabled(layers) {
65
- if (layers.env !== undefined) {
66
- const value = layers.env.trim().toLowerCase();
67
- return { enabled: value !== "0" && value !== "false", source: "env" };
68
- }
69
- if (layers.config !== undefined)
70
- return { enabled: layers.config, source: "config" };
71
- if (layers.runtime !== undefined)
72
- return { enabled: layers.runtime, source: "session" };
73
- return { enabled: true, source: "default" };
74
- }
75
- /** A fresh state: no marks, no blocks, counters start where validation expects them. */
76
- export function createDcpState() {
77
- return {
78
- messageIds: createMessageIdState(),
79
- prune: { tools: {}, messages: { blocks: [], nextBlockId: 1, nextRunId: 1 } },
80
- stats: { totalPruneTokens: 0, totalCompressTokens: 0 },
81
- };
82
- }
83
- /**
84
- * Where a session's state is stored. Session ids are already file-safe, but
85
- * normalise anyway: this value builds a path, and a stray separator here would
86
- * write outside the dcp directory.
87
- */
88
- export function dcpStatePath(sessionId) {
89
- const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, "_") || "session";
90
- return join(AGENT_DIR, "dcp", `${safe}.json`);
91
- }
92
- function isDcpState(value) {
93
- if (typeof value !== "object" || value === null)
94
- return false;
95
- const state = value;
96
- const messages = state.prune?.messages;
97
- const ids = state.messageIds;
98
- return (typeof state.prune === "object" &&
99
- state.prune !== null &&
100
- typeof state.prune.tools === "object" &&
101
- state.prune.tools !== null &&
102
- typeof messages === "object" &&
103
- messages !== null &&
104
- Array.isArray(messages.blocks) &&
105
- Number.isInteger(messages.nextBlockId) &&
106
- Number.isInteger(messages.nextRunId) &&
107
- typeof ids === "object" &&
108
- ids !== null &&
109
- Number.isInteger(ids.nextRef) &&
110
- typeof ids.keyToRef === "object" &&
111
- ids.keyToRef !== null &&
112
- typeof ids.refToKey === "object" &&
113
- ids.refToKey !== null &&
114
- typeof state.stats === "object" &&
115
- state.stats !== null &&
116
- Number.isFinite(state.stats.totalPruneTokens) &&
117
- Number.isFinite(state.stats.totalCompressTokens));
118
- }
119
- /**
120
- * Per-session cache. The handler runs before every LLM call and the slash
121
- * commands mutate between them; one live object per session keeps them from
122
- * overwriting each other's load-modify-save cycles.
123
- */
124
- const states = new Map();
125
- /** The session's state, loaded from disk on first touch. */
126
- export function getDcpState(sessionId) {
127
- const cached = states.get(sessionId);
128
- if (cached)
129
- return cached;
130
- let loaded;
131
- try {
132
- const file = dcpStatePath(sessionId);
133
- if (existsSync(file)) {
134
- const parsed = JSON.parse(readFileSync(file, "utf8"));
135
- if (isDcpState(parsed))
136
- loaded = parsed;
137
- }
138
- }
139
- catch {
140
- // An unreadable or hand-mangled state file starts fresh rather than
141
- // killing every request with a parse error.
142
- loaded = undefined;
143
- }
144
- const state = loaded ?? createDcpState();
145
- states.set(sessionId, state);
146
- return state;
147
- }
148
- /** Persist the cached state for this session. No-op before first touch. */
149
- export function saveDcpState(sessionId) {
150
- const state = states.get(sessionId);
151
- if (!state)
152
- return;
153
- const file = dcpStatePath(sessionId);
154
- mkdirSync(dirname(file), { recursive: true });
155
- writeFileAtomicSync(file, JSON.stringify(state));
156
- }
157
- /**
158
- * Drop a session's state, in memory and on disk.
159
- *
160
- * The cache is process-wide and keyed by session, and the file under
161
- * `AGENT_DIR/dcp/` is one per session. Neither had a delete: `/new` and
162
- * `/resume` replace the session, but the previous one's ref maps and its file
163
- * stayed for good. The design spec says the state is deleted when the session
164
- * is discarded. A later resume of the same session finds no file and starts
165
- * fresh, which is the correct state for a session that was thrown away — the
166
- * refs it stored pointed at a transcript that no longer exists.
167
- */
168
- export function dropDcpState(sessionId) {
169
- states.delete(sessionId);
170
- try {
171
- unlinkSync(dcpStatePath(sessionId));
172
- }
173
- catch {
174
- // Already gone is the state we wanted.
175
- }
176
- }
package/dist/device-id.js DELETED
@@ -1,90 +0,0 @@
1
- /**
2
- * A stable identifier for the machine the CLI runs on.
3
- *
4
- * "HWID" means the identifiers the operating system itself treats as belonging
5
- * to the hardware: the Windows MachineGuid, the macOS platform UUID, or the
6
- * Linux DMI product UUID. A host name is not one of those — it changes with a
7
- * rename and says nothing about the box — so it is only a last resort when no
8
- * platform identifier can be read at all.
9
- *
10
- * The value is hashed before it leaves the machine. The gateway only needs to
11
- * tell one device from another, not to know which physical machine it is, so
12
- * the raw identifier never travels and is never stored.
13
- */
14
- import { execFileSync } from "node:child_process";
15
- import { createHash } from "node:crypto";
16
- import { readFileSync } from "node:fs";
17
- import { arch, hostname, platform } from "node:os";
18
- function readText(path) {
19
- try {
20
- return readFileSync(path, "utf8").trim();
21
- }
22
- catch {
23
- return "";
24
- }
25
- }
26
- function run(command, args) {
27
- try {
28
- return execFileSync(command, args, { encoding: "utf8", timeout: 3000 });
29
- }
30
- catch {
31
- return "";
32
- }
33
- }
34
- /** Windows: the MachineGuid the OS installs once per machine. */
35
- function windowsMachineGuid() {
36
- const out = run("reg", [
37
- "query",
38
- "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
39
- "/v",
40
- "MachineGuid",
41
- ]);
42
- return out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]+)/)?.[1] ?? "";
43
- }
44
- /** macOS: IOPlatformUUID, which follows the logic board. */
45
- function macPlatformUuid() {
46
- const out = run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
47
- return out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)?.[1] ?? "";
48
- }
49
- /** Linux: DMI first (firmware-provided), then the install's machine id. */
50
- function linuxHardwareIds() {
51
- return [
52
- "/sys/class/dmi/id/product_uuid",
53
- "/sys/class/dmi/id/board_serial",
54
- "/sys/class/dmi/id/product_serial",
55
- "/etc/machine-id",
56
- "/var/lib/dbus/machine-id",
57
- ]
58
- .map(readText)
59
- .filter(Boolean);
60
- }
61
- /** The identifiers this platform offers, strongest first. */
62
- export function hardwareIds() {
63
- switch (platform()) {
64
- case "win32": {
65
- const guid = windowsMachineGuid();
66
- return guid ? [guid] : [];
67
- }
68
- case "darwin": {
69
- const uuid = macPlatformUuid();
70
- return uuid ? [uuid] : [];
71
- }
72
- default:
73
- return linuxHardwareIds();
74
- }
75
- }
76
- let cached;
77
- /**
78
- * A 32-character hex id, stable for the life of the machine. Falls back to the
79
- * host name only when the platform exposes no hardware identifier at all, and
80
- * says so through the `weak` flag rather than pretending it is an HWID.
81
- */
82
- export function deviceId() {
83
- if (cached)
84
- return { id: cached, weak: false };
85
- const ids = hardwareIds();
86
- const weak = ids.length === 0;
87
- const material = [platform(), arch(), ...ids, weak ? hostname() : ""].join("|");
88
- cached = createHash("sha256").update(material).digest("hex").slice(0, 32);
89
- return { id: cached, weak };
90
- }