howtofixcli 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-63ZCQDXH.js +6 -0
  5. package/dist/chunk-6FUWMLWO.js +1 -0
  6. package/dist/chunk-7CHDNTH6.js +1 -0
  7. package/dist/chunk-ACOMNDUL.js +185 -0
  8. package/dist/chunk-AEVPET7X.js +2 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-BHVBO4SC.js +1 -0
  11. package/dist/chunk-FPXVDP32.js +8 -0
  12. package/dist/chunk-GIUYIKH3.js +109 -0
  13. package/dist/chunk-GQCAJ56I.js +5 -0
  14. package/dist/chunk-GXB7RZHH.js +1 -0
  15. package/dist/chunk-IGEY3AWF.js +1 -0
  16. package/dist/chunk-ILDDDLRC.js +1 -0
  17. package/dist/chunk-JFYVRWJI.js +2 -0
  18. package/dist/chunk-MVLCGU3G.js +1 -0
  19. package/dist/chunk-N345KWWK.js +1 -0
  20. package/dist/chunk-NBQFPCRT.js +1 -0
  21. package/dist/chunk-OIPSLIZE.js +5 -0
  22. package/dist/chunk-ORJ2QZBG.js +3 -0
  23. package/dist/chunk-PCALBZOO.js +2 -0
  24. package/dist/chunk-PDNBGUYP.js +2 -0
  25. package/dist/chunk-S656RE4A.js +1 -0
  26. package/dist/chunk-TSEKPGK4.js +1 -0
  27. package/dist/chunk-U6CI6KXN.js +34 -0
  28. package/dist/chunk-UTYFLLSM.js +1 -0
  29. package/dist/chunk-V3R4FYTJ.js +1 -0
  30. package/dist/chunk-V656XVXV.js +8 -0
  31. package/dist/chunk-WKUJ7CFO.js +1 -0
  32. package/dist/chunk-WOT6VMZA.js +1 -0
  33. package/dist/chunk-WUPU6LTD.js +4 -0
  34. package/dist/chunk-WX3V5P7U.js +5 -0
  35. package/dist/chunk-ZKZ6WCI3.js +2 -0
  36. package/dist/main.js +94 -536
  37. package/package.json +15 -62
  38. package/dist/account-credential.js +0 -136
  39. package/dist/account-credential.test.js +0 -38
  40. package/dist/activity.js +0 -124
  41. package/dist/agent-defs.js +0 -237
  42. package/dist/agent-limiter.js +0 -68
  43. package/dist/agents.js +0 -554
  44. package/dist/anim.js +0 -361
  45. package/dist/args.js +0 -228
  46. package/dist/at-file.js +0 -28
  47. package/dist/brand-extension.js +0 -87
  48. package/dist/brand.js +0 -76
  49. package/dist/browser.js +0 -58
  50. package/dist/cache-report.js +0 -139
  51. package/dist/checkpoint.js +0 -437
  52. package/dist/claims.js +0 -167
  53. package/dist/cli-auth.js +0 -142
  54. package/dist/cli-checkin.js +0 -116
  55. package/dist/cli-credentials.js +0 -254
  56. package/dist/clipboard.js +0 -149
  57. package/dist/collapse.js +0 -129
  58. package/dist/commands.js +0 -301
  59. package/dist/compaction-guard.js +0 -81
  60. package/dist/context-extension.js +0 -17
  61. package/dist/dcp/commands.js +0 -172
  62. package/dist/dcp/compress-state.js +0 -139
  63. package/dist/dcp/compress.js +0 -329
  64. package/dist/dcp/dcp-extension.js +0 -288
  65. package/dist/dcp/dedup.js +0 -93
  66. package/dist/dcp/ids.js +0 -189
  67. package/dist/dcp/index.js +0 -25
  68. package/dist/dcp/prompts.js +0 -68
  69. package/dist/dcp/protected-content.js +0 -221
  70. package/dist/dcp/protected.js +0 -161
  71. package/dist/dcp/prune.js +0 -418
  72. package/dist/dcp/state.js +0 -176
  73. package/dist/device-id.js +0 -90
  74. package/dist/device-id.test.js +0 -23
  75. package/dist/errors.js +0 -63
  76. package/dist/extensions.js +0 -48
  77. package/dist/fatal.js +0 -35
  78. package/dist/feed.js +0 -498
  79. package/dist/fs-atomic.js +0 -54
  80. package/dist/gateway-errors.js +0 -90
  81. package/dist/git.js +0 -230
  82. package/dist/goal-extension.js +0 -88
  83. package/dist/goal.js +0 -222
  84. package/dist/hooks-extension.js +0 -176
  85. package/dist/hooks.js +0 -443
  86. package/dist/howtofix-account.js +0 -715
  87. package/dist/hud-line.js +0 -153
  88. package/dist/hwid.js +0 -130
  89. package/dist/interrupt.js +0 -60
  90. package/dist/json-events.js +0 -50
  91. package/dist/level-style.js +0 -112
  92. package/dist/loop-guard.js +0 -95
  93. package/dist/lsp-tools.js +0 -32
  94. package/dist/lsp.js +0 -347
  95. package/dist/mcp-config.js +0 -85
  96. package/dist/mcp-lifecycle.js +0 -147
  97. package/dist/model-catalog.js +0 -217
  98. package/dist/model-resolve.js +0 -65
  99. package/dist/notes.js +0 -78
  100. package/dist/oauth-login.js +0 -568
  101. package/dist/paths.js +0 -61
  102. package/dist/permissions.js +0 -298
  103. package/dist/plan-mode.js +0 -363
  104. package/dist/preset-command.js +0 -227
  105. package/dist/preset-extension.js +0 -35
  106. package/dist/preset-mcp.js +0 -292
  107. package/dist/presets.js +0 -727
  108. package/dist/prompt-files.js +0 -103
  109. package/dist/prompt-text.js +0 -41
  110. package/dist/proxy.js +0 -137
  111. package/dist/quality/ast.js +0 -196
  112. package/dist/quality/cognitive.js +0 -83
  113. package/dist/quality/cyclomatic.js +0 -30
  114. package/dist/quality/halstead.js +0 -83
  115. package/dist/quality/index.js +0 -18
  116. package/dist/quality/length.js +0 -3
  117. package/dist/quality/maintainability.js +0 -15
  118. package/dist/quality/nesting.js +0 -57
  119. package/dist/quality/report.js +0 -803
  120. package/dist/quality/smells.js +0 -67
  121. package/dist/quality-tool.js +0 -63
  122. package/dist/read-only-commands.js +0 -597
  123. package/dist/repo-map.js +0 -292
  124. package/dist/rtk.js +0 -193
  125. package/dist/sandbox.js +0 -105
  126. package/dist/schema.js +0 -214
  127. package/dist/session-lifecycle.js +0 -19
  128. package/dist/session-retention.js +0 -111
  129. package/dist/session.js +0 -446
  130. package/dist/sessions.js +0 -100
  131. package/dist/settings.js +0 -148
  132. package/dist/skills.js +0 -35
  133. package/dist/slop-tool.js +0 -96
  134. package/dist/spend-cap.js +0 -43
  135. package/dist/subagent.js +0 -171
  136. package/dist/tar.js +0 -146
  137. package/dist/theme.js +0 -79
  138. package/dist/thinking.js +0 -45
  139. package/dist/todo.js +0 -114
  140. package/dist/tool-loadout.js +0 -97
  141. package/dist/tool-render.js +0 -769
  142. package/dist/tools.js +0 -176
  143. package/dist/ui.js +0 -4751
  144. package/dist/update.js +0 -533
  145. package/dist/usage-segment.js +0 -69
  146. package/dist/web-tools.js +0 -46
  147. package/dist/web.js +0 -265
  148. package/dist/welcome.js +0 -179
  149. package/dist/workflow-engine.js +0 -246
  150. package/dist/workflow-progress.js +0 -446
  151. package/dist/workflow-view.js +0 -195
  152. package/dist/workflow.js +0 -349
package/dist/collapse.js DELETED
@@ -1,129 +0,0 @@
1
- /**
2
- * Collapse a run of consecutive read/search calls into one summary line.
3
- *
4
- * The shape is Claude Code's (`src/utils/collapseReadSearch.ts`): consecutive
5
- * Read/Grep/Glob calls become `Read 3 files, searched for 2 patterns`, and the
6
- * run breaks when anything else appears — assistant text, an edit, a shell
7
- * command that is not a search. A model exploring a repository issues those
8
- * calls in bursts, and a feed that lists each one buries the two lines that
9
- * matter under twenty that do not.
10
- *
11
- * Three details are taken from the reference rather than invented:
12
- *
13
- * 1. **Plural per category, not per group.** `1 file` / `2 files` is decided
14
- * next to the count it belongs to, because a group can hold one read and
15
- * three greps and the two need different words.
16
- * 2. **Active and finished read differently.** `Reading 2 files` while the run
17
- * is live, `Read 2 files` once it settles. That is the same information the
18
- * per-call spinner carries, one level up.
19
- * 3. **The break is on assistant text, not on a message boundary.** Two calls
20
- * from the same assistant turn that sit either side of a paragraph are two
21
- * thoughts, and merging them would claim a grouping the model did not make.
22
- *
23
- * Pure over a list of calls: no clock, no session, no rendering. What it
24
- * decides is which calls belong together and what the line says; whether that
25
- * line is drawn is the feed's business.
26
- */
27
- /** The categories the summary counts, one word pair each. */
28
- const CATEGORIES = {
29
- read: { active: "Reading", done: "Read", one: "file", many: "files" },
30
- search: { active: "Searching for", done: "Searched for", one: "pattern", many: "patterns" },
31
- list: { active: "Listing", done: "Listed", one: "directory", many: "directories" },
32
- };
33
- /**
34
- * Which category a tool belongs to, or `undefined` when it does not collapse.
35
- *
36
- * `read` reads a file; `grep` and `find` search; `ls` lists. Everything else
37
- * breaks the run — `edit` and `write` change the tree, and a `bash` call is not
38
- * classified here because this repository's bash tool is a general shell, not
39
- * the search/read primitive Claude Code splits its own out into.
40
- */
41
- function categoryOf(toolName) {
42
- if (toolName === "read")
43
- return "read";
44
- if (toolName === "grep" || toolName === "find" || toolName === "glob")
45
- return "search";
46
- if (toolName === "ls")
47
- return "list";
48
- return undefined;
49
- }
50
- /**
51
- * Find every run worth collapsing, in order.
52
- *
53
- * A run of one is NOT a group: `Read(src/ui.ts)` says more than `Read 1 file`,
54
- * and replacing a single call with a summary loses the path for no gain. That
55
- * threshold is the whole reason this returns a list of runs rather than a
56
- * rewritten transcript.
57
- */
58
- export function collapseGroups(calls, live = false) {
59
- const groups = [];
60
- let run;
61
- const flush = () => {
62
- if (!run || run.indices.length < 2) {
63
- run = undefined;
64
- return;
65
- }
66
- groups.push({
67
- start: run.indices[0],
68
- indices: run.indices,
69
- counts: run.counts,
70
- summary: summaryOf(run.counts, live),
71
- });
72
- run = undefined;
73
- };
74
- for (const [index, call] of calls.entries()) {
75
- const category = categoryOf(call.toolName);
76
- // Anything that is not a read/search/list BREAKS the run, which is the
77
- // reference's rule: an edit, a shell command or a paragraph of assistant
78
- // text sits between two explorations and says they were separate.
79
- if (category === undefined) {
80
- flush();
81
- continue;
82
- }
83
- if (!run)
84
- run = { indices: [], counts: {} };
85
- run.indices.push(index);
86
- run.counts[category] = (run.counts[category] ?? 0) + 1;
87
- }
88
- flush();
89
- return groups;
90
- }
91
- /**
92
- * `Reading 2 files, searching for 3 patterns`.
93
- *
94
- * Only the first clause is capitalised, which is how the reference builds it:
95
- * the verb is chosen by whether it is the first part being pushed, so the line
96
- * reads as one sentence rather than a list of headings.
97
- */
98
- /**
99
- * The tense is a parameter rather than an options object: there is one call
100
- * site and it forwards the same flag the group was built with, so a named bag
101
- * would add a layer without adding a distinction. The gate flags the boolean
102
- * anyway, and it is in the baseline - the rule is right about the general case
103
- * and this is the case it does not fit.
104
- */
105
- function summaryOf(counts, live) {
106
- const parts = [];
107
- // Fixed clause order, taken from the reference (search, read, list, repl):
108
- // the summary reads the same for the same work whatever order the model
109
- // happened to issue it in, which is the point of a summary line.
110
- for (const category of ["search", "read", "list"]) {
111
- const count = counts[category];
112
- if (count === undefined)
113
- continue;
114
- const spec = CATEGORIES[category];
115
- const verb = live ? spec.active : spec.done;
116
- const word = count === 1 ? spec.one : spec.many;
117
- parts.push(`${parts.length === 0 ? verb : verb.toLowerCase()} ${count} ${word}`);
118
- }
119
- return parts.join(", ");
120
- }
121
- // `collapseSummary` was exported here and used by nothing: `collapseGroups`
122
- // already puts the finished line in `CollapseGroup.summary`, which is where
123
- // every caller reads it from. `check-wiring.mjs` caught the dead export, and the
124
- // fix is to delete it rather than to add a caller that exists to justify it.
125
- //
126
- // The count is spelled out rather than shown as `×3` because the categories
127
- // differ: `Read ×3` is fine for one kind of call and meaningless for a mixed
128
- // run, and a summary that only works for the easy case is one that has to be
129
- // special-cased later.
package/dist/commands.js DELETED
@@ -1,301 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { AGENT_DIR } from "./paths.js";
5
- import { APP_TITLE } from "./brand.js";
6
- import { reportError } from "./errors.js";
7
- import { detectSandbox } from "./sandbox.js";
8
- const MIN_NODE = [22, 19];
9
- /** Everything the CLI needs from the machine, checked without a model call. */
10
- export async function collectChecks(gatewayUrl = "https://api.howtofix.id/api/status") {
11
- const checks = [];
12
- const [major, minor] = process.versions.node.split(".").map(Number);
13
- const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
14
- checks.push({
15
- name: "node",
16
- ok: nodeOk,
17
- detail: `${process.versions.node} (needs >= ${MIN_NODE[0]}.${MIN_NODE[1]})`,
18
- });
19
- const git = spawnSync("git", ["--version"], { encoding: "utf8" });
20
- checks.push({
21
- name: "git",
22
- ok: git.status === 0,
23
- detail: git.status === 0 ? git.stdout.trim() : "not found on PATH",
24
- });
25
- const sandbox = detectSandbox();
26
- checks.push({
27
- name: "sandbox",
28
- ok: Boolean(sandbox),
29
- detail: sandbox ?? "no working bubblewrap (Linux) or sandbox-exec (macOS); --sandbox unavailable",
30
- optional: true,
31
- });
32
- let stateOk = false;
33
- let stateDetail = AGENT_DIR;
34
- try {
35
- const probe = join(AGENT_DIR, `.doctor-${process.pid}`);
36
- writeFileSync(probe, "ok");
37
- unlinkSync(probe);
38
- stateOk = true;
39
- }
40
- catch (error) {
41
- stateDetail = `${AGENT_DIR} is not writable: ${error instanceof Error ? error.message : String(error)}`;
42
- }
43
- checks.push({ name: "state dir", ok: stateOk, detail: stateDetail });
44
- const authPath = join(AGENT_DIR, "auth.json");
45
- let authDetail = "no auth.json — run `howtofix` and sign in";
46
- let authOk = false;
47
- try {
48
- const parsed = JSON.parse(readFileSync(authPath, "utf8"));
49
- const providers = Object.keys(parsed).length;
50
- authOk = providers > 0;
51
- authDetail = authOk ? `${providers} provider credential${providers === 1 ? "" : "s"}` : authDetail;
52
- }
53
- catch {
54
- // Missing or unreadable: the sentence above already says what to do.
55
- }
56
- checks.push({ name: "auth", ok: authOk, detail: authDetail, optional: true });
57
- try {
58
- const started = Date.now();
59
- const response = await fetch(gatewayUrl, { signal: AbortSignal.timeout(5000) });
60
- checks.push({
61
- name: "gateway",
62
- ok: response.ok,
63
- detail: `${gatewayUrl} → ${response.status} in ${Date.now() - started}ms`,
64
- optional: true,
65
- });
66
- }
67
- catch (error) {
68
- checks.push({
69
- name: "gateway",
70
- ok: false,
71
- detail: `${gatewayUrl} unreachable: ${error instanceof Error ? error.message : String(error)}`,
72
- optional: true,
73
- });
74
- }
75
- return checks;
76
- }
77
- export async function runDoctor() {
78
- const checks = await collectChecks();
79
- const width = Math.max(...checks.map((check) => check.name.length));
80
- for (const check of checks) {
81
- const mark = check.ok ? "ok " : check.optional ? "warn" : "FAIL";
82
- console.log(` ${mark} ${check.name.padEnd(width)} ${check.detail}`);
83
- }
84
- const failed = checks.filter((check) => !check.ok && !check.optional);
85
- if (failed.length > 0) {
86
- console.log(`\n${failed.length} problem${failed.length === 1 ? "" : "s"} must be fixed before a session will work.`);
87
- return 1;
88
- }
89
- return 0;
90
- }
91
- /** Configured providers and their models, straight from models.json. */
92
- export function runModels(root = AGENT_DIR) {
93
- const path = join(root, "models.json");
94
- let raw;
95
- try {
96
- raw = readFileSync(path, "utf8");
97
- }
98
- catch (error) {
99
- const code = error.code;
100
- if (code === "ENOENT") {
101
- console.log(` no models.json at ${path} — run \`howtofix /connect\` to pull the catalogue`);
102
- }
103
- else {
104
- reportError(APP_TITLE, "models", error);
105
- }
106
- return 1;
107
- }
108
- let parsed;
109
- try {
110
- parsed = JSON.parse(raw);
111
- }
112
- catch (error) {
113
- // A file that exists but does not parse is a different problem from a
114
- // missing one: /connect will not fix it, and saying "no models.json" sends
115
- // the user looking for a file that is already there.
116
- console.error(`${APP_TITLE}: models: ${path} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
117
- console.error(" fix the file, or delete it and run `howtofix /connect` to write a new one");
118
- return 1;
119
- }
120
- const providers = parsed.providers ?? {};
121
- let total = 0;
122
- for (const [name, provider] of Object.entries(providers)) {
123
- const ids = (provider.models ?? []).map((model) => model.id).filter(Boolean);
124
- total += ids.length;
125
- console.log(` ${name} (${ids.length})`);
126
- for (const id of ids)
127
- console.log(` ${id}`);
128
- }
129
- if (total === 0) {
130
- console.log(" no models configured");
131
- return 1;
132
- }
133
- return 0;
134
- }
135
- /** Saved sessions, newest first. */
136
- export function listSessionFiles(limit = 15, root = AGENT_DIR) {
137
- const sessionsRoot = join(root, "sessions");
138
- let projects;
139
- try {
140
- projects = readdirSync(sessionsRoot);
141
- }
142
- catch {
143
- return [];
144
- }
145
- const rows = [];
146
- for (const project of projects) {
147
- const dir = join(sessionsRoot, project);
148
- let files;
149
- try {
150
- files = readdirSync(dir);
151
- }
152
- catch {
153
- continue;
154
- }
155
- for (const file of files) {
156
- if (!file.endsWith(".jsonl"))
157
- continue;
158
- const full = join(dir, file);
159
- try {
160
- const info = statSync(full);
161
- rows.push({ file: full, project, modified: info.mtimeMs, bytes: info.size });
162
- }
163
- catch {
164
- // A session removed mid-walk is not an error worth reporting.
165
- }
166
- }
167
- }
168
- return rows.sort((a, b) => b.modified - a.modified).slice(0, limit);
169
- }
170
- export function runSessions(argv) {
171
- const searchIndex = argv.indexOf("--search");
172
- if (searchIndex >= 0) {
173
- const query = argv[searchIndex + 1]?.trim();
174
- if (!query) {
175
- console.error(`${APP_TITLE}: sessions: --search needs a word or phrase`);
176
- return 2;
177
- }
178
- const hits = searchSessions(query);
179
- if (hits.length === 0) {
180
- console.log(` no session mentions "${query}"`);
181
- return 0;
182
- }
183
- for (const hit of hits) {
184
- const when = new Date(hit.row.modified).toISOString().replace("T", " ").slice(0, 16);
185
- console.log(` ${when} ${hit.row.project} (${hit.matches} hit${hit.matches === 1 ? "" : "s"})`);
186
- console.log(` ${hit.row.file}`);
187
- console.log(` ${hit.snippet}`);
188
- }
189
- return 0;
190
- }
191
- const limitIndex = argv.indexOf("--limit");
192
- const limit = limitIndex >= 0 ? Number(argv[limitIndex + 1]) || 15 : 15;
193
- const rows = listSessionFiles(limit);
194
- if (rows.length === 0) {
195
- console.log(` no saved sessions under ${join(AGENT_DIR, "sessions")}`);
196
- return 0;
197
- }
198
- const exportIndex = argv.indexOf("--export");
199
- if (exportIndex >= 0) {
200
- const which = Number(argv[exportIndex + 1]);
201
- if (!Number.isInteger(which) || which < 1 || which > rows.length) {
202
- console.error(`${APP_TITLE}: sessions: --export needs a number from the list (1-${rows.length})`);
203
- return 2;
204
- }
205
- const row = rows[which - 1];
206
- try {
207
- console.log(exportSessionMarkdown(row.file));
208
- }
209
- catch (error) {
210
- reportError(APP_TITLE, "sessions", error);
211
- return 1;
212
- }
213
- return 0;
214
- }
215
- rows.forEach((row, index) => {
216
- const when = new Date(row.modified).toISOString().replace("T", " ").slice(0, 16);
217
- const kb = Math.round(row.bytes / 1024);
218
- console.log(` ${String(index + 1).padStart(2)} ${when} ${String(kb).padStart(6)}k ${row.project}`);
219
- console.log(` ${row.file}`);
220
- });
221
- return 0;
222
- }
223
- /** Text of one JSONL line, whether its content is a string or content blocks. */
224
- function lineText(line) {
225
- try {
226
- const entry = JSON.parse(line);
227
- const content = entry.message?.content;
228
- if (typeof content === "string")
229
- return content.replace(/\s+/g, " ").trim();
230
- if (Array.isArray(content)) {
231
- return content
232
- .map((block) => (block && typeof block === "object" && "text" in block ? String(block.text) : ""))
233
- .join(" ")
234
- .replace(/\s+/g, " ")
235
- .trim();
236
- }
237
- }
238
- catch {
239
- // A line that does not parse is still searchable as raw text.
240
- }
241
- return line.replace(/\s+/g, " ").trim();
242
- }
243
- /** Sessions whose text mentions `query`, with the first matching line as evidence. */
244
- export function searchSessions(query, limit = 10, root = AGENT_DIR) {
245
- const needle = query.toLowerCase();
246
- const hits = [];
247
- for (const row of listSessionFiles(500, root)) {
248
- let text;
249
- try {
250
- text = readFileSync(row.file, "utf8");
251
- }
252
- catch {
253
- continue;
254
- }
255
- let matches = 0;
256
- let snippet = "";
257
- for (const line of text.split("\n")) {
258
- if (!line.toLowerCase().includes(needle))
259
- continue;
260
- matches++;
261
- if (!snippet)
262
- snippet = lineText(line).slice(0, 160);
263
- }
264
- if (matches > 0)
265
- hits.push({ row, matches, snippet });
266
- if (hits.length >= limit)
267
- break;
268
- }
269
- return hits;
270
- }
271
- /** A session as markdown, for pasting into an issue or a chat. */
272
- export function exportSessionMarkdown(file) {
273
- const out = [`# Session ${file.split(/[\\/]/).pop() ?? file}`, ""];
274
- for (const line of readFileSync(file, "utf8").split("\n")) {
275
- if (!line.trim())
276
- continue;
277
- let entry;
278
- try {
279
- entry = JSON.parse(line);
280
- }
281
- catch {
282
- continue;
283
- }
284
- if (entry.type !== "message")
285
- continue;
286
- const text = lineText(line);
287
- if (!text)
288
- continue;
289
- out.push(`## ${entry.message?.role ?? "unknown"}`, "", text, "");
290
- }
291
- return out.join("\n").trimEnd();
292
- }
293
- /** True when the file exists and is a directory — used by doctor's callers. */
294
- export function stateDirExists() {
295
- try {
296
- return existsSync(AGENT_DIR) && statSync(AGENT_DIR).isDirectory();
297
- }
298
- catch {
299
- return false;
300
- }
301
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Consecutive failures before the breaker trips.
3
- *
4
- * Same value as the reference. Three is enough to distinguish a broken
5
- * compaction from a transient API error, and small enough that the wasted calls
6
- * stay negligible.
7
- */
8
- export const MAX_CONSECUTIVE_FAILURES = 3;
9
- export function emptyGuardState() {
10
- return { consecutiveFailures: 0, tripped: false };
11
- }
12
- /**
13
- * Whether a compaction should be refused.
14
- *
15
- * Pure, so the policy is testable without a session: manual compaction is always
16
- * allowed, and an untripped breaker allows everything.
17
- */
18
- export function shouldSkipCompaction(state, reason) {
19
- if (reason === "manual")
20
- return false;
21
- return state.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES;
22
- }
23
- /** Fold one failure into the state. Aborts are not failures. */
24
- export function noteFailure(state, aborted) {
25
- if (aborted)
26
- return state;
27
- const consecutiveFailures = state.consecutiveFailures + 1;
28
- return {
29
- consecutiveFailures,
30
- tripped: state.tripped || consecutiveFailures >= MAX_CONSECUTIVE_FAILURES,
31
- };
32
- }
33
- /** Fold one success into the state: the compaction worked, so the count resets. */
34
- export function noteSuccess() {
35
- return emptyGuardState();
36
- }
37
- /** The message the user sees once, when the breaker trips. */
38
- export function breakerMessage(state) {
39
- return [
40
- `Auto-compaction failed ${state.consecutiveFailures} times in a row, so it is switched off for this session.`,
41
- "The context is over the limit and compaction cannot recover it. Start a new session, or switch to a model with a larger context window.",
42
- "Manual /compact is still allowed.",
43
- ].join(" ");
44
- }
45
- let state = emptyGuardState();
46
- export function compactionGuardState() {
47
- return state;
48
- }
49
- /** Test seam: forget the counts. */
50
- export function resetCompactionGuard() {
51
- state = emptyGuardState();
52
- }
53
- /**
54
- * Wire the breaker into the session.
55
- *
56
- * The refusal happens in `session_before_compact`, which the SDK consults before
57
- * it runs the summariser — so a refused attempt costs no API call at all.
58
- */
59
- export function createCompactionGuardExtension() {
60
- return (hxf) => {
61
- hxf.on("session_start", async () => {
62
- state = emptyGuardState();
63
- });
64
- hxf.on("session_compact", async () => {
65
- state = noteSuccess();
66
- });
67
- hxf.on("session_compact_failed", async (event, ctx) => {
68
- const before = state;
69
- state = noteFailure(state, event.aborted);
70
- // Report the trip once, when it happens, rather than on every later turn.
71
- if (!before.tripped && state.tripped && ctx?.ui) {
72
- ctx.ui.notify(breakerMessage(state), "warning");
73
- }
74
- });
75
- hxf.on("session_before_compact", async (event) => {
76
- if (!shouldSkipCompaction(state, event.reason))
77
- return undefined;
78
- return { cancel: true };
79
- });
80
- };
81
- }
@@ -1,17 +0,0 @@
1
- import { readMessageText, writeMessageText } from "./prompt-text.js";
2
- import { renderNotes } from "./notes.js";
3
- export function createContextExtension() {
4
- return (hxf) => {
5
- hxf.on("context_with_system", async (event) => {
6
- const notes = renderNotes();
7
- if (!notes)
8
- return undefined;
9
- const head = event.messages[0];
10
- if (!head || head.role !== "system")
11
- return undefined;
12
- const combined = `${readMessageText(head.content)}\n\n${notes}`;
13
- const messages = event.messages.map((message) => (message === head ? { ...head, content: writeMessageText(head.content, combined) } : message));
14
- return { messages };
15
- });
16
- };
17
- }