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
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
- }