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/update.js DELETED
@@ -1,533 +0,0 @@
1
- /**
2
- * Keeping an npm-installed HowToFix current.
3
- *
4
- * The shape is taken from Claude Code's updater, because the pitfalls it solves
5
- * are not obvious:
6
- *
7
- * 1. **The check must run from the home directory.** `npm view` reads the
8
- * nearest `.npmrc`, so running it inside a project lets a checked-out
9
- * repository point the CLI at an attacker's registry — and then the update
10
- * installs whatever that registry serves. Every npm invocation here runs
11
- * with `cwd` set to the user's home.
12
- * 2. **The check must never block the session.** It is a network call on the
13
- * startup path, so it is bounded by a short timeout and its failure is
14
- * silent: an offline machine must not notice that anything was attempted.
15
- * 3. **An install must be exclusive.** Two sessions starting at once would
16
- * otherwise run two `npm install -g` against the same tree; a lock file with
17
- * a stale timeout keeps one of them from corrupting the other's install.
18
- * 4. **Only an npm install can be updated this way.** A copy run from a source
19
- * checkout, or bundled some other way, must be told so rather than have
20
- * `npm install -g` write a second copy somewhere else.
21
- *
22
- * The version comparison is local (no semver dependency) and deliberately
23
- * forgiving: a version string this cannot parse is treated as "not newer", so a
24
- * weird tag can never trigger an install.
25
- */
26
- import { execFile } from "node:child_process";
27
- import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
28
- import { homedir } from "node:os";
29
- import { delimiter, dirname, join, sep } from "node:path";
30
- /** The package an installed copy updates from. */
31
- export const PACKAGE_NAME = "howtofixcli";
32
- /** The npm launchers to look for on PATH, in the order npm itself installs them. */
33
- const NPM_BIN_NAMES = ["npm", "npm.cmd", "npm.exe"];
34
- /**
35
- * How long the registry may take before the check is abandoned.
36
- *
37
- * This runs on the startup path through `npm view`, and the first `npm` of a
38
- * session is not a cheap call: node loads `npm-cli.js` and its whole module
39
- * graph before it touches the network. On a cold Windows box behind a virus
40
- * scanner that alone can take several seconds, and the registry lookup that
41
- * follows is a second network round trip on top. The old five seconds was
42
- * measured on a warm cache; it expired before the answer arrived often enough
43
- * that the check looked unreliable, which is exactly what it was.
44
- */
45
- export const CHECK_TIMEOUT_MS = 20_000;
46
- /** How many times a check may be attempted before it is reported as lost. */
47
- export const CHECK_ATTEMPTS = 2;
48
- /** An install gets longer than a check: it downloads a tarball. */
49
- export const INSTALL_TIMEOUT_MS = 120_000;
50
- /**
51
- * How long a lock may sit before it is considered abandoned.
52
- *
53
- * A killed process leaves its lock behind, and without a staleness rule the
54
- * next update would never run. Ten minutes is longer than any install here and
55
- * shorter than a user's patience.
56
- */
57
- export const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
58
- /**
59
- * How long a half-written lock is respected.
60
- *
61
- * The file is created and then filled with a pid; a reader that catches the gap
62
- * sees an empty file. Five seconds is far longer than that gap and far shorter
63
- * than a user's patience.
64
- */
65
- export const BROKEN_LOCK_GRACE_MS = 5 * 1000;
66
- /**
67
- * Compare two dotted version strings.
68
- *
69
- * Returns -1, 0 or 1. Anything unparseable returns 0 ("equal"), so an odd tag
70
- * cannot be mistaken for a newer release. Pre-release suffixes are ignored:
71
- * this updater only ever installs the `latest` tag, which is not a pre-release.
72
- */
73
- export function compareVersions(left, right) {
74
- const parse = (value) => {
75
- const core = value.trim().split("-")[0]?.split("+")[0] ?? "";
76
- if (!/^\d+(\.\d+)*$/.test(core))
77
- return undefined;
78
- return core.split(".").map((part) => Number(part));
79
- };
80
- const a = parse(left);
81
- const b = parse(right);
82
- if (!a || !b)
83
- return 0;
84
- const length = Math.max(a.length, b.length);
85
- for (let index = 0; index < length; index += 1) {
86
- const x = a[index] ?? 0;
87
- const y = b[index] ?? 0;
88
- if (x > y)
89
- return 1;
90
- if (x < y)
91
- return -1;
92
- }
93
- return 0;
94
- }
95
- /**
96
- * Whether `candidate` is a strictly newer release than `current`.
97
- *
98
- * A pre-release candidate is never treated as an upgrade. The registry's
99
- * `latest` tag is stable by convention, so a `-beta` string reaching here means
100
- * the tag was moved by mistake — and a broken beta silently replacing a working
101
- * install is a far worse outcome than missing one release. Core versions are
102
- * still compared, so a pre-release of an *older* core stays older.
103
- */
104
- export function isNewer(candidate, current) {
105
- if (candidate.includes("-"))
106
- return false;
107
- return compareVersions(candidate, current) > 0;
108
- }
109
- /**
110
- * Run a command and capture its output, never throwing.
111
- *
112
- * `error.code` is reported twice, deliberately: as the numeric exit code when it
113
- * is a number, and as `errorCode` when it is a string. A spawn that never
114
- * started — no such file, not executable, refused on Windows — carries a string
115
- * there and leaves `stderr` empty, which is the only way to tell "the program
116
- * did not run" apart from "the program ran and failed". Reading `stderr` alone
117
- * reported both as a registry that did not answer.
118
- */
119
- const run = (command, args, options) => {
120
- return new Promise((resolve) => {
121
- execFile(command, args, { cwd: options.cwd, timeout: options.timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
122
- const raw = error ? error.code : undefined;
123
- const code = typeof raw === "number" ? raw : error ? 1 : 0;
124
- const errorCode = typeof raw === "string" ? raw : undefined;
125
- resolve({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), ...(errorCode ? { errorCode } : {}) });
126
- });
127
- });
128
- };
129
- /**
130
- * How a spawn that never started is told apart from a registry that did not answer.
131
- *
132
- * A spawn that fails reports a *string* code in `error.code` — `ENOENT` for a
133
- * program that is not there, `EACCES` for one that is not executable, `EINVAL`
134
- * for a `.cmd` refused on Windows — and leaves `stderr` empty. Reading `stderr`
135
- * therefore classified every one of those as "the registry did not answer",
136
- * which sends a user to look at their network when the problem is on their disk.
137
- * The code is the signal; `stderr` is only a fallback for a runner that reports
138
- * it there.
139
- */
140
- const MISSING_COMMAND = new Set(["ENOENT", "EACCES", "EINVAL", "EPERM"]);
141
- /**
142
- * The npm to run, which is not always the one on PATH.
143
- *
144
- * `npm install -g howtofixcli` puts the `howtofix` bin next to npm's own, but a
145
- * GUI launcher, a shim directory, or an nvm-style shell hook can leave `node`
146
- * resolvable and `npm` not — and then the update check reported a network-style
147
- * failure for a machine that simply had no npm in front of it.
148
- *
149
- * npm's own entry point is preferred over the launcher on PATH, because the
150
- * launcher on Windows is `npm.cmd`: `execFile` refuses a `.cmd` without a shell
151
- * (the fix for CVE-2024-27980), so spawning it fails with `EINVAL` before npm
152
- * ever runs. `npm-cli.js` is a JavaScript file, which `execFile` starts with the
153
- * running node on every platform. It sits beside node in every layout npm
154
- * itself installs — POSIX `<node>/../lib/node_modules/npm`, Windows
155
- * `<node>/node_modules/npm` — so both are probed.
156
- *
157
- * The PATH lookup is still the fallback, so a version-managed npm that is not
158
- * laid out this way is found. Only npm is probed: the updater knows the shape of
159
- * an npm installation, and guessing at other package managers would be worse
160
- * than saying plainly that npm is missing.
161
- */
162
- export function resolveNpm() {
163
- const nodeDir = dirname(process.execPath);
164
- // POSIX layout, then the Windows layout, which keeps npm under the node dir.
165
- for (const relative of [
166
- ["..", "lib", "node_modules", "npm", "bin", "npm-cli.js"],
167
- ["node_modules", "npm", "bin", "npm-cli.js"],
168
- ]) {
169
- const npmCli = join(nodeDir, ...relative);
170
- if (existsSync(npmCli))
171
- return { command: process.execPath, args: [npmCli] };
172
- }
173
- const onPath = process.env.PATH?.split(delimiter).filter(Boolean) ?? [];
174
- for (const dir of onPath) {
175
- for (const name of NPM_BIN_NAMES) {
176
- const candidate = join(dir, name);
177
- // A `.cmd`/`.bat` cannot be spawned without a shell, so a shim in one of
178
- // those forms is skipped rather than returned to fail later.
179
- if (name.endsWith(".cmd") || name.endsWith(".bat"))
180
- continue;
181
- if (existsSync(candidate))
182
- return { command: candidate, args: [] };
183
- }
184
- }
185
- // Windows was giving up here. npm's launcher there is `npm.cmd`, which
186
- // `execFile` refuses to start without a shell, so the loop above skips it — and
187
- // if the `npm-cli.js` probes missed, the answer was "npm was not found" on a
188
- // machine whose npm is right there on PATH. The `.cmd` is still not spawnable
189
- // directly, so it is run *through* cmd.exe, which is how npm itself invokes it.
190
- if (process.platform === "win32") {
191
- for (const dir of onPath) {
192
- for (const name of NPM_BIN_NAMES) {
193
- if (!name.endsWith(".cmd") && !name.endsWith(".bat"))
194
- continue;
195
- const candidate = join(dir, name);
196
- if (existsSync(candidate)) {
197
- return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${candidate}"`] };
198
- }
199
- }
200
- }
201
- }
202
- return undefined;
203
- }
204
- /** A missing npm is a fact about the machine, not a failed registry lookup. */
205
- export const NPM_MISSING_MESSAGE = "could not check for a newer version: npm was not found, so run `npm install -g howtofixcli@latest` yourself";
206
- /** A registry that answered nothing is transient, and the wording says so. */
207
- export const CHECK_FAILED_MESSAGE = "could not check for a newer version: the registry did not answer";
208
- /**
209
- * A one-line note that a check did not happen, for the feed.
210
- *
211
- * The check itself stays silent when it succeeds and the copy is current — "you
212
- * are up to date" on every start is noise. A check that *failed* is different:
213
- * staying silent about it is what made a broken lookup indistinguishable from a
214
- * working one, and the user's only signal was that the update never appeared.
215
- * The note is one line, says only what was tried, and does not pretend the
216
- * absence of an update means the software is current.
217
- */
218
- export function checkFailedNote(check) {
219
- if (check.latest !== undefined)
220
- return undefined;
221
- return `${check.message} (this is why no update was offered; /update retries it)`;
222
- }
223
- /** Whether the failure was "there is no npm here" rather than "npm said no". */
224
- function npmMissing(result) {
225
- if (result.code === -2)
226
- return true;
227
- const code = result.errorCode ?? (result.stderr ?? "").trim();
228
- return MISSING_COMMAND.has(code);
229
- }
230
- /** Where the update lock lives. */
231
- function lockPath() {
232
- const configured = process.env.HOWTOFIX_CODING_AGENT_DIR?.trim();
233
- const dir = configured || join(homedir(), ".howtofix", "agent");
234
- try {
235
- mkdirSync(dir, { recursive: true });
236
- }
237
- catch {
238
- // A read-only home is not fatal: the lock then just never blocks.
239
- }
240
- return join(dir, "update.lock");
241
- }
242
- /**
243
- * Whether a process with this id still exists.
244
- *
245
- * `EPERM` is the interesting answer: it means the process is there but belongs
246
- * to another user. Treating that as "dead" would let a second install start
247
- * while the first is still writing, which is the race the lock exists to stop.
248
- */
249
- export function pidAlive(pid) {
250
- if (!Number.isInteger(pid) || pid <= 0)
251
- return false;
252
- try {
253
- process.kill(pid, 0);
254
- return true;
255
- }
256
- catch (error) {
257
- return error?.code === "EPERM";
258
- }
259
- }
260
- function readLock(path, now) {
261
- try {
262
- const ageMs = now - statSync(path).mtimeMs;
263
- const raw = readFileSync(path, "utf8").trim();
264
- const pid = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : undefined;
265
- return { pid, ageMs };
266
- }
267
- catch {
268
- return undefined;
269
- }
270
- }
271
- /**
272
- * Whether a lock still represents work in progress.
273
- *
274
- * Age alone is not enough. An update that crashed, or that the user cancelled
275
- * by closing the terminal, leaves the file behind; waiting the full ten minutes
276
- * before trying again is exactly the wrong answer, and it produced the message
277
- * "another HowToFix is already installing an update" when nothing was. The pid
278
- * written inside the file is the fact that settles it: a dead holder means a
279
- * free lock, immediately.
280
- */
281
- function lockIsHeld(state) {
282
- if (!state)
283
- return false;
284
- if (state.pid === undefined) {
285
- // Half-written file: give the writer a moment, then treat it as debris.
286
- return state.ageMs < BROKEN_LOCK_GRACE_MS;
287
- }
288
- if (state.pid === process.pid)
289
- return false;
290
- if (!pidAlive(state.pid))
291
- return false;
292
- // A live holder that has been inside `npm install` for longer than the
293
- // timeout is stuck, not working. Taking over is better than blocking forever.
294
- return state.ageMs < LOCK_TIMEOUT_MS;
295
- }
296
- /**
297
- * Take the update lock, or report that someone else holds it.
298
- *
299
- * A lock whose holder is gone is taken over at once; one held by a live process
300
- * blocks until {@link LOCK_TIMEOUT_MS}. The state is re-read immediately before
301
- * the file is removed, so two processes that both saw a stale lock cannot both
302
- * believe they own it.
303
- */
304
- export function acquireUpdateLock(now = Date.now()) {
305
- const path = lockPath();
306
- try {
307
- if (existsSync(path)) {
308
- if (lockIsHeld(readLock(path, now)))
309
- return false;
310
- if (lockIsHeld(readLock(path, now)))
311
- return false;
312
- unlinkSync(path);
313
- }
314
- writeFileSync(path, String(process.pid), { mode: 0o600 });
315
- return true;
316
- }
317
- catch {
318
- // An unwritable home means no lock; an update is still better attempted
319
- // than silently skipped, so this reports success rather than blocking.
320
- return true;
321
- }
322
- }
323
- /** Release the update lock. Never throws, and never removes another's lock. */
324
- export function releaseUpdateLock() {
325
- const path = lockPath();
326
- try {
327
- const state = readLock(path, Date.now());
328
- if (state && state.pid !== undefined && state.pid !== process.pid)
329
- return;
330
- unlinkSync(path);
331
- }
332
- catch {
333
- // Already gone, or never written.
334
- }
335
- }
336
- /**
337
- * What the registry says the newest release is, or why it could not be read.
338
- *
339
- * Retried once. The failure this catches is a dropped connection or a stalled
340
- * DNS lookup in the first second of a session — transient by nature, and the
341
- * only chance to notice it used to be a restart, because nothing else asks
342
- * again before the process exits.
343
- *
344
- * `no-npm` is not retried: npm's absence is a fact about the machine, and
345
- * running the lookup twice would not change it.
346
- */
347
- async function probeLatest(options) {
348
- const runner = options?.runner ?? run;
349
- const npm = resolveNpm();
350
- // No npm anywhere on this machine: the user can fix that, so it is named.
351
- if (!npm)
352
- return { reason: "no-npm" };
353
- let last = "no-answer";
354
- for (let attempt = 0; attempt < CHECK_ATTEMPTS; attempt += 1) {
355
- // The runner is documented never to throw, and `checkForUpdate` is documented
356
- // never to throw either — but a promise executor that throws (an `execFile`
357
- // that rejects its own arguments, say) rejects the returned promise, and this
358
- // runs on the startup path. A spawn that could not start is not an answer from
359
- // the registry, so it is classified as one, not propagated.
360
- let result;
361
- try {
362
- result = await runner(npm.command, [...npm.args, "view", `${PACKAGE_NAME}@latest`, "version", "--prefer-online"], {
363
- cwd: homedir(),
364
- timeoutMs: options?.timeoutMs ?? CHECK_TIMEOUT_MS,
365
- });
366
- }
367
- catch {
368
- last = "no-answer";
369
- continue;
370
- }
371
- if (result.code !== 0) {
372
- // npm exists but could not run: a launcher that is not executable, or a
373
- // shim whose target is gone. Both are the same advice as a missing npm,
374
- // and neither is worth a second attempt.
375
- if (npmMissing(result))
376
- return { reason: "no-npm" };
377
- last = "no-answer";
378
- continue;
379
- }
380
- const version = result.stdout.trim().split("\n").pop()?.trim() ?? "";
381
- if (/^\d+\.\d+/.test(version))
382
- return { version };
383
- last = "no-answer";
384
- }
385
- return { reason: last };
386
- }
387
- /** The newest published version, or `undefined` when it could not be read. */
388
- export async function latestVersion(options) {
389
- return (await probeLatest(options)).version;
390
- }
391
- /**
392
- * Ask the registry whether a newer release exists.
393
- *
394
- * Never throws and never blocks for long: this runs on the startup path, so an
395
- * offline machine, a slow registry, or a missing `npm` must all be quiet
396
- * non-events rather than a delay or a crash.
397
- */
398
- export async function checkForUpdate(current, options) {
399
- const probe = await probeLatest(options);
400
- if (!probe.version) {
401
- // The two failures read the same to a user and could not be more different
402
- // to fix: one is "install npm", the other is "try again". Saying which is
403
- // the whole value of the message.
404
- return {
405
- current,
406
- available: false,
407
- message: probe.reason === "no-npm" ? NPM_MISSING_MESSAGE : CHECK_FAILED_MESSAGE,
408
- };
409
- }
410
- const latest = probe.version;
411
- if (!isNewer(latest, current)) {
412
- return { current, latest, available: false, message: `HowToFix ${current} is up to date` };
413
- }
414
- return {
415
- current,
416
- latest,
417
- available: true,
418
- message: `HowToFix ${latest} is available (you have ${current})`,
419
- };
420
- }
421
- /**
422
- * The line that explains an npm failure.
423
- *
424
- * npm prints deprecation warnings on stderr and a real failure after them, so
425
- * taking the last line reported `npm warn deprecated node-domexception@1.0.0`
426
- * as the reason an install failed. Warnings are dropped, error lines win, and
427
- * the code that is left is mapped to the action a user can actually take.
428
- */
429
- export function npmFailureDetail(stdout, stderr, errorCode) {
430
- const lines = `${stderr}\n${stdout}`
431
- .split("\n")
432
- .map((line) => line.trim())
433
- .filter(Boolean)
434
- .filter((line) => !/^npm warn\b/i.test(line));
435
- const errors = lines.filter((line) => /^npm error\b/i.test(line) || /\bERR!/i.test(line));
436
- const codeLine = errors.find((line) => /^npm error code\b/i.test(line));
437
- const detail = (codeLine ?? errors.at(-1) ?? lines.at(-1) ?? errorCode ?? "")
438
- .replace(/^npm error\s*/i, "")
439
- .trim();
440
- // The hint reads the whole output: the code that names the cause is often on
441
- // an earlier line than the path or syscall that failed last.
442
- const evidence = `${lines.join(" ")} ${errorCode ?? ""}`;
443
- let hint = "";
444
- if (/EACCES|EPERM|permission denied/i.test(evidence)) {
445
- hint = " — npm could not replace its files; close other HowToFix windows and run the terminal as Administrator";
446
- }
447
- else if (/EBUSY|EEXIST|ENOTEMPTY|resource busy|being used by another process/i.test(evidence)) {
448
- hint = " — a file is in use; close other HowToFix windows and try again";
449
- }
450
- else if (/ENOSPC/i.test(evidence)) {
451
- hint = " — the disk is full";
452
- }
453
- else if (/ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ECONNREFUSED/i.test(evidence)) {
454
- hint = " — npm could not reach the registry";
455
- }
456
- return { detail, hint };
457
- }
458
- /**
459
- * Install the newest release globally.
460
- *
461
- * The lock is the point: two sessions starting together would otherwise run two
462
- * installs over the same tree. A held lock is not a failure — it means another
463
- * process is already doing exactly this — so it is reported as such and the
464
- * caller can simply wait for the next start.
465
- */
466
- export async function installUpdate(options) {
467
- const runner = options?.runner ?? run;
468
- if (!acquireUpdateLock()) {
469
- return { ok: false, message: "another HowToFix is already installing an update" };
470
- }
471
- try {
472
- const npm = resolveNpm();
473
- if (!npm) {
474
- return { ok: false, message: "npm was not found, so run `npm install -g howtofixcli@latest` yourself" };
475
- }
476
- const spec = options?.version ? `${PACKAGE_NAME}@${options.version}` : `${PACKAGE_NAME}@latest`;
477
- const result = await runner(npm.command, [...npm.args, "install", "-g", spec], {
478
- cwd: homedir(),
479
- timeoutMs: options?.timeoutMs ?? INSTALL_TIMEOUT_MS,
480
- });
481
- if (result.code !== 0) {
482
- const { detail, hint } = npmFailureDetail(result.stdout, result.stderr, result.errorCode);
483
- return { ok: false, message: `the update did not install${hint}${detail ? `: ${detail}` : ""}` };
484
- }
485
- return { ok: true, version: options?.version, message: "updated; it takes effect the next time HowToFix starts" };
486
- }
487
- catch (cause) {
488
- return { ok: false, message: `the update failed: ${cause instanceof Error ? cause.message : String(cause)}` };
489
- }
490
- finally {
491
- releaseUpdateLock();
492
- }
493
- }
494
- /**
495
- * Whether the running copy is one `npm install -g` can update.
496
- *
497
- * The test is on the launcher's path, resolved through symlinks, and *not* on
498
- * `import.meta.url`. That is not a style preference: esbuild substitutes
499
- * `import.meta.url` with the **output file's** URL at build time, so in the
500
- * published bundle it is forever the path of the machine that built it — a
501
- * release would never recognise itself as installed and could never update.
502
- * `process.argv[1]` is the file the user actually ran; `realpath` follows the
503
- * `bin` symlink npm creates into `lib/node_modules/howtofixcli/`.
504
- *
505
- * An npm install — global or as a dependency — lives under
506
- * `node_modules/howtofixcli/`, so that is what the resolved path is tested for.
507
- * A copy run from a source checkout has no business being replaced by a global
508
- * install, and the caller is told so instead of having a second copy written.
509
- */
510
- export function isInstalledCopy(launcher = process.argv[1]) {
511
- if (!launcher)
512
- return false;
513
- let path = launcher;
514
- try {
515
- // npm's global `bin` entry is a symlink to the package's own file.
516
- path = realpathSync(launcher);
517
- }
518
- catch {
519
- // A launcher that cannot be resolved is tested as the path it was given.
520
- }
521
- return path.includes(`${sep}node_modules${sep}${PACKAGE_NAME}${sep}`);
522
- }
523
- /** Whether the user turned the automatic update off. */
524
- export function autoUpdateEnabled(settingsPath) {
525
- try {
526
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
527
- return parsed.autoUpdate !== false;
528
- }
529
- catch {
530
- // Missing or unreadable settings means the default, which is on.
531
- return true;
532
- }
533
- }
@@ -1,69 +0,0 @@
1
- import { mkdirSync, readFileSync } from "node:fs";
2
- import { dirname } from "node:path";
3
- import { USAGE_CACHE_PATH } from "./paths.js";
4
- import { usageSegment } from "./hud-line.js";
5
- import { writeFileAtomicSync } from "./fs-atomic.js";
6
- import { checkUsage, HOWTOFIX_PROVIDERS, isHowtofixProvider } from "./howtofix-account.js";
7
- const cachePath = USAGE_CACHE_PATH;
8
- const cacheTtlMs = 180_000;
9
- /**
10
- * The connected account's key, or undefined.
11
- *
12
- * Mirrors how `/usage` and `/sync` resolve it: the first configured gateway
13
- * provider that the runtime knows about, then that provider's key.
14
- */
15
- export async function howtofixKeyFrom(source) {
16
- const configured = new Set((await source.modelRuntime.getAvailable()).map((model) => model.provider).filter((provider) => isHowtofixProvider(provider)));
17
- const target = HOWTOFIX_PROVIDERS.find((provider) => configured.has(provider));
18
- if (!target)
19
- return undefined;
20
- const auth = await source.modelRuntime.getAuth(target);
21
- return auth?.auth?.apiKey;
22
- }
23
- function readCache() {
24
- try {
25
- const cache = JSON.parse(readFileSync(cachePath, "utf8"));
26
- if (!cache.data || typeof cache.cachedAt !== "number")
27
- return undefined;
28
- return { data: cache.data, cachedAt: cache.cachedAt };
29
- }
30
- catch {
31
- return undefined;
32
- }
33
- }
34
- function format(usage) {
35
- if (usage.unlimited)
36
- return usageSegment("unlimited", "", usage.latencyMs);
37
- const money = usage.unit === "USD" || usage.unit === "CNY";
38
- const balance = money ? `$${usage.remaining.toFixed(2)}` : `${usage.remaining.toLocaleString()}`;
39
- return usageSegment(balance, usage.unit, usage.latencyMs);
40
- }
41
- /**
42
- * The HUD segment, or undefined when there is nothing to say.
43
- *
44
- * A stale cache is better than nothing: when the gateway cannot be reached the
45
- * last known figure is shown rather than the segment vanishing, and only a start
46
- * with no cache and no reachable gateway leaves the HUD without one.
47
- */
48
- export async function loadUsageSegment(source) {
49
- const cache = readCache();
50
- if (cache && Date.now() - cache.cachedAt < cacheTtlMs)
51
- return format(cache.data);
52
- const key = await howtofixKeyFrom(source);
53
- if (!key)
54
- return cache ? format(cache.data) : undefined;
55
- const started = Date.now();
56
- const check = await checkUsage(key, { timeoutMs: 3_000 });
57
- if (!check.ok)
58
- return cache ? format(cache.data) : undefined;
59
- const usage = {
60
- remaining: check.remaining ?? 0,
61
- limit: check.limit ?? 0,
62
- unit: check.unit,
63
- unlimited: check.unlimited,
64
- latencyMs: Date.now() - started,
65
- };
66
- mkdirSync(dirname(cachePath), { recursive: true });
67
- writeFileAtomicSync(cachePath, JSON.stringify({ data: usage, cachedAt: Date.now() }));
68
- return format(usage);
69
- }
package/dist/web-tools.js DELETED
@@ -1,46 +0,0 @@
1
- import { Type } from "@netrodev/hxf-ai";
2
- import { defineTool } from "@netrodev/hxf-coding-agent";
3
- import { fetchUrl, searchWeb } from "./web.js";
4
- function formatHits(hits) {
5
- if (hits.length === 0)
6
- return "no results";
7
- return hits.map((hit, i) => `${i + 1}. ${hit.title}\n ${hit.url}${hit.snippet ? `\n ${hit.snippet}` : ""}`).join("\n");
8
- }
9
- export const webSearchTool = defineTool({
10
- name: "web_search",
11
- label: "web_search",
12
- description: "Search the public web. Use for current docs, changelogs, error messages, and anything not in this repo. Returns titles, URLs, and snippets. Follow a URL with web_fetch.",
13
- promptSnippet: "web_search: search the public web",
14
- parameters: Type.Object({
15
- query: Type.String({ description: "Search query" }),
16
- }),
17
- async execute(_toolCallId, params) {
18
- const result = await searchWeb(params.query, { braveKey: process.env.BRAVE_API_KEY });
19
- if (!result.ok) {
20
- return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
21
- }
22
- return {
23
- content: [{ type: "text", text: formatHits(result.hits) }],
24
- details: { count: result.hits.length, provider: result.provider },
25
- };
26
- },
27
- });
28
- export const webFetchTool = defineTool({
29
- name: "web_fetch",
30
- label: "web_fetch",
31
- description: "Fetch a public http(s) URL and return its text. HTML is stripped to visible text. Local, loopback, and private addresses are refused.",
32
- promptSnippet: "web_fetch: read a public URL as text",
33
- parameters: Type.Object({
34
- url: Type.String({ description: "http(s) URL" }),
35
- }),
36
- async execute(_toolCallId, params) {
37
- const result = await fetchUrl(params.url);
38
- if (!result.ok) {
39
- return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
40
- }
41
- return {
42
- content: [{ type: "text", text: result.text }],
43
- details: { url: result.url, contentType: result.contentType, chars: result.text.length },
44
- };
45
- },
46
- });