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/repo-map.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { extname, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { Type } from "@netrodev/hxf-ai";
|
|
4
|
-
import { defineTool } from "@netrodev/hxf-coding-agent";
|
|
5
|
-
/**
|
|
6
|
-
* A ranked outline of the repository, built without a parser dependency.
|
|
7
|
-
*
|
|
8
|
-
* The model cannot read a large tree, and listing every file is worse than
|
|
9
|
-
* useless: it spends the budget evenly on files that matter and files that do
|
|
10
|
-
* not. This walks the tree, pulls the declarations out of each file, follows the
|
|
11
|
-
* relative imports between them, and ranks by how much of the code depends on a
|
|
12
|
-
* file — the same idea as a tree-sitter repo map, done with regexes so it works
|
|
13
|
-
* on any machine without native builds.
|
|
14
|
-
*
|
|
15
|
-
* The ranking is the point. A file that ten others import is more likely to be
|
|
16
|
-
* the one worth reading than a file nobody mentions, and a symbol outline says
|
|
17
|
-
* what is in it before a single line is opened.
|
|
18
|
-
*/
|
|
19
|
-
const SKIP_DIRS = new Set([
|
|
20
|
-
"node_modules",
|
|
21
|
-
".git",
|
|
22
|
-
"dist",
|
|
23
|
-
"build",
|
|
24
|
-
"out",
|
|
25
|
-
"coverage",
|
|
26
|
-
"vendor",
|
|
27
|
-
"target",
|
|
28
|
-
"__pycache__",
|
|
29
|
-
".venv",
|
|
30
|
-
"venv",
|
|
31
|
-
".next",
|
|
32
|
-
".turbo",
|
|
33
|
-
".cache",
|
|
34
|
-
]);
|
|
35
|
-
const LANGUAGES = {
|
|
36
|
-
".ts": {
|
|
37
|
-
kind: "ts",
|
|
38
|
-
patterns: [
|
|
39
|
-
/^\s*export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
|
|
40
|
-
/^\s*export\s+(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/gm,
|
|
41
|
-
/^\s*export\s+(?:interface|type)\s+([A-Za-z_$][\w$]*)/gm,
|
|
42
|
-
/^\s*export\s+const\s+([A-Za-z_$][\w$]*)/gm,
|
|
43
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm,
|
|
44
|
-
/^\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/gm,
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
".tsx": { kind: "tsx", patterns: [] },
|
|
48
|
-
".js": { kind: "js", patterns: [] },
|
|
49
|
-
".mjs": { kind: "js", patterns: [] },
|
|
50
|
-
".go": {
|
|
51
|
-
kind: "go",
|
|
52
|
-
patterns: [
|
|
53
|
-
/^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm,
|
|
54
|
-
/^type\s+([A-Za-z_]\w*)\s+(?:struct|interface)/gm,
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
".py": {
|
|
58
|
-
kind: "py",
|
|
59
|
-
patterns: [/^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, /^class\s+([A-Za-z_]\w*)/gm],
|
|
60
|
-
},
|
|
61
|
-
".rs": {
|
|
62
|
-
kind: "rs",
|
|
63
|
-
patterns: [
|
|
64
|
-
/^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_]\w*)/gm,
|
|
65
|
-
/^\s*(?:pub\s+)?(?:struct|enum|trait)\s+([A-Za-z_]\w*)/gm,
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
// TSX/JS share the TypeScript patterns; declared separately so the table reads
|
|
70
|
-
// as "which extensions are walked", not "which extensions have a parser".
|
|
71
|
-
LANGUAGES[".tsx"].patterns = LANGUAGES[".ts"].patterns;
|
|
72
|
-
LANGUAGES[".js"].patterns = LANGUAGES[".ts"].patterns;
|
|
73
|
-
LANGUAGES[".mjs"].patterns = LANGUAGES[".ts"].patterns;
|
|
74
|
-
const MAX_FILE_BYTES = 512 * 1024;
|
|
75
|
-
const MAX_DEPTH = 12;
|
|
76
|
-
/** Declarations in one file, deduplicated and in source order. */
|
|
77
|
-
export function extractSymbols(text, ext) {
|
|
78
|
-
const language = LANGUAGES[ext];
|
|
79
|
-
if (!language)
|
|
80
|
-
return [];
|
|
81
|
-
const seen = new Set();
|
|
82
|
-
const out = [];
|
|
83
|
-
for (const pattern of language.patterns) {
|
|
84
|
-
pattern.lastIndex = 0;
|
|
85
|
-
for (const match of text.matchAll(pattern)) {
|
|
86
|
-
const name = match[1];
|
|
87
|
-
if (name && !seen.has(name)) {
|
|
88
|
-
seen.add(name);
|
|
89
|
-
out.push(name);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
95
|
-
/** Relative import specifiers, as written in the file. */
|
|
96
|
-
export function extractImports(text, ext) {
|
|
97
|
-
const found = new Set();
|
|
98
|
-
const add = (spec) => {
|
|
99
|
-
if (spec && (spec.startsWith("./") || spec.startsWith("../")))
|
|
100
|
-
found.add(spec);
|
|
101
|
-
};
|
|
102
|
-
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".mjs") {
|
|
103
|
-
for (const match of text.matchAll(/(?:from|require\()\s*["']([^"']+)["']/g))
|
|
104
|
-
add(match[1]);
|
|
105
|
-
for (const match of text.matchAll(/^\s*import\s+["']([^"']+)["']/gm))
|
|
106
|
-
add(match[1]);
|
|
107
|
-
}
|
|
108
|
-
else if (ext === ".go") {
|
|
109
|
-
for (const match of text.matchAll(/^\s*(?:[\w.]+\s+)?"([^"]+)"/gm))
|
|
110
|
-
add(match[1]);
|
|
111
|
-
}
|
|
112
|
-
else if (ext === ".py") {
|
|
113
|
-
for (const match of text.matchAll(/^\s*from\s+(\.+[\w.]*)\s+import/gm)) {
|
|
114
|
-
const spec = match[1].replace(/\./g, "/");
|
|
115
|
-
add(spec.startsWith("/") ? `.${spec}` : `./${spec}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else if (ext === ".rs") {
|
|
119
|
-
for (const match of text.matchAll(/^\s*use\s+(?:crate|super|self)::([\w:]+)/gm)) {
|
|
120
|
-
add(`./${match[1].split("::").join("/")}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return [...found];
|
|
124
|
-
}
|
|
125
|
-
/** Resolve an import specifier to a file that is in the walked set. */
|
|
126
|
-
function resolveImport(fromPath, spec, known) {
|
|
127
|
-
const base = fromPath.split("/").slice(0, -1).join("/");
|
|
128
|
-
const target = join(base, spec).split(sep).join("/").replace(/^\.\//, "");
|
|
129
|
-
const candidates = [
|
|
130
|
-
target,
|
|
131
|
-
`${target}.ts`,
|
|
132
|
-
`${target}.tsx`,
|
|
133
|
-
`${target}.js`,
|
|
134
|
-
`${target}.mjs`,
|
|
135
|
-
`${target}.go`,
|
|
136
|
-
`${target}.py`,
|
|
137
|
-
`${target}.rs`,
|
|
138
|
-
`${target}/index.ts`,
|
|
139
|
-
`${target}/index.tsx`,
|
|
140
|
-
`${target}/index.js`,
|
|
141
|
-
`${target}/mod.rs`,
|
|
142
|
-
`${target}.py`,
|
|
143
|
-
];
|
|
144
|
-
return candidates.find((candidate) => known.has(candidate));
|
|
145
|
-
}
|
|
146
|
-
/** Walk the tree, skipping the directories nobody wants in a map. */
|
|
147
|
-
export function collectFiles(root, subdir = ".") {
|
|
148
|
-
const files = [];
|
|
149
|
-
const walk = (dir, depth) => {
|
|
150
|
-
if (depth > MAX_DEPTH)
|
|
151
|
-
return;
|
|
152
|
-
let entries;
|
|
153
|
-
try {
|
|
154
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
for (const entry of entries) {
|
|
160
|
-
const full = join(dir, entry.name);
|
|
161
|
-
if (entry.isDirectory()) {
|
|
162
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
|
|
163
|
-
continue;
|
|
164
|
-
walk(full, depth + 1);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const ext = extname(entry.name);
|
|
168
|
-
if (!LANGUAGES[ext])
|
|
169
|
-
continue;
|
|
170
|
-
try {
|
|
171
|
-
if (statSync(full).size > MAX_FILE_BYTES)
|
|
172
|
-
continue;
|
|
173
|
-
const text = readFileSync(full, "utf8");
|
|
174
|
-
const rel = relative(root, full).split(sep).join("/");
|
|
175
|
-
files.push({ path: rel, symbols: extractSymbols(text, ext), imports: extractImports(text, ext) });
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// An unreadable file is not a reason to fail the map.
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
walk(resolve(root, subdir), 0);
|
|
183
|
-
return files;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Rank files by how much of the tree depends on them.
|
|
187
|
-
*
|
|
188
|
-
* PageRank over the import graph, with a small personalisation toward files
|
|
189
|
-
* that mention the query. Twenty iterations is far past convergence for a
|
|
190
|
-
* repository of this size and keeps the call cheap.
|
|
191
|
-
*/
|
|
192
|
-
export function rankFiles(files, query) {
|
|
193
|
-
if (files.length === 0)
|
|
194
|
-
return [];
|
|
195
|
-
const known = new Set(files.map((file) => file.path));
|
|
196
|
-
const index = new Map(files.map((file, i) => [file.path, i]));
|
|
197
|
-
const out = files.map(() => []);
|
|
198
|
-
const incoming = files.map(() => []);
|
|
199
|
-
files.forEach((file, i) => {
|
|
200
|
-
for (const spec of file.imports) {
|
|
201
|
-
const target = resolveImport(file.path, spec, known);
|
|
202
|
-
if (!target || target === file.path)
|
|
203
|
-
continue;
|
|
204
|
-
const j = index.get(target);
|
|
205
|
-
if (j === undefined)
|
|
206
|
-
continue;
|
|
207
|
-
out[i].push(j);
|
|
208
|
-
incoming[j].push(i);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
const n = files.length;
|
|
212
|
-
const needle = query?.trim().toLowerCase();
|
|
213
|
-
const personal = files.map((file) => {
|
|
214
|
-
if (!needle)
|
|
215
|
-
return 1 / n;
|
|
216
|
-
const haystack = `${file.path} ${file.symbols.join(" ")}`.toLowerCase();
|
|
217
|
-
return haystack.includes(needle) ? 4 / n : 1 / n;
|
|
218
|
-
});
|
|
219
|
-
const personalTotal = personal.reduce((sum, value) => sum + value, 0);
|
|
220
|
-
let rank = files.map((_, i) => personal[i] / personalTotal);
|
|
221
|
-
for (let iteration = 0; iteration < 20; iteration++) {
|
|
222
|
-
const next = files.map((_, i) => 0.15 * (personal[i] / personalTotal));
|
|
223
|
-
for (let i = 0; i < n; i++) {
|
|
224
|
-
if (out[i].length === 0) {
|
|
225
|
-
// A file that imports nothing spreads its weight over the whole tree,
|
|
226
|
-
// which is what keeps a leaf-only repository from collapsing to zero.
|
|
227
|
-
for (let j = 0; j < n; j++)
|
|
228
|
-
next[j] += (0.85 * rank[i]) / n;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
for (const j of out[i])
|
|
232
|
-
next[j] += (0.85 * rank[i]) / out[i].length;
|
|
233
|
-
}
|
|
234
|
-
rank = next;
|
|
235
|
-
}
|
|
236
|
-
return files
|
|
237
|
-
.map((file, i) => ({
|
|
238
|
-
file,
|
|
239
|
-
score: rank[i],
|
|
240
|
-
// A query is a statement about what the caller is looking for, so a file
|
|
241
|
-
// that names it is worth more than any amount of graph weight: the ranking
|
|
242
|
-
// still orders the matches among themselves.
|
|
243
|
-
match: needle ? `${file.path} ${file.symbols.join(" ")}`.toLowerCase().includes(needle) : false,
|
|
244
|
-
}))
|
|
245
|
-
.sort((a, b) => Number(b.match) - Number(a.match) || b.score - a.score || a.file.path.localeCompare(b.file.path))
|
|
246
|
-
.map((entry) => entry.file);
|
|
247
|
-
}
|
|
248
|
-
/** Render a ranked map inside a character budget (roughly 4 chars per token). */
|
|
249
|
-
export function renderRepoMap(files, options = {}) {
|
|
250
|
-
const budget = Math.max(200, Math.min(8000, options.budget ?? 1200));
|
|
251
|
-
const chars = budget * 4;
|
|
252
|
-
const ranked = rankFiles(files, options.query);
|
|
253
|
-
const lines = [];
|
|
254
|
-
let used = 0;
|
|
255
|
-
for (const file of ranked) {
|
|
256
|
-
const symbols = file.symbols.slice(0, 8).join(", ");
|
|
257
|
-
const line = symbols ? `${file.path}: ${symbols}` : `${file.path}`;
|
|
258
|
-
if (used + line.length + 1 > chars)
|
|
259
|
-
break;
|
|
260
|
-
lines.push(line);
|
|
261
|
-
used += line.length + 1;
|
|
262
|
-
}
|
|
263
|
-
if (lines.length === 0)
|
|
264
|
-
return "(no source files found)";
|
|
265
|
-
const omitted = ranked.length - lines.length;
|
|
266
|
-
if (omitted > 0)
|
|
267
|
-
lines.push(`… ${omitted} more file${omitted === 1 ? "" : "s"} not shown`);
|
|
268
|
-
return lines.join("\n");
|
|
269
|
-
}
|
|
270
|
-
let projectRoot;
|
|
271
|
-
/** The session's project directory, supplied by the caller for the same reason
|
|
272
|
-
* claims take one: a tool cannot know which project the process is in. */
|
|
273
|
-
export function setRepoRoot(next) {
|
|
274
|
-
projectRoot = next;
|
|
275
|
-
}
|
|
276
|
-
export const repoMapTool = defineTool({
|
|
277
|
-
name: "repo_map",
|
|
278
|
-
label: "repo_map",
|
|
279
|
-
description: "A ranked outline of the repository: files by importance with their top declarations. Use it to find where something lives before reading files, instead of listing directories.",
|
|
280
|
-
promptSnippet: "repo_map: ranked file outline of the project",
|
|
281
|
-
parameters: Type.Object({
|
|
282
|
-
path: Type.Optional(Type.String({ description: "Subdirectory to map, relative to the project root" })),
|
|
283
|
-
query: Type.Optional(Type.String({ description: "Keyword to bias the ranking, e.g. 'billing'" })),
|
|
284
|
-
budget: Type.Optional(Type.Number({ description: "Token budget, default 1200" })),
|
|
285
|
-
}),
|
|
286
|
-
async execute(_toolCallId, params) {
|
|
287
|
-
const root = projectRoot?.() ?? process.cwd();
|
|
288
|
-
const files = collectFiles(root, params.path ?? ".");
|
|
289
|
-
const text = renderRepoMap(files, { budget: params.budget, query: params.query });
|
|
290
|
-
return { content: [{ type: "text", text }], details: { files: files.length } };
|
|
291
|
-
},
|
|
292
|
-
});
|
package/dist/rtk.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output compression for tool results (the "read the tool, keep the token" layer).
|
|
3
|
-
*
|
|
4
|
-
* A coding agent spends most of its context on tool output, and most of that output is
|
|
5
|
-
* not information: a `git diff` where one line in forty changed, a `grep` that matched
|
|
6
|
-
* the same import in fifty files, an `ls -R` of a `node_modules` nobody asked about.
|
|
7
|
-
* Paying full price for it shortens the effective context and, on a metered gateway,
|
|
8
|
-
* costs money for text the model will never reason about.
|
|
9
|
-
*
|
|
10
|
-
* This runs as a `tool_result` handler, which is the right seam: the result has already
|
|
11
|
-
* been produced and stored, so compression changes only what the model is asked to read,
|
|
12
|
-
* never what the tool actually returned. That distinction matters — a `bash` command that
|
|
13
|
-
* wrote a file must never be re-run or altered because its *report* was shortened.
|
|
14
|
-
*
|
|
15
|
-
* Two rules keep it honest:
|
|
16
|
-
*
|
|
17
|
-
* - **Never invent content.** Every line the model sees came from the tool. A summary
|
|
18
|
-
* says how many lines were dropped and where they went; it never paraphrases them.
|
|
19
|
-
* - **Never hide a failure.** An error result, or a result the tool marked truncated,
|
|
20
|
-
* is passed through untouched. Losing the one line that explains why a command failed
|
|
21
|
-
* costs far more than the tokens it saves.
|
|
22
|
-
*
|
|
23
|
-
* Off by default. Compression is a trade the caller makes with knowledge of their task:
|
|
24
|
-
* a search whose whole point is a count needs every line, and a run that is skimming a
|
|
25
|
-
* large diff does not want any.
|
|
26
|
-
*/
|
|
27
|
-
/** Tools whose output is worth compressing. Reads and writes are already exact. */
|
|
28
|
-
const COMPRESSIBLE = new Set(["bash", "grep", "find", "ls", "bash_output", "powershell"]);
|
|
29
|
-
/**
|
|
30
|
-
* Tools whose lines are *progress* rather than *findings*, and so may be run-collapsed.
|
|
31
|
-
*
|
|
32
|
-
* The distinction is what the numbers mean. In `Compiling module 10 done in 30ms` the
|
|
33
|
-
* numbers are a counter and the lines are interchangeable — keeping two loses nothing.
|
|
34
|
-
* In `src/auth.ts:42: const x = 1` the numbers identify *which* finding this is, and
|
|
35
|
-
* collapsing a hundred of them into a notice deletes the answer to the search the user
|
|
36
|
-
* just ran. A grep must therefore keep every line it matched; only its redundant blanks
|
|
37
|
-
* and an elision of the middle may go.
|
|
38
|
-
*/
|
|
39
|
-
const RUN_COLLAPSIBLE = new Set(["bash", "bash_output", "powershell"]);
|
|
40
|
-
/** Below this many lines there is nothing worth summarising. */
|
|
41
|
-
export const MIN_LINES = 40;
|
|
42
|
-
/** How many lines of a long run to keep at each end. */
|
|
43
|
-
export const KEEP_HEAD = 12;
|
|
44
|
-
export const KEEP_TAIL = 8;
|
|
45
|
-
/** A run must be at least this long before it is worth collapsing. */
|
|
46
|
-
export const MIN_RUN = 6;
|
|
47
|
-
/**
|
|
48
|
-
* True when a line carries no information beyond its neighbours' shape.
|
|
49
|
-
*
|
|
50
|
-
* Deliberately narrow. A "useful" test that guessed would eventually drop the one line
|
|
51
|
-
* that mattered, so only plainly redundant lines qualify: repeats of the line above,
|
|
52
|
-
* separators, and blank runs.
|
|
53
|
-
*/
|
|
54
|
-
function isRedundant(line, previous) {
|
|
55
|
-
const trimmed = line.trim();
|
|
56
|
-
if (trimmed === "")
|
|
57
|
-
return true;
|
|
58
|
-
// A rule of only punctuation or dashes is a separator, not content.
|
|
59
|
-
if (/^[-=*_~+#]{3,}$/.test(trimmed))
|
|
60
|
-
return true;
|
|
61
|
-
if (previous !== undefined && line === previous)
|
|
62
|
-
return true;
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* The shape of a line, with every number run replaced by a placeholder.
|
|
67
|
-
*
|
|
68
|
-
* Numbers move; the text around them is what makes two lines the same kind of line. A
|
|
69
|
-
* trailing-only match misses the common case — `Compiling module 10 done in 30ms` has
|
|
70
|
-
* its counter in the middle, so every line looked unique and none were collapsed.
|
|
71
|
-
*
|
|
72
|
-
* Collapsing is defensible for tool output in a way it is not for, say, a claim: the
|
|
73
|
-
* output is *reproducible*. The tool can be run again, and the notice below always says
|
|
74
|
-
* how many lines went and what shape they had, so nothing is lost that cannot be
|
|
75
|
-
* recovered by re-running the command.
|
|
76
|
-
*/
|
|
77
|
-
function shapeOf(line) {
|
|
78
|
-
return line.replace(/\d+/g, "#").trimEnd();
|
|
79
|
-
}
|
|
80
|
-
function collapseNumericRuns(lines) {
|
|
81
|
-
const out = [];
|
|
82
|
-
let collapsed = 0;
|
|
83
|
-
let i = 0;
|
|
84
|
-
while (i < lines.length) {
|
|
85
|
-
const key = shapeOf(lines[i]);
|
|
86
|
-
// A line with no digits has no counter to move, so a run of them is content that
|
|
87
|
-
// happens to repeat — not progress. Only a changing number makes a run collapsible.
|
|
88
|
-
if (key === "" || !/\d/.test(lines[i])) {
|
|
89
|
-
out.push(lines[i]);
|
|
90
|
-
i += 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
let j = i + 1;
|
|
94
|
-
while (j < lines.length && shapeOf(lines[j]) === key && /\d/.test(lines[j]))
|
|
95
|
-
j += 1;
|
|
96
|
-
const run = j - i;
|
|
97
|
-
if (run >= MIN_RUN) {
|
|
98
|
-
out.push(lines[i]);
|
|
99
|
-
out.push(` … ${run - 2} more line${run - 2 === 1 ? "" : "s"} of the same shape …`);
|
|
100
|
-
out.push(lines[j - 1]);
|
|
101
|
-
collapsed += 1;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
for (let k = i; k < j; k += 1)
|
|
105
|
-
out.push(lines[k]);
|
|
106
|
-
}
|
|
107
|
-
i = j;
|
|
108
|
-
}
|
|
109
|
-
return { out, collapsed };
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Compress one tool result, or return undefined to leave it exactly as it was.
|
|
113
|
-
*
|
|
114
|
-
* Returning `undefined` rather than the original content is the difference between
|
|
115
|
-
* "nothing to do" and "replace it with itself": the handler contract treats an omitted
|
|
116
|
-
* field as untouched, and re-sending identical content burns the work for no gain.
|
|
117
|
-
*/
|
|
118
|
-
export function compressToolResult(text, details, options = {}) {
|
|
119
|
-
// A result the tool already truncated has been shaped by a rule that understands the
|
|
120
|
-
// tool; second-guessing it here only risks dropping the tail it deliberately kept.
|
|
121
|
-
if (details?.truncated)
|
|
122
|
-
return undefined;
|
|
123
|
-
const lines = text.split("\n");
|
|
124
|
-
if (lines.length < MIN_LINES)
|
|
125
|
-
return undefined;
|
|
126
|
-
// 1. Drop plainly redundant lines, keeping count so the notice can be truthful.
|
|
127
|
-
const kept = [];
|
|
128
|
-
let droppedRedundant = 0;
|
|
129
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
130
|
-
if (isRedundant(lines[i], kept[kept.length - 1])) {
|
|
131
|
-
// Keep one blank line as a paragraph break; collapse only the runs.
|
|
132
|
-
if (lines[i].trim() === "" && kept[kept.length - 1]?.trim() !== "") {
|
|
133
|
-
kept.push("");
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
droppedRedundant += 1;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
kept.push(lines[i]);
|
|
140
|
-
}
|
|
141
|
-
// 2. Collapse repeated-shape runs — but only for tools whose numbers are progress
|
|
142
|
-
// rather than content. A grep's line numbers identify findings; collapsing them
|
|
143
|
-
// would delete the answer. See RUN_COLLAPSIBLE.
|
|
144
|
-
const mayCollapseRuns = options.toolName === undefined || RUN_COLLAPSIBLE.has(options.toolName);
|
|
145
|
-
const { out: collapsedLines, collapsed } = mayCollapseRuns ? collapseNumericRuns(kept) : { out: kept, collapsed: 0 };
|
|
146
|
-
// 3. Keep both ends if it is still too long. The head carries the command and its
|
|
147
|
-
// first results; the tail carries the failure or the summary, which is usually
|
|
148
|
-
// why anyone reads a long output at all.
|
|
149
|
-
const head = options.head ?? KEEP_HEAD;
|
|
150
|
-
const tail = options.tail ?? KEEP_TAIL;
|
|
151
|
-
let final = collapsedLines;
|
|
152
|
-
let elided = 0;
|
|
153
|
-
if (collapsedLines.length > head + tail + MIN_RUN) {
|
|
154
|
-
elided = collapsedLines.length - head - tail;
|
|
155
|
-
final = [...collapsedLines.slice(0, head), ` … ${elided} line${elided === 1 ? "" : "s"} omitted (${head} kept from the top, ${tail} from the end) …`, ...collapsedLines.slice(collapsedLines.length - tail)];
|
|
156
|
-
}
|
|
157
|
-
const saved = droppedRedundant + collapsed;
|
|
158
|
-
if (saved === 0 && elided === 0)
|
|
159
|
-
return undefined;
|
|
160
|
-
return {
|
|
161
|
-
text: final.join("\n"),
|
|
162
|
-
stats: { before: lines.length, after: final.length, collapsed },
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Build the extension that applies compression to tool results.
|
|
167
|
-
*
|
|
168
|
-
* `enabled` is read per call rather than captured, so toggling it mid-session takes
|
|
169
|
-
* effect on the next tool result instead of the next process.
|
|
170
|
-
*/
|
|
171
|
-
export function createRtkExtension(options) {
|
|
172
|
-
return (hxf) => {
|
|
173
|
-
hxf.on("tool_result", (event) => {
|
|
174
|
-
if (!options.enabled())
|
|
175
|
-
return undefined;
|
|
176
|
-
// A failure is never compressed. The line that explains it is exactly the
|
|
177
|
-
// line a heuristic is most likely to decide is redundant.
|
|
178
|
-
if (event.isError)
|
|
179
|
-
return undefined;
|
|
180
|
-
if (!COMPRESSIBLE.has(event.toolName))
|
|
181
|
-
return undefined;
|
|
182
|
-
const textPart = event.content.find((c) => c.type === "text");
|
|
183
|
-
if (!textPart || textPart.type !== "text")
|
|
184
|
-
return undefined;
|
|
185
|
-
const details = event.details;
|
|
186
|
-
const result = compressToolResult(textPart.text, details, { toolName: event.toolName });
|
|
187
|
-
if (!result)
|
|
188
|
-
return undefined;
|
|
189
|
-
const content = event.content.map((c) => (c === textPart ? { type: "text", text: result.text } : c));
|
|
190
|
-
return { content };
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
}
|
package/dist/sandbox.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
function isExecutable(path) {
|
|
5
|
-
try {
|
|
6
|
-
accessSync(path, constants.X_OK);
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
/** Which sandbox this machine can actually provide, if any. */
|
|
14
|
-
export function detectSandbox(platform = process.platform, pathEnv = process.env.PATH ?? "") {
|
|
15
|
-
const dirs = pathEnv.split(":").filter(Boolean);
|
|
16
|
-
const find = (name) => dirs.map((dir) => join(dir, name)).find(isExecutable);
|
|
17
|
-
if (platform === "linux") {
|
|
18
|
-
const bwrap = find("bwrap");
|
|
19
|
-
// Installed is not the same as usable: a container or a kernel with
|
|
20
|
-
// unprivileged user namespaces disabled has the binary and still refuses to
|
|
21
|
-
// create a namespace. Probing costs a few milliseconds and turns a confusing
|
|
22
|
-
// runtime failure into an honest "not available here".
|
|
23
|
-
if (!bwrap)
|
|
24
|
-
return undefined;
|
|
25
|
-
const probe = spawnSync(bwrap, ["--ro-bind", "/", "/", "--", "true"], { stdio: "ignore", timeout: 3000 });
|
|
26
|
-
return probe.status === 0 ? "bubblewrap" : undefined;
|
|
27
|
-
}
|
|
28
|
-
if (platform === "darwin")
|
|
29
|
-
return find("sandbox-exec") ? "seatbelt" : undefined;
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The argv that re-runs `argv` inside the sandbox.
|
|
34
|
-
*
|
|
35
|
-
* The bind list is the security boundary: `/` is mounted read-only, then the
|
|
36
|
-
* project and the CLI's state directory are mounted writable over it. `/tmp` is
|
|
37
|
-
* a fresh tmpfs, so a build cannot leave anything behind outside the project.
|
|
38
|
-
* Network stays on — a coding agent that cannot reach a registry is not usable —
|
|
39
|
-
* and `--unshare-pid` is left off because some toolchains need to see the
|
|
40
|
-
* process tree.
|
|
41
|
-
*/
|
|
42
|
-
export function sandboxArgv(kind, paths) {
|
|
43
|
-
if (kind === "bubblewrap") {
|
|
44
|
-
return [
|
|
45
|
-
"bwrap",
|
|
46
|
-
"--die-with-parent",
|
|
47
|
-
"--new-session",
|
|
48
|
-
"--ro-bind",
|
|
49
|
-
"/",
|
|
50
|
-
"/",
|
|
51
|
-
"--dev",
|
|
52
|
-
"/dev",
|
|
53
|
-
"--proc",
|
|
54
|
-
"/proc",
|
|
55
|
-
"--tmpfs",
|
|
56
|
-
"/tmp",
|
|
57
|
-
"--bind",
|
|
58
|
-
paths.cwd,
|
|
59
|
-
paths.cwd,
|
|
60
|
-
"--bind",
|
|
61
|
-
paths.agentDir,
|
|
62
|
-
paths.agentDir,
|
|
63
|
-
"--chdir",
|
|
64
|
-
paths.cwd,
|
|
65
|
-
"--",
|
|
66
|
-
...paths.argv,
|
|
67
|
-
];
|
|
68
|
-
}
|
|
69
|
-
const profile = [
|
|
70
|
-
"(version 1)",
|
|
71
|
-
"(deny default)",
|
|
72
|
-
"(allow process*)",
|
|
73
|
-
"(allow signal)",
|
|
74
|
-
"(allow sysctl-read)",
|
|
75
|
-
"(allow mach-lookup)",
|
|
76
|
-
"(allow network*)",
|
|
77
|
-
"(allow file-read*)",
|
|
78
|
-
`(allow file-write* (subpath "${paths.cwd}") (subpath "${paths.agentDir}") (subpath "/tmp") (subpath "/private/tmp") (subpath "/private/var/folders"))`,
|
|
79
|
-
"",
|
|
80
|
-
].join("\n");
|
|
81
|
-
return ["sandbox-exec", "-p", profile, ...paths.argv];
|
|
82
|
-
}
|
|
83
|
-
/** A sentence for the banner, so the mode is never a secret. */
|
|
84
|
-
export function describeSandbox(kind) {
|
|
85
|
-
return kind === "bubblewrap"
|
|
86
|
-
? "sandbox: bubblewrap — project writable, rest of the filesystem read-only"
|
|
87
|
-
: "sandbox: seatbelt — project writable, rest of the filesystem read-only";
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Re-execute this process inside the sandbox and return its exit code.
|
|
91
|
-
*
|
|
92
|
-
* Called once, before any session exists, and guarded by HOWTOFIX_SANDBOXED so
|
|
93
|
-
* the re-executed process does not try to wrap itself again.
|
|
94
|
-
*/
|
|
95
|
-
export function enterSandbox(kind, paths) {
|
|
96
|
-
const result = spawnSync(sandboxArgv(kind, paths)[0], sandboxArgv(kind, paths).slice(1), {
|
|
97
|
-
stdio: "inherit",
|
|
98
|
-
env: { ...process.env, HOWTOFIX_SANDBOXED: "1" },
|
|
99
|
-
});
|
|
100
|
-
if (result.error) {
|
|
101
|
-
console.error(`sandbox: could not start ${kind}: ${result.error.message}`);
|
|
102
|
-
return 1;
|
|
103
|
-
}
|
|
104
|
-
return result.status ?? 1;
|
|
105
|
-
}
|