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/middle-prune.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trim the middle of a large tool result instead of dropping the result.
|
|
3
|
-
*
|
|
4
|
-
* A build log or a test run is mostly noise in the middle: the command and its
|
|
5
|
-
* first lines are at the top, the failure and the exit status are at the bottom.
|
|
6
|
-
* Keeping both ends means the model still sees what it asked for and what went
|
|
7
|
-
* wrong, at a fraction of the context. Dropping the whole result, which is what
|
|
8
|
-
* happens once pruning starts, loses both.
|
|
9
|
-
*/
|
|
10
|
-
export const MIDDLE_PRUNE_DEFAULTS = {
|
|
11
|
-
thresholdChars: 8192,
|
|
12
|
-
headChars: 4096,
|
|
13
|
-
tailChars: 1024,
|
|
14
|
-
};
|
|
15
|
-
/** Cut the middle out of one text, leaving a marker that says how much went. */
|
|
16
|
-
export function pruneMiddle(text, options = {}) {
|
|
17
|
-
const threshold = options.thresholdChars ?? MIDDLE_PRUNE_DEFAULTS.thresholdChars;
|
|
18
|
-
const headChars = options.headChars ?? MIDDLE_PRUNE_DEFAULTS.headChars;
|
|
19
|
-
const tailChars = options.tailChars ?? MIDDLE_PRUNE_DEFAULTS.tailChars;
|
|
20
|
-
if (text.length <= threshold || headChars + tailChars >= text.length) {
|
|
21
|
-
return { text, prunedChars: 0 };
|
|
22
|
-
}
|
|
23
|
-
// A cut in the middle of a surrogate pair produces a broken character, and the
|
|
24
|
-
// provider rejects the request for it. Step back one unit when the cut would
|
|
25
|
-
// split a pair.
|
|
26
|
-
let headEnd = headChars;
|
|
27
|
-
const headCode = text.charCodeAt(headEnd - 1);
|
|
28
|
-
if (headCode >= 0xd800 && headCode <= 0xdbff)
|
|
29
|
-
headEnd -= 1;
|
|
30
|
-
let tailStart = text.length - tailChars;
|
|
31
|
-
const tailCode = text.charCodeAt(tailStart);
|
|
32
|
-
if (tailCode >= 0xdc00 && tailCode <= 0xdfff)
|
|
33
|
-
tailStart += 1;
|
|
34
|
-
const head = text.slice(0, headEnd);
|
|
35
|
-
const tail = text.slice(tailStart);
|
|
36
|
-
const removed = tailStart - headEnd;
|
|
37
|
-
const marker = `\n\n[... ${removed} characters pruned from the middle ...]\n\n`;
|
|
38
|
-
return { text: head + marker + tail, prunedChars: removed - marker.length };
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Apply the middle prune to every tool result in a request.
|
|
42
|
-
*
|
|
43
|
-
* Returns new messages only where something was cut, so callers can keep using
|
|
44
|
-
* the original array when nothing changed.
|
|
45
|
-
*/
|
|
46
|
-
export function applyMiddlePrune(messages, options = {}) {
|
|
47
|
-
let savedChars = 0;
|
|
48
|
-
let prunedResults = 0;
|
|
49
|
-
let changed = false;
|
|
50
|
-
const out = messages.map((message) => {
|
|
51
|
-
const candidate = message;
|
|
52
|
-
if (candidate.role !== "toolResult" || !Array.isArray(candidate.content))
|
|
53
|
-
return message;
|
|
54
|
-
const messageContent = candidate.content;
|
|
55
|
-
let touched = false;
|
|
56
|
-
const content = messageContent.map((part) => {
|
|
57
|
-
if (!part || typeof part !== "object")
|
|
58
|
-
return part;
|
|
59
|
-
const candidate = part;
|
|
60
|
-
if (candidate.type !== "text" || typeof candidate.text !== "string")
|
|
61
|
-
return part;
|
|
62
|
-
const result = pruneMiddle(candidate.text, options);
|
|
63
|
-
if (result.prunedChars <= 0)
|
|
64
|
-
return part;
|
|
65
|
-
touched = true;
|
|
66
|
-
savedChars += result.prunedChars;
|
|
67
|
-
return { ...part, text: result.text };
|
|
68
|
-
});
|
|
69
|
-
if (!touched)
|
|
70
|
-
return message;
|
|
71
|
-
changed = true;
|
|
72
|
-
prunedResults += 1;
|
|
73
|
-
return { ...message, content };
|
|
74
|
-
});
|
|
75
|
-
return { messages: changed ? out : messages, savedChars, prunedResults };
|
|
76
|
-
}
|
package/dist/dcp/prompts.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export function rangePrompt(format = "xml") {
|
|
2
|
-
const compact = format === "compact";
|
|
3
|
-
const message = compact ? "@4@" : "mNNNN";
|
|
4
|
-
const block = compact ? "@b1@" : "bN";
|
|
5
|
-
const placeholder = compact ? "@b1@" : "(bN)";
|
|
6
|
-
return `COMPRESSING A RANGE OF THIS CONVERSATION
|
|
7
|
-
|
|
8
|
-
This is what the \`compress\` tool is for, and how a range must be summarised when it is used. Reach for it when the conversation has grown long enough that its older turns cost more than they are worth - never as a way to answer the user, and never on a range they did not ask about. Nothing here is a standing instruction to summarise: it is the contract the tool's output is held to.
|
|
9
|
-
|
|
10
|
-
THE SUMMARY
|
|
11
|
-
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings... EVERYTHING that maintains context integrity. This is not a brief note - it is an authoritative record so faithful that the original conversation adds no value.
|
|
12
|
-
|
|
13
|
-
USER INTENT FIDELITY
|
|
14
|
-
When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
15
|
-
Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
|
|
16
|
-
|
|
17
|
-
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
18
|
-
|
|
19
|
-
COMPRESSED BLOCK PLACEHOLDERS
|
|
20
|
-
When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
|
|
21
|
-
|
|
22
|
-
- \`${placeholder}\`
|
|
23
|
-
|
|
24
|
-
Compressed block sections in context are clearly marked with a header:
|
|
25
|
-
|
|
26
|
-
- \`[Compressed conversation section]\`
|
|
27
|
-
|
|
28
|
-
${compact ? "Compressed block IDs look like `@b1@`, distinct from raw message IDs like `@4@`." : "Compressed block IDs always use the `bN` form (never `mNNNN`) and are represented in the same XML metadata tag format."}
|
|
29
|
-
|
|
30
|
-
Rules:
|
|
31
|
-
|
|
32
|
-
- Include every required block placeholder exactly once.
|
|
33
|
-
- Do not invent placeholders for blocks outside the selected range.
|
|
34
|
-
- Treat \`${placeholder}\` placeholders as RESERVED TOKENS. Do not emit \`${placeholder}\` text anywhere except intentional placeholders.
|
|
35
|
-
- If you need to mention a block in prose, use plain text like \`compressed ${compact ? "block 1" : "bN"}\` (not as a placeholder).
|
|
36
|
-
- Preflight check before finalizing: the set of \`${placeholder}\` placeholders in your summary must exactly match the required set, with no duplicates.
|
|
37
|
-
|
|
38
|
-
These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
|
|
39
|
-
|
|
40
|
-
FLOW PRESERVATION WITH PLACEHOLDERS
|
|
41
|
-
When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
|
|
42
|
-
|
|
43
|
-
- Treat each placeholder as a stand-in for a full conversation segment, not a short label.
|
|
44
|
-
- Ensure transitions before and after each placeholder preserve chronology and causality.
|
|
45
|
-
- Do not write text that depends on the placeholder staying literal (for example, "as noted in \`${compact ? "@b2@" : "(b2)"}\`").
|
|
46
|
-
- Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
|
|
47
|
-
|
|
48
|
-
BOUNDARY IDS
|
|
49
|
-
You specify boundaries by ID using the injected IDs visible in the conversation:
|
|
50
|
-
|
|
51
|
-
- \`${message}\` IDs identify raw messages
|
|
52
|
-
- \`${block}\` IDs identify previously compressed blocks
|
|
53
|
-
|
|
54
|
-
${compact ? "Each message has an ID like `@4@`. Copy the whole marker, including both `@` characters, into `startId` and `endId`." : "Each message has an ID inside XML metadata tags like `<dcp-message-id>...</dcp-message-id>`."}
|
|
55
|
-
The same ID tag appears in every tool output of the message it belongs to — each unique ID identifies one complete message.
|
|
56
|
-
Treat these tags as boundary metadata only, not as tool result content.
|
|
57
|
-
|
|
58
|
-
Rules:
|
|
59
|
-
|
|
60
|
-
- Pick \`startId\` and \`endId\` directly from injected IDs in context.
|
|
61
|
-
- IDs must exist in the current visible context.
|
|
62
|
-
- \`startId\` must appear before \`endId\`.
|
|
63
|
-
- Do not invent IDs. Use only IDs that are present in context.
|
|
64
|
-
|
|
65
|
-
BATCHING
|
|
66
|
-
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
67
|
-
`;
|
|
68
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { refIndicesOf } from "./ids.js";
|
|
2
|
-
import { activeCompressedRanges, estimateSummaryTokens } from "./compress-state.js";
|
|
3
|
-
import { HARD_EXCEPTION_TOOLS, isToolProtected } from "./protected.js";
|
|
4
|
-
/**
|
|
5
|
-
* The shared predicate, once: selection members in transcript order, minus
|
|
6
|
-
* the ones an active block already covers, resolved to array positions.
|
|
7
|
-
*
|
|
8
|
-
* `exceptBlockId` is the block this append is FOR. At render time that block is
|
|
9
|
-
* already active and covers exactly the window being appended, so leaving it in
|
|
10
|
-
* the skip set makes every append a no-op — silently, with a stored summary that
|
|
11
|
-
* looks correct and has lost the user's own words. The caller that renders a
|
|
12
|
-
* block's summary passes its id; a caller doing anything else omits it.
|
|
13
|
-
*/
|
|
14
|
-
function unprotectedPositions(selection, source, state, exceptBlockId) {
|
|
15
|
-
// No ref state (a transcript that was never tagged): nothing resolves, so
|
|
16
|
-
// every append is a no-op rather than an error — the compaction case.
|
|
17
|
-
const posByRef = new Map();
|
|
18
|
-
if (source.idState) {
|
|
19
|
-
const refs = refIndicesOf(source.idState, source.messages);
|
|
20
|
-
for (let position = 0; position < refs.length; position++) {
|
|
21
|
-
const ref = refs[position];
|
|
22
|
-
if (ref !== null)
|
|
23
|
-
posByRef.set(ref, position);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const covered = new Set();
|
|
27
|
-
for (const range of activeCompressedRanges(state)) {
|
|
28
|
-
if (exceptBlockId !== undefined && range.blockId === exceptBlockId)
|
|
29
|
-
continue;
|
|
30
|
-
for (const index of range.coveredIndices) {
|
|
31
|
-
covered.add(index);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const positions = [];
|
|
35
|
-
if (source.idState) {
|
|
36
|
-
for (const ref of selection.indices) {
|
|
37
|
-
if (covered.has(ref))
|
|
38
|
-
continue;
|
|
39
|
-
const position = posByRef.get(ref);
|
|
40
|
-
if (position === undefined)
|
|
41
|
-
continue;
|
|
42
|
-
positions.push(position);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return positions;
|
|
46
|
-
}
|
|
47
|
-
/** Every non-empty text of a user message; the content may be a bare string. */
|
|
48
|
-
function userTextParts(message) {
|
|
49
|
-
if (typeof message.content === "string") {
|
|
50
|
-
return message.content.trim() ? [message.content] : [];
|
|
51
|
-
}
|
|
52
|
-
const texts = [];
|
|
53
|
-
for (const part of message.content) {
|
|
54
|
-
if (part.type === "text" && part.text.trim()) {
|
|
55
|
-
texts.push(part.text);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return texts;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Append the first non-empty text of every selected user message that is not
|
|
62
|
-
* already covered by an active block. Byte-identical no-op when `enabled` is
|
|
63
|
-
* false or nothing qualifies — a heading with no body would be worse than
|
|
64
|
-
* nothing, it reads as a bug to the model.
|
|
65
|
-
*/
|
|
66
|
-
export function appendProtectedUserMessages(summary, selection, source, state, enabled, forBlockId) {
|
|
67
|
-
if (!enabled)
|
|
68
|
-
return summary;
|
|
69
|
-
const userTexts = [];
|
|
70
|
-
for (const position of unprotectedPositions(selection, source, state, forBlockId)) {
|
|
71
|
-
const message = source.messages[position];
|
|
72
|
-
if (!message || message.role !== "user")
|
|
73
|
-
continue;
|
|
74
|
-
const first = userTextParts(message)[0];
|
|
75
|
-
if (first !== undefined) {
|
|
76
|
-
userTexts.push(first);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (userTexts.length === 0)
|
|
80
|
-
return summary;
|
|
81
|
-
const heading = "\n\nThe following user messages were sent in this conversation verbatim:";
|
|
82
|
-
const body = userTexts.map((text) => `\n${text}`).join("");
|
|
83
|
-
return summary + heading + body;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Append every `<protect>...</protect>` capture from the selected user
|
|
87
|
-
* messages that is not already covered by an active block. Same no-op rule
|
|
88
|
-
* as the user-message path.
|
|
89
|
-
*/
|
|
90
|
-
export function appendProtectedPromptInfo(summary, selection, source, state, enabled, forBlockId) {
|
|
91
|
-
if (!enabled)
|
|
92
|
-
return summary;
|
|
93
|
-
const protectedTexts = [];
|
|
94
|
-
for (const position of unprotectedPositions(selection, source, state, forBlockId)) {
|
|
95
|
-
const message = source.messages[position];
|
|
96
|
-
if (!message || message.role !== "user")
|
|
97
|
-
continue;
|
|
98
|
-
for (const text of userTextParts(message)) {
|
|
99
|
-
protectedTexts.push(...extractProtectedPromptInfo(text));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (protectedTexts.length === 0)
|
|
103
|
-
return summary;
|
|
104
|
-
const heading = "\n\nThe following protected prompt information was included in this conversation verbatim:";
|
|
105
|
-
const body = protectedTexts.map((text) => `\n${text}`).join("");
|
|
106
|
-
return summary + heading + body;
|
|
107
|
-
}
|
|
108
|
-
/** Every `<protect>...</protect>` capture in `text`, trimmed, empties dropped. */
|
|
109
|
-
export function extractProtectedPromptInfo(text) {
|
|
110
|
-
const protectedTexts = [];
|
|
111
|
-
const protectTagRegex = /<protect>([\s\S]*?)<\/protect>/gi;
|
|
112
|
-
for (const match of text.matchAll(protectTagRegex)) {
|
|
113
|
-
const captured = match[1]?.trim();
|
|
114
|
-
if (captured) {
|
|
115
|
-
protectedTexts.push(captured);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return protectedTexts;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Append the outputs of protected tools from the selection window. A tool is
|
|
122
|
-
* protected when its name matches the hard exceptions, the configured list,
|
|
123
|
-
* or a configured file pattern matches a path in its arguments.
|
|
124
|
-
*
|
|
125
|
-
* The window is also the pairing scope: an output whose call sits outside the
|
|
126
|
-
* window has no arguments to inspect, so it falls back to name-based
|
|
127
|
-
* protection (`isToolProtected` with an undefined input).
|
|
128
|
-
*/
|
|
129
|
-
export function appendProtectedTools(summary, selection, source, state, protectedTools = [], protectedFilePatterns = [], forBlockId) {
|
|
130
|
-
const argsByCallId = new Map();
|
|
131
|
-
const outputs = [];
|
|
132
|
-
for (const position of unprotectedPositions(selection, source, state, forBlockId)) {
|
|
133
|
-
const message = source.messages[position];
|
|
134
|
-
if (!message)
|
|
135
|
-
continue;
|
|
136
|
-
if (message.role === "assistant") {
|
|
137
|
-
for (const part of message.content) {
|
|
138
|
-
if (part.type === "toolCall") {
|
|
139
|
-
argsByCallId.set(part.id, part.arguments);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (message.role === "toolResult") {
|
|
145
|
-
// Error outputs are prune candidates, not keepsakes; the reference only
|
|
146
|
-
// ever preserved completed output.
|
|
147
|
-
if (message.isError)
|
|
148
|
-
continue;
|
|
149
|
-
const text = message.content
|
|
150
|
-
.filter((part) => part.type === "text")
|
|
151
|
-
.map((part) => part.text)
|
|
152
|
-
.join("");
|
|
153
|
-
if (text.trim()) {
|
|
154
|
-
outputs.push({ toolCallId: message.toolCallId, toolName: message.toolName, text });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (outputs.length === 0)
|
|
159
|
-
return summary;
|
|
160
|
-
const toolNames = [...new Set([...HARD_EXCEPTION_TOOLS, ...protectedTools])];
|
|
161
|
-
const filePatterns = [...protectedFilePatterns];
|
|
162
|
-
const blocks = [];
|
|
163
|
-
for (const output of outputs) {
|
|
164
|
-
const input = argsByCallId.get(output.toolCallId);
|
|
165
|
-
if (!isToolProtected(output.toolName, input, toolNames, filePatterns))
|
|
166
|
-
continue;
|
|
167
|
-
blocks.push(`\n### Tool: ${output.toolName}\n${output.text}`);
|
|
168
|
-
}
|
|
169
|
-
if (blocks.length === 0)
|
|
170
|
-
return summary;
|
|
171
|
-
const heading = "\n\nThe following protected tools were used in this conversation as well:";
|
|
172
|
-
return summary + heading + blocks.join("");
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Store-side enrichment: layer a block's window's protected content onto its
|
|
176
|
-
* stored summary in one mutation.
|
|
177
|
-
*
|
|
178
|
-
* This is the only path that carries the text, and the handler calls it once
|
|
179
|
-
* per active block on every request. It runs here rather than in the compress
|
|
180
|
-
* tool because the tool's `messages()` is a ref snapshot, not the transcript —
|
|
181
|
-
* the appends resolve refs to messages, so only a caller holding both can do
|
|
182
|
-
* this at all.
|
|
183
|
-
*
|
|
184
|
-
* Contract rules, each observable in `check-dcp.mjs`:
|
|
185
|
-
*
|
|
186
|
-
* 1. The selection is the block's own coverage — min/max bound it, the
|
|
187
|
-
* indices enumerate it.
|
|
188
|
-
* 2. `forBlockId` is passed on every append: the block is active and covers
|
|
189
|
-
* its window, so without the self-exclusion everything no-ops silently.
|
|
190
|
-
* 3. The result is written back with `summaryTokens` recomputed through the
|
|
191
|
-
* same estimator creation uses — stale tokens would lie about the savings.
|
|
192
|
-
* 4. Idempotent via `protectedEnriched`: a re-run on a resumed session never
|
|
193
|
-
* duplicates the user's words. The flag lives on the block, not in the
|
|
194
|
-
* text, so a nested block whose body inherits an enriched summary still
|
|
195
|
-
* enriches its own window.
|
|
196
|
-
* 5. A transcript that no longer holds the covered refs simply resolves to
|
|
197
|
-
* no messages: nothing is appended, nothing throws.
|
|
198
|
-
*
|
|
199
|
-
* `state` must carry the ref assignment (`DcpState.messageIds`); typed as an
|
|
200
|
-
* intersection so this module does not depend on the store that depends on
|
|
201
|
-
* `compress-state.ts`.
|
|
202
|
-
*/
|
|
203
|
-
export function enrichBlockSummary(state, block, messages) {
|
|
204
|
-
if (block.protectedEnriched)
|
|
205
|
-
return;
|
|
206
|
-
const covered = block.coveredIndices;
|
|
207
|
-
if (covered.length > 0) {
|
|
208
|
-
const selection = {
|
|
209
|
-
startIdx: Math.min(...covered),
|
|
210
|
-
endIdx: Math.max(...covered),
|
|
211
|
-
indices: [...covered],
|
|
212
|
-
requiredBlockIds: [],
|
|
213
|
-
tokenByIndex: new Map(),
|
|
214
|
-
};
|
|
215
|
-
const source = { messages, idState: state.messageIds };
|
|
216
|
-
const enriched = appendProtectedTools(appendProtectedPromptInfo(appendProtectedUserMessages(block.summary, selection, source, state, true, block.blockId), selection, source, state, true, block.blockId), selection, source, state, [], [], block.blockId);
|
|
217
|
-
block.summary = enriched;
|
|
218
|
-
block.summaryTokens = estimateSummaryTokens(enriched);
|
|
219
|
-
}
|
|
220
|
-
block.protectedEnriched = true;
|
|
221
|
-
}
|
package/dist/dcp/protected.js
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Static protection layer: which tools and which file paths are never pruned.
|
|
3
|
-
*
|
|
4
|
-
* Ported from Opencode-DCP `lib/protected-patterns.ts`. `normalizePath`,
|
|
5
|
-
* `matchesGlob` (`*`, `?`, `**`), `getFilePathsFromParameters` (`path`,
|
|
6
|
-
* `filePath`, `patchText`, `multiedit`), `isFilePathProtected`,
|
|
7
|
-
* `isToolNameProtected`, and `isToolProtected` keep DCP's behaviour; the only
|
|
8
|
-
* divergence is typed access — the port uses `unknown` narrowing instead of the
|
|
9
|
-
* original's `any`.
|
|
10
|
-
*
|
|
11
|
-
* The keep-set (goal/plan/notes aware) is the other, dynamic protection layer
|
|
12
|
-
* and lives in `prune.ts`; this module is pure pattern matching.
|
|
13
|
-
*/
|
|
14
|
-
/** Collapse Windows separators so paths compare across platforms. */
|
|
15
|
-
export function normalizePath(input) {
|
|
16
|
-
// A single backslash. In source, "\\" is the one-character string; the
|
|
17
|
-
// previous "\\\\" was a *two*-character string, so it only ever matched a
|
|
18
|
-
// doubled separator -- which a real Windows path does not contain. The
|
|
19
|
-
// normalisation was therefore a no-op on the only platform that needs it.
|
|
20
|
-
return input.replaceAll("\\", "/");
|
|
21
|
-
}
|
|
22
|
-
function escapeRegExpChar(ch) {
|
|
23
|
-
return /[\\.^$+{}()|\[\]]/.test(ch) ? `\\${ch}` : ch;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Glob match supporting `*` (no `/`), `?` (one non-`/`), and `**`
|
|
27
|
-
* (`**` before a slash = zero or more directories, bare `**` = anything).
|
|
28
|
-
*/
|
|
29
|
-
export function matchesGlob(inputPath, pattern) {
|
|
30
|
-
if (!pattern)
|
|
31
|
-
return false;
|
|
32
|
-
const input = normalizePath(inputPath);
|
|
33
|
-
const pat = normalizePath(pattern);
|
|
34
|
-
let regex = "^";
|
|
35
|
-
for (let i = 0; i < pat.length; i++) {
|
|
36
|
-
const ch = pat[i];
|
|
37
|
-
if (ch === "*") {
|
|
38
|
-
const next = pat[i + 1];
|
|
39
|
-
if (next === "*") {
|
|
40
|
-
const after = pat[i + 2];
|
|
41
|
-
if (after === "/") {
|
|
42
|
-
// A doubled star before a slash: zero or more directories
|
|
43
|
-
regex += "(?:.*/)?";
|
|
44
|
-
i += 2;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
// **
|
|
48
|
-
regex += ".*";
|
|
49
|
-
i++;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
// *
|
|
53
|
-
regex += "[^/]*";
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (ch === "?") {
|
|
57
|
-
regex += "[^/]";
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (ch === "/") {
|
|
61
|
-
regex += "/";
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
regex += escapeRegExpChar(ch);
|
|
65
|
-
}
|
|
66
|
-
regex += "$";
|
|
67
|
-
return new RegExp(regex).test(input);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Extract file paths from a tool call's parameters:
|
|
71
|
-
* - `apply_patch`/`patch` → paths embedded in `patchText`
|
|
72
|
-
* - `multiedit` → top-level `filePath` plus each edit's `filePath`
|
|
73
|
-
* - any tool → a string `filePath`
|
|
74
|
-
* - `read`/`write`/`edit` → their V2 `path` parameter
|
|
75
|
-
* Returns unique non-empty paths.
|
|
76
|
-
*/
|
|
77
|
-
export function getFilePathsFromParameters(tool, parameters) {
|
|
78
|
-
if (typeof parameters !== "object" || parameters === null) {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
const paths = [];
|
|
82
|
-
const params = parameters;
|
|
83
|
-
// 1. apply_patch uses patchText with embedded paths
|
|
84
|
-
if ((tool === "apply_patch" || tool === "patch") && typeof params.patchText === "string") {
|
|
85
|
-
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
86
|
-
let match;
|
|
87
|
-
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
88
|
-
paths.push(match[1].trim());
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// 2. multiedit uses top-level filePath and nested edits array
|
|
92
|
-
if (tool === "multiedit") {
|
|
93
|
-
if (typeof params.filePath === "string") {
|
|
94
|
-
paths.push(params.filePath);
|
|
95
|
-
}
|
|
96
|
-
if (Array.isArray(params.edits)) {
|
|
97
|
-
for (const edit of params.edits) {
|
|
98
|
-
if (edit && typeof edit === "object" && typeof edit.filePath === "string") {
|
|
99
|
-
paths.push(edit.filePath);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// 3. Default check for common filePath parameter (read, write, edit, etc)
|
|
105
|
-
if (typeof params.filePath === "string") {
|
|
106
|
-
paths.push(params.filePath);
|
|
107
|
-
}
|
|
108
|
-
// V2 file tools use path rather than filePath.
|
|
109
|
-
if (["read", "write", "edit"].includes(tool) && typeof params.path === "string") {
|
|
110
|
-
paths.push(params.path);
|
|
111
|
-
}
|
|
112
|
-
// Unique, non-empty
|
|
113
|
-
return [...new Set(paths)].filter((p) => p.length > 0);
|
|
114
|
-
}
|
|
115
|
-
export function isFilePathProtected(filePaths, patterns) {
|
|
116
|
-
if (filePaths.length === 0)
|
|
117
|
-
return false;
|
|
118
|
-
if (patterns.length === 0)
|
|
119
|
-
return false;
|
|
120
|
-
return filePaths.some((path) => patterns.some((pattern) => matchesGlob(path, pattern)));
|
|
121
|
-
}
|
|
122
|
-
const GLOB_CHARS = /[*?]/;
|
|
123
|
-
/**
|
|
124
|
-
* Tools that are never pruned, deduped, or compressed — their results carry
|
|
125
|
-
* state the session replays from (todo/plan), or steer the run (goal/context/task).
|
|
126
|
-
* Spec §3.4 hard exceptions; the default protection list for dedup and prune.
|
|
127
|
-
*/
|
|
128
|
-
export const HARD_EXCEPTION_TOOLS = ["todo", "goal", "context", "task"];
|
|
129
|
-
export function isToolNameProtected(toolName, patterns) {
|
|
130
|
-
if (!toolName || patterns.length === 0)
|
|
131
|
-
return false;
|
|
132
|
-
const exactPatterns = new Set();
|
|
133
|
-
const globPatterns = [];
|
|
134
|
-
for (const pattern of patterns) {
|
|
135
|
-
if (GLOB_CHARS.test(pattern)) {
|
|
136
|
-
globPatterns.push(pattern);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
exactPatterns.add(pattern);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (exactPatterns.has(toolName))
|
|
143
|
-
return true;
|
|
144
|
-
return globPatterns.some((pattern) => matchesGlob(toolName, pattern));
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* True when the tool name matches `tools` or any of its file paths match
|
|
148
|
-
* `files`. `metadata.toolCalls` (V2 code-mode nested calls) is checked too:
|
|
149
|
-
* one combined output is protected when any nested call matches.
|
|
150
|
-
*/
|
|
151
|
-
export function isToolProtected(tool, input, tools, files, metadata) {
|
|
152
|
-
const calls = [{ tool, input }];
|
|
153
|
-
if (tool === "execute" && Array.isArray(metadata?.toolCalls)) {
|
|
154
|
-
for (const call of metadata.toolCalls) {
|
|
155
|
-
if (call && typeof call === "object" && typeof call.tool === "string") {
|
|
156
|
-
calls.push({ tool: call.tool, input: call.input });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return calls.some((call) => isToolNameProtected(call.tool, tools) || isFilePathProtected(getFilePathsFromParameters(call.tool, call.input), files));
|
|
161
|
-
}
|