howtofixcli 0.7.8 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-63ZCQDXH.js +6 -0
  5. package/dist/chunk-6FUWMLWO.js +1 -0
  6. package/dist/chunk-7CHDNTH6.js +1 -0
  7. package/dist/chunk-ACOMNDUL.js +185 -0
  8. package/dist/chunk-AEVPET7X.js +2 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-BHVBO4SC.js +1 -0
  11. package/dist/chunk-FPXVDP32.js +8 -0
  12. package/dist/chunk-GIUYIKH3.js +109 -0
  13. package/dist/chunk-GQCAJ56I.js +5 -0
  14. package/dist/chunk-GXB7RZHH.js +1 -0
  15. package/dist/chunk-IGEY3AWF.js +1 -0
  16. package/dist/chunk-ILDDDLRC.js +1 -0
  17. package/dist/chunk-JFYVRWJI.js +2 -0
  18. package/dist/chunk-MVLCGU3G.js +1 -0
  19. package/dist/chunk-N345KWWK.js +1 -0
  20. package/dist/chunk-NBQFPCRT.js +1 -0
  21. package/dist/chunk-OIPSLIZE.js +5 -0
  22. package/dist/chunk-ORJ2QZBG.js +3 -0
  23. package/dist/chunk-PCALBZOO.js +2 -0
  24. package/dist/chunk-PDNBGUYP.js +2 -0
  25. package/dist/chunk-S656RE4A.js +1 -0
  26. package/dist/chunk-TSEKPGK4.js +1 -0
  27. package/dist/chunk-U6CI6KXN.js +34 -0
  28. package/dist/chunk-UTYFLLSM.js +1 -0
  29. package/dist/chunk-V3R4FYTJ.js +1 -0
  30. package/dist/chunk-V656XVXV.js +8 -0
  31. package/dist/chunk-WKUJ7CFO.js +1 -0
  32. package/dist/chunk-WOT6VMZA.js +1 -0
  33. package/dist/chunk-WUPU6LTD.js +4 -0
  34. package/dist/chunk-WX3V5P7U.js +5 -0
  35. package/dist/chunk-ZKZ6WCI3.js +2 -0
  36. package/dist/main.js +94 -536
  37. package/package.json +15 -62
  38. package/dist/account-credential.js +0 -136
  39. package/dist/account-credential.test.js +0 -38
  40. package/dist/activity.js +0 -124
  41. package/dist/agent-defs.js +0 -237
  42. package/dist/agent-limiter.js +0 -68
  43. package/dist/agents.js +0 -554
  44. package/dist/anim.js +0 -361
  45. package/dist/args.js +0 -228
  46. package/dist/at-file.js +0 -28
  47. package/dist/brand-extension.js +0 -87
  48. package/dist/brand.js +0 -76
  49. package/dist/browser.js +0 -58
  50. package/dist/cache-report.js +0 -139
  51. package/dist/checkpoint.js +0 -437
  52. package/dist/claims.js +0 -167
  53. package/dist/cli-auth.js +0 -142
  54. package/dist/cli-checkin.js +0 -120
  55. package/dist/cli-credentials.js +0 -254
  56. package/dist/clipboard.js +0 -149
  57. package/dist/collapse.js +0 -129
  58. package/dist/commands.js +0 -301
  59. package/dist/compaction-guard.js +0 -81
  60. package/dist/context-extension.js +0 -17
  61. package/dist/dcp/commands.js +0 -172
  62. package/dist/dcp/compress-state.js +0 -139
  63. package/dist/dcp/compress.js +0 -329
  64. package/dist/dcp/dcp-extension.js +0 -288
  65. package/dist/dcp/dedup.js +0 -93
  66. package/dist/dcp/ids.js +0 -189
  67. package/dist/dcp/index.js +0 -25
  68. package/dist/dcp/prompts.js +0 -68
  69. package/dist/dcp/protected-content.js +0 -221
  70. package/dist/dcp/protected.js +0 -161
  71. package/dist/dcp/prune.js +0 -418
  72. package/dist/dcp/state.js +0 -176
  73. package/dist/device-id.js +0 -90
  74. package/dist/device-id.test.js +0 -23
  75. package/dist/errors.js +0 -63
  76. package/dist/extensions.js +0 -48
  77. package/dist/fatal.js +0 -35
  78. package/dist/feed.js +0 -498
  79. package/dist/fs-atomic.js +0 -54
  80. package/dist/gateway-errors.js +0 -90
  81. package/dist/git.js +0 -230
  82. package/dist/goal-extension.js +0 -88
  83. package/dist/goal.js +0 -222
  84. package/dist/hooks-extension.js +0 -176
  85. package/dist/hooks.js +0 -443
  86. package/dist/howtofix-account.js +0 -715
  87. package/dist/hud-line.js +0 -153
  88. package/dist/hwid.js +0 -130
  89. package/dist/interrupt.js +0 -60
  90. package/dist/json-events.js +0 -50
  91. package/dist/level-style.js +0 -112
  92. package/dist/loop-guard.js +0 -95
  93. package/dist/lsp-tools.js +0 -32
  94. package/dist/lsp.js +0 -347
  95. package/dist/mcp-config.js +0 -85
  96. package/dist/mcp-lifecycle.js +0 -147
  97. package/dist/model-catalog.js +0 -217
  98. package/dist/model-resolve.js +0 -65
  99. package/dist/notes.js +0 -78
  100. package/dist/oauth-login.js +0 -568
  101. package/dist/paths.js +0 -61
  102. package/dist/permissions.js +0 -298
  103. package/dist/plan-mode.js +0 -363
  104. package/dist/preset-command.js +0 -227
  105. package/dist/preset-extension.js +0 -35
  106. package/dist/preset-mcp.js +0 -292
  107. package/dist/presets.js +0 -727
  108. package/dist/prompt-files.js +0 -103
  109. package/dist/prompt-text.js +0 -41
  110. package/dist/proxy.js +0 -137
  111. package/dist/quality/ast.js +0 -196
  112. package/dist/quality/cognitive.js +0 -83
  113. package/dist/quality/cyclomatic.js +0 -30
  114. package/dist/quality/halstead.js +0 -83
  115. package/dist/quality/index.js +0 -18
  116. package/dist/quality/length.js +0 -3
  117. package/dist/quality/maintainability.js +0 -15
  118. package/dist/quality/nesting.js +0 -57
  119. package/dist/quality/report.js +0 -803
  120. package/dist/quality/smells.js +0 -67
  121. package/dist/quality-tool.js +0 -63
  122. package/dist/read-only-commands.js +0 -597
  123. package/dist/repo-map.js +0 -292
  124. package/dist/rtk.js +0 -193
  125. package/dist/sandbox.js +0 -105
  126. package/dist/schema.js +0 -214
  127. package/dist/session-lifecycle.js +0 -19
  128. package/dist/session-retention.js +0 -111
  129. package/dist/session.js +0 -446
  130. package/dist/sessions.js +0 -100
  131. package/dist/settings.js +0 -148
  132. package/dist/skills.js +0 -35
  133. package/dist/slop-tool.js +0 -96
  134. package/dist/spend-cap.js +0 -43
  135. package/dist/subagent.js +0 -171
  136. package/dist/tar.js +0 -146
  137. package/dist/theme.js +0 -79
  138. package/dist/thinking.js +0 -45
  139. package/dist/todo.js +0 -114
  140. package/dist/tool-loadout.js +0 -97
  141. package/dist/tool-render.js +0 -769
  142. package/dist/tools.js +0 -176
  143. package/dist/ui.js +0 -4757
  144. package/dist/update.js +0 -533
  145. package/dist/usage-segment.js +0 -69
  146. package/dist/web-tools.js +0 -46
  147. package/dist/web.js +0 -265
  148. package/dist/welcome.js +0 -179
  149. package/dist/workflow-engine.js +0 -246
  150. package/dist/workflow-progress.js +0 -446
  151. package/dist/workflow-view.js +0 -195
  152. package/dist/workflow.js +0 -349
package/dist/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
- }