howtofixcli 0.7.13 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-7DTQKDO4.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-AUDMRGWF.js +189 -0
- package/dist/chunk-CPJKKHGH.js +1 -0
- package/dist/chunk-DAPXGSGD.js +12 -0
- package/dist/chunk-DOD7MUGX.js +1 -0
- package/dist/chunk-DV2BBBBJ.js +2 -0
- package/dist/chunk-EIBOS5KO.js +5 -0
- package/dist/chunk-EYWOVCSS.js +6 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-HT3MFLAV.js +34 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-JMKK26AT.js +2 -0
- package/dist/chunk-LAHVHRCS.js +2 -0
- package/dist/chunk-LOOFOBTS.js +1 -0
- package/dist/chunk-LSJC73DG.js +1 -0
- package/dist/chunk-MOKUBKE7.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-ONZLNFQ2.js +3 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-SX5VE6XA.js +110 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-UNK5HRQR.js +1 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V2RPZMLK.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-VLMQSD25.js +1 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-Y7YM5R75.js +5 -0
- package/dist/chunk-YW7NORH3.js +2 -0
- package/dist/main.js +99 -540
- package/package.json +17 -64
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -120
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -294
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -295
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/middle-prune.js +0 -76
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -735
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -429
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-facts.js +0 -35
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -447
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4778
- package/dist/update.js +0 -551
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- 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
|
-
}
|