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/hud-line.js DELETED
@@ -1,153 +0,0 @@
1
- import { formatAge } from "./workflow-progress.js";
2
- import { levelInk } from "./level-style.js";
3
- import { fg } from "./theme.js";
4
- const RESET = "\x1b[0m";
5
- const DIM = "\x1b[2m";
6
- const BOLD = "\x1b[1m";
7
- const RED = "\x1b[31m";
8
- const GREEN = "\x1b[32m";
9
- const YELLOW = "\x1b[33m";
10
- const MAGENTA = "\x1b[35m";
11
- const CYAN = "\x1b[36m";
12
- function paint(text, color) {
13
- return `${color}${text}${RESET}`;
14
- }
15
- /**
16
- * Paint a thinking level in its own colour.
17
- *
18
- * Goes through `fg()` rather than emitting an escape here, so the level obeys the
19
- * same colour mode as the rest of the interface: on a terminal without 24-bit
20
- * colour it becomes the nearest 256-cube index instead of an escape the terminal
21
- * ignores. Split out from `sessionLine` because the caller may already have styled
22
- * the level (a shimmer band sweeps it at the ceiling) and must pass that through
23
- * untouched — re-colouring a string that already carries escapes nests them.
24
- */
25
- export function paintLevel(text, level) {
26
- return fg(text, levelInk(level));
27
- }
28
- function contextColor(percent) {
29
- if (percent >= 85)
30
- return RED;
31
- if (percent >= 70)
32
- return YELLOW;
33
- return GREEN;
34
- }
35
- /**
36
- * The mark before the thinking level, in place of the word "thinking".
37
- *
38
- * Not 🧠: U+1F9E0 carries the Unicode Emoji property and is present in **no**
39
- * monospace font on this box (only Apple Color Emoji and LastResort), so it
40
- * renders as a colour glyph at best and tofu at worst. `✦` (U+2726) is outside
41
- * the Emoji set, comes from Menlo/Courier and the other monospace faces here, and
42
- * reads as "the model is thinking" without a word.
43
- */
44
- export const THINKING_MARK = "✦";
45
- /**
46
- * The session row: model, thinking level, project, branch.
47
- *
48
- * `thinking` arrives already styled. That is the caller's job because the level
49
- * has two looks — its own colour, and a shimmer band when it is at the model's
50
- * ceiling — and only the caller knows which one applies. What this function owns
51
- * is the mark and where the level sits on the line.
52
- */
53
- export function sessionLine(model, thinking, folder, git) {
54
- const badge = paint(`[${model}]`, CYAN);
55
- const level = `${paint(THINKING_MARK, DIM)} ${thinking}`;
56
- const project = paint(folder, YELLOW);
57
- const branch = git ? ` ${paint("git:(", MAGENTA)}${paint(git.replace(/^git:\(/, "").replace(/\)$/, ""), CYAN)}${paint(")", MAGENTA)}` : "";
58
- return `${badge}${paint(" │ ", DIM)}${level}${paint(" │ ", DIM)}${project}${branch}`;
59
- }
60
- /**
61
- * Prefix a HUD row with the running spinner and how long it has been running.
62
- *
63
- * It sits on the row that already carries the model, so the two facts that change
64
- * during a run — what the model is, and that it is working — are read together,
65
- * and the spinner glyph is painted here, at draw time, from the caller's clock. A
66
- * glyph captured when an event arrived is frozen exactly while the model is
67
- * quiet, which is the one stretch a spinner is for.
68
- *
69
- * `waitedMs` is a duration, not a formatted string: the unit switch (seconds →
70
- * minutes → hours) belongs with the rest of the duration formatting, and this
71
- * function is about where the clock sits on the line.
72
- */
73
- export function withSpinner(line, frame, waitedMs) {
74
- if (!frame)
75
- return line;
76
- const clock = waitedMs === undefined ? frame : `${frame} ${paint(formatAge(waitedMs), DIM)}`;
77
- return `${paint(clock, CYAN)} ${paint("│", DIM)} ${line}`;
78
- }
79
- /**
80
- * The context meter and whatever segments belong beside it.
81
- *
82
- * Segments are variadic and empty ones are dropped, so a caller passes the
83
- * rows it has and never has to know which of them are showing anything right
84
- * now: the balance arrives asynchronously and the spend is zero until the
85
- * first turn, and both would otherwise need a conditional at every call site.
86
- */
87
- export function contextLine(percent, ...segments) {
88
- const safe = Number.isFinite(percent) ? Math.min(100, Math.max(0, percent)) : 0;
89
- const filled = Math.round(safe / 10);
90
- const color = contextColor(safe);
91
- const bar = `${color}${"█".repeat(filled)}${DIM}${"░".repeat(10 - filled)}${RESET}`;
92
- const value = paint(`${Math.round(safe)}%`, color);
93
- const balance = segments.filter(Boolean).join(paint(" │ ", DIM));
94
- return `${paint("Context", DIM)} ${bar} ${value}${balance ? `${paint(" │ ", DIM)}${balance}` : ""}`;
95
- }
96
- /**
97
- * The session's spend so far, as a segment of the context row.
98
- *
99
- * A fresh session draws nothing: "$0.000" is a number nobody asked for, and a
100
- * value that is always present is a value that stops being read. The precision
101
- * is the SDK footer's (`$0.123`) rather than `/stats`'s four digits - this is
102
- * read at a glance while the model works, and a session rarely spends enough
103
- * for the last digit to mean anything.
104
- */
105
- export function costSegment(costUsd) {
106
- if (!Number.isFinite(costUsd) || costUsd <= 0)
107
- return "";
108
- return `${paint("Spent", DIM)} ${paint(`$${costUsd.toFixed(3)}`, GREEN)}`;
109
- }
110
- /**
111
- * The `/usage` panel: the account's quota as a bar, the way the context line
112
- * shows the window.
113
- *
114
- * A number alone ("6.50 USD left") makes the reader do the division before they
115
- * can tell whether it matters. The bar is the answer to that question, and the
116
- * percentage is its exact form; the two belong together for the same reason the
117
- * context line carries both.
118
- *
119
- * The thresholds are `contextColor`'s, so "how full is this" reads the same on
120
- * both rows: green under 70%, yellow to 84%, red at 85% and above. A quota that
121
- * is 85% spent is the one worth acting on, and a colour that meant something
122
- * different here would be worse than no colour.
123
- *
124
- * There is deliberately no reset countdown. Claude Code has one because its
125
- * windows expire on a schedule; howtofix.id reports a balance and no window
126
- * end, so a countdown would have to be invented rather than read.
127
- */
128
- export function quotaLines(provider, limit, used, unit) {
129
- const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 0;
130
- const safeUsed = Number.isFinite(used) ? Math.max(0, used) : 0;
131
- const remaining = Math.max(0, safeLimit - safeUsed);
132
- // A zero limit is the unlimited case at the call site, but a division here
133
- // would still produce NaN and paint "NaN%" on the row.
134
- const percent = safeLimit > 0 ? Math.min(100, (safeUsed / safeLimit) * 100) : 0;
135
- const filled = Math.round(percent / 10);
136
- const color = contextColor(percent);
137
- const bar = `${color}${"█".repeat(filled)}${DIM}${"░".repeat(10 - filled)}${RESET}`;
138
- const money = (value) => `${value.toFixed(2)} ${unit}`;
139
- // The widths are fixed so the numbers line up between the two states of the
140
- // panel; a ragged column is harder to scan than a padded one.
141
- return [
142
- `${paint(provider, BOLD)}${paint(" account", DIM)}`,
143
- ` ${paint("used", DIM)} ${bar} ${paint(`${percent.toFixed(1)}%`, color)}`,
144
- ` ${paint("granted", DIM)} ${money(safeLimit)}`,
145
- ` ${paint("spent", DIM)} ${money(safeUsed)}`,
146
- ` ${paint("remaining", DIM)} ${paint(money(remaining), color)}`,
147
- ];
148
- }
149
- export function usageSegment(balance, unit, latencyMs) {
150
- const latency = latencyMs === null ? "" : paint(` ${latencyMs}ms`, DIM);
151
- const label = unit && unit !== "USD" ? `${unit} ` : "";
152
- return `${paint("Balance", DIM)} ${paint(`${label}${balance}`, GREEN)}${latency}`;
153
- }
package/dist/hwid.js DELETED
@@ -1,130 +0,0 @@
1
- /**
2
- * The machine's own id, where the OS keeps one.
3
- *
4
- * The panel already receives a fingerprint, but until now that fingerprint was
5
- * built from the hostname, the platform, the username and the CPU model — none
6
- * of which is hardware. Two different machines built from the same image
7
- * produce the same value, and one machine changes it by renaming itself. That
8
- * leaves the panel's device list unable to answer the one question it exists
9
- * for: is this the same computer as before, or a different one?
10
- *
11
- * Measured on the live panel: one physical box had already produced two device
12
- * sessions and two access tokens under the same fingerprint, because each
13
- * sign-in mints a fresh `device_id` and nothing tied it back to the machine.
14
- *
15
- * So this module reads the identifier the OS keeps for the machine itself:
16
- *
17
- * linux /etc/machine-id, then /var/lib/dbus/machine-id, then the DMI
18
- * product UUID (a firmware value, not a name)
19
- * win32 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
20
- * darwin IOPlatformUUID from `ioreg`
21
- *
22
- * The value never leaves the process raw. What is hashed into the fingerprint
23
- * is an HMAC over it, so the panel can match a machine across runs without ever
24
- * being handed the machine's serial number.
25
- *
26
- * The honest limits, stated where they belong:
27
- *
28
- * - A container that mounts no machine-id and exposes no DMI falls back to the
29
- * hostname hash, and that is weak. The source is reported rather than
30
- * hidden, so a weak id is visibly weak instead of silently indistinguishable
31
- * from a strong one.
32
- * - Anyone with root can change /etc/machine-id. This raises the cost of
33
- * passing one machine off as many; it is not a wall. A wall needs a secret
34
- * the panel issues once, which is what the device session already is.
35
- */
36
- import { execFileSync } from "node:child_process";
37
- import { createHmac } from "node:crypto";
38
- import { readFileSync } from "node:fs";
39
- import { arch, cpus, hostname, platform, userInfo } from "node:os";
40
- /**
41
- * Domain separation for the HMAC. A digest computed for one purpose must never
42
- * be presentable as another, and this keeps the hwid out of the same space as
43
- * the panel's other HMACs.
44
- */
45
- const HWID_DOMAIN = "howtofix-cli-hwid-v1";
46
- /** Read a file and return its trimmed contents, or undefined. Never throws. */
47
- function readTrimmed(path) {
48
- try {
49
- const value = readFileSync(path, "utf8").trim();
50
- return value.length > 0 ? value : undefined;
51
- }
52
- catch {
53
- return undefined;
54
- }
55
- }
56
- /** Run a command and return its stdout, or undefined. Never throws, never waits long. */
57
- function run(command, args) {
58
- try {
59
- const out = execFileSync(command, args, { encoding: "utf8", timeout: 3_000, stdio: ["ignore", "pipe", "ignore"] });
60
- return out.trim().length > 0 ? out.trim() : undefined;
61
- }
62
- catch {
63
- return undefined;
64
- }
65
- }
66
- /**
67
- * The raw machine identifier, with its source, or undefined when the OS exposes
68
- * none.
69
- *
70
- * Order matters: a machine-id is the OS's own identity for the install, a DMI
71
- * product UUID is the firmware's for the board, and both outrank a name.
72
- */
73
- export function readMachineIdentity() {
74
- const os = platform();
75
- if (os === "linux") {
76
- // The canonical install id. Present on systemd hosts and on most container
77
- // images; on a host without it, dbus keeps its own copy of the same value.
78
- const machineId = readTrimmed("/etc/machine-id");
79
- if (machineId)
80
- return { value: machineId, source: "machine-id", strong: true };
81
- const dbusId = readTrimmed("/var/lib/dbus/machine-id");
82
- if (dbusId)
83
- return { value: dbusId, source: "dbus-machine-id", strong: true };
84
- // The firmware's id for the board. Readable without root on most kernels.
85
- const productUuid = readTrimmed("/sys/class/dmi/id/product_uuid");
86
- if (productUuid)
87
- return { value: productUuid, source: "dmi-product-uuid", strong: true };
88
- return undefined;
89
- }
90
- if (os === "win32") {
91
- // The value Windows itself uses as the machine identity. `reg query` prints
92
- // " MachineGuid REG_SZ <guid>", so the guid is the last field.
93
- const out = run("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
94
- const match = out?.match(/MachineGuid\s+REG_SZ\s+(\S+)/i);
95
- if (match?.[1])
96
- return { value: match[1], source: "windows-machine-guid", strong: true };
97
- return undefined;
98
- }
99
- if (os === "darwin") {
100
- // IOPlatformUUID is the Mac's hardware identity.
101
- const out = run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
102
- const match = out?.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
103
- if (match?.[1])
104
- return { value: match[1], source: "platform-uuid", strong: true };
105
- return undefined;
106
- }
107
- return undefined;
108
- }
109
- /**
110
- * The machine id this build reports, with the strength of its source.
111
- *
112
- * The fallback is the same hostname/platform/CPU tuple the fingerprint used
113
- * before this module existed, so a machine with no OS identity behaves exactly
114
- * as it did — but it is now labelled `strong: false` instead of being
115
- * indistinguishable from a hardware id.
116
- */
117
- export function resolveHwid() {
118
- const identity = readMachineIdentity();
119
- const raw = identity?.value ?? [hostname(), platform(), arch(), userInfo().username].join("\u0000");
120
- const source = identity?.source ?? "hostname";
121
- return {
122
- id: createHmac("sha256", HWID_DOMAIN).update(raw).digest("hex").slice(0, 32),
123
- source,
124
- strong: identity?.strong ?? false,
125
- };
126
- }
127
- /** The parts the fingerprint hashes, with the machine id first so it dominates. */
128
- export function fingerprintParts(hwid) {
129
- return [hwid.id, hwid.source, hostname(), platform(), arch(), userInfo().username, String(cpus().length), cpus()[0]?.model ?? ""];
130
- }
package/dist/interrupt.js DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * Whether a raw terminal sequence is a Ctrl+C, in every form a terminal sends it.
3
- *
4
- * The workbench runs in raw mode and handles Ctrl+C itself, so Node never sees
5
- * the SIGINT it would otherwise raise. That only holds for the form the key
6
- * matcher recognises. Three forms are in use:
7
- *
8
- * - the raw byte 0x03, which is what a terminal sends with no keyboard
9
- * protocol negotiated;
10
- * - Kitty's CSI-u sequence, `\x1b[<codepoint>;<modifier>u`, where 99 is "c"
11
- * and 5 is the ctrl modifier;
12
- * - xterm's modifyOtherKeys, `\x1b[27;<modifier>;<codepoint>~`, same numbers.
13
- *
14
- * A Windows terminal that reports the enhanced form sends a sequence the key
15
- * matcher does not know. The byte never matches, the handler never consumes it,
16
- * and the process is left to Node's default, which is to exit. That is the
17
- * close that looks like a Ctrl+C nobody pressed twice.
18
- *
19
- * The check is deliberately wider than the key matcher: a false positive here
20
- * costs one keystroke, a miss costs the whole session.
21
- */
22
- const CTRL_C_BYTE = String.fromCharCode(3);
23
- /** "c" is codepoint 99, and the ctrl modifier is reported as 5. */
24
- const CTRL_MODIFIER = 5;
25
- const C_CODEPOINT = 99;
26
- export function isInterrupt(data) {
27
- if (data === CTRL_C_BYTE)
28
- return true;
29
- // Kitty keyboard protocol: CSI codepoint ; modifier u.
30
- const kitty = /^\x1b\[(\d+);(\d+)u$/.exec(data);
31
- if (kitty && Number(kitty[1]) === C_CODEPOINT && (Number(kitty[2]) & CTRL_MODIFIER) !== 0)
32
- return true;
33
- // xterm modifyOtherKeys: CSI 27 ; modifier ; codepoint ~.
34
- const modifyOther = /^\x1b\[27;(\d+);(\d+)~$/.exec(data);
35
- if (modifyOther && Number(modifyOther[2]) === C_CODEPOINT && (Number(modifyOther[1]) & CTRL_MODIFIER) !== 0)
36
- return true;
37
- return false;
38
- }
39
- /**
40
- * Keep a SIGINT from killing the process outright.
41
- *
42
- * Raw mode is supposed to stop the terminal sending SIGINT at all, but a
43
- * Windows console and a terminal that resets its mode on resize both still do.
44
- * Node's default for an unhandled SIGINT is to exit immediately, which skips
45
- * the shutdown path: no MCP servers are stopped, no session is disposed, and
46
- * the screen is left in raw mode. Handing it to the same function a keypress
47
- * uses means the close looks the same however it arrived.
48
- *
49
- * Idempotent: a second signal while the first shutdown is running must not
50
- * start another.
51
- */
52
- export function catchInterrupt(onInterrupt) {
53
- let handled = false;
54
- process.on("SIGINT", () => {
55
- if (handled)
56
- return;
57
- handled = true;
58
- onInterrupt();
59
- });
60
- }
@@ -1,50 +0,0 @@
1
- function isAssistantMessageUpdate(event) {
2
- return event.type === "message_update";
3
- }
4
- /**
5
- * A delta event, with the cumulative `partial` removed. `toolcall_start` keeps
6
- * the tool's id and name, which the delta itself does not carry.
7
- */
8
- function toJsonAssistantMessageEvent(event) {
9
- if (event.type === "toolcall_start") {
10
- const index = event.contentIndex;
11
- const part = index === undefined ? undefined : event.partial?.content[index];
12
- const toolCall = part;
13
- if (toolCall?.type !== "toolCall") {
14
- // Dropped rather than thrown: see the module comment.
15
- return undefined;
16
- }
17
- const { partial: _partial, ...delta } = event;
18
- return { ...delta, id: toolCall.id, toolName: toolCall.name };
19
- }
20
- if (!("partial" in event))
21
- return event;
22
- const { partial: _partial, ...delta } = event;
23
- return delta;
24
- }
25
- /**
26
- * One session event in the wire shape. Returns `undefined` for the events that
27
- * cannot be represented (a malformed update), so the caller skips them instead
28
- * of writing a line that is not JSON.
29
- */
30
- export function toJsonEvent(event) {
31
- if (!isAssistantMessageUpdate(event))
32
- return event;
33
- if (event.message.role !== "assistant")
34
- return undefined;
35
- const assistantMessageEvent = toJsonAssistantMessageEvent(event.assistantMessageEvent);
36
- if (assistantMessageEvent === undefined)
37
- return undefined;
38
- return {
39
- type: "message_update",
40
- usage: event.message.usage,
41
- assistantMessageEvent,
42
- };
43
- }
44
- /** The line to write for an event, or `undefined` when it has no wire shape. */
45
- export function jsonLine(event) {
46
- const shaped = toJsonEvent(event);
47
- if (shaped === undefined)
48
- return undefined;
49
- return `${JSON.stringify(shaped)}\n`;
50
- }
@@ -1,112 +0,0 @@
1
- /**
2
- * How a thinking level looks: its colour, and how it answers a change.
3
- *
4
- * The level used to be one thing — the word, painted dim, identical at `off` and
5
- * at `max`. That is a reading problem, not a decoration problem: the level is the
6
- * one setting that changes what every request costs, and the HUD is where it is
7
- * read. Colouring it means a glance tells you where you are.
8
- *
9
- * Three rules hold this together, and each of them is here because the obvious
10
- * alternative is wrong:
11
- *
12
- * 1. **The word stays.** Colour is a second cue, never the only one. The word is
13
- * legible in a monochrome terminal, in `screen`, in a screenshot, and to a
14
- * reader with red-green colour vision deficiency — the colour is not.
15
- *
16
- * 2. **The ramp is in OKHSL, not RGB.** Interpolating hue in RGB is what produces
17
- * the muddy, "off" middle that makes a gradient look cheap. OKHSL keeps
18
- * perceptual lightness even as hue swings from blue to orange, which is why the
19
- * steps below read as one family rather than as seven unrelated colours.
20
- *
21
- * 3. **No new glyphs.** A gauge of dots beside the level was the obvious design
22
- * and it was measured and dropped: the HUD line already reaches 89 columns with
23
- * a real model name, so a four-column gauge pushes it past 80 and wraps the
24
- * footer on a default terminal. The level's own width is already paid for.
25
- *
26
- * The 256-colour indices below were not chosen by eye — they were computed by
27
- * quantising each value through the terminal's own cube, because the first
28
- * hand-picked ramp collapsed to *six* distinct colours with `medium` and `high`
29
- * both landing on 68. A ramp that is beautiful in truecolor and ambiguous in 256
30
- * is a regression for exactly the terminals this had to stay safe on.
31
- */
32
- import { parseColor } from "@netrodev/hxf-tui";
33
- /**
34
- * One colour per level, low to high: a desaturated slate at `off`, then blue →
35
- * indigo → violet → magenta → orange.
36
- *
37
- * `off` is deliberately outside the ramp. It is not "the lowest amount of
38
- * thinking", it is thinking switched off, and giving it the ramp's first hue
39
- * would invite reading it as a low setting.
40
- */
41
- const LEVEL_INK = {
42
- // `off` is lifted to 46% rather than the ramp's darker end: at 36% it measured a
43
- // 2.35:1 contrast ratio against the panel background, which is below the 3:1
44
- // floor for legible text — the one level that is *most* often the current one was
45
- // also the hardest to read.
46
- off: parseColor("okhsl(230 12% 46%)"),
47
- minimal: parseColor("okhsl(240 55% 49%)"),
48
- low: parseColor("okhsl(252 60% 55%)"),
49
- medium: parseColor("okhsl(270 64% 60%)"),
50
- high: parseColor("okhsl(298 70% 64%)"),
51
- xhigh: parseColor("okhsl(332 80% 66%)"),
52
- max: parseColor("okhsl(42 95% 64%)"),
53
- };
54
- /**
55
- * The same hues, lifted and desaturated: what a level flashes as the moment it
56
- * changes.
57
- *
58
- * A change has to be visible without the word being re-read, and the change is
59
- * often a single keypress. Mixing toward this rather than toward white keeps the
60
- * flash inside the ramp's own character — a white flash is how several themes
61
- * read an error, and raising the effort is not an error.
62
- */
63
- const LEVEL_FLASH = {
64
- off: parseColor("okhsl(230 14% 64%)"),
65
- minimal: parseColor("okhsl(240 60% 64%)"),
66
- low: parseColor("okhsl(252 64% 68%)"),
67
- medium: parseColor("okhsl(270 68% 72%)"),
68
- high: parseColor("okhsl(298 74% 76%)"),
69
- xhigh: parseColor("okhsl(332 82% 78%)"),
70
- max: parseColor("okhsl(42 96% 76%)"),
71
- };
72
- /** The colour a level is drawn in. Unknown names fall back to `medium`. */
73
- export function levelInk(level) {
74
- return LEVEL_INK[level] ?? LEVEL_INK.medium;
75
- }
76
- /**
77
- * The ramp as a sequence, low to high, for surfaces that want to show the whole
78
- * ladder at once rather than one rung of it.
79
- *
80
- * `off` is not in it: it is not the bottom of the ladder, it is the ladder put
81
- * away, and a spectrum that started at a desaturated slate would read as a dim
82
- * ramp rather than as "no thinking".
83
- */
84
- export const LEVEL_RAMP = [LEVEL_INK.minimal, LEVEL_INK.low, LEVEL_INK.medium, LEVEL_INK.high, LEVEL_INK.xhigh, LEVEL_INK.max];
85
- /** The colour a level flashes toward while a change is still fresh. */
86
- export function levelFlash(level) {
87
- return LEVEL_FLASH[level] ?? LEVEL_FLASH.medium;
88
- }
89
- /**
90
- * How long a level change stays lit.
91
- *
92
- * The glint crosses a span of `word + 2 * GLIMMER_PAD` characters, one character
93
- * per frame, so the travel is `span * tick`. The workbench repaints at 30 fps, so
94
- * a six-letter level is 14 steps of 33 ms — about 460 ms, with the light on the
95
- * word for most of it. The first version was 320 ms, set for a blurred band,
96
- * which made the sharper one a flicker — long enough to see that something
97
- * happened, too short to see what it was. It still finishes well inside the pause
98
- * between two keystrokes of a run up the ladder.
99
- */
100
- export const LEVEL_FLASH_MS = 460;
101
- /**
102
- * How far through the flash we are, 1 at the instant of the change down to 0.
103
- *
104
- * `ageMs` is passed in rather than read from the clock so the caller owns time:
105
- * this is called from `render()`, which must not depend on when it was called for
106
- * anything but the effect.
107
- */
108
- export function flashLevel(ageMs) {
109
- if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs >= LEVEL_FLASH_MS)
110
- return 0;
111
- return 1 - ageMs / LEVEL_FLASH_MS;
112
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * Stops an agent from repeating the same tool call until the budget is gone.
3
- *
4
- * Why this exists: every harness in this space has the same field report. The
5
- * most-discussed agent-quality complaints on the competitor trackers are loops,
6
- * not crashes — "stuck in loop of verification", "gemini stuck in thinking loop
7
- * for hours", "agent repeatedly runs broad searches causing massive context
8
- * overflow", "auto-compaction infinite loop". A model that re-issues an identical
9
- * call is not making progress, and nothing in the SDK or the session stops it: it
10
- * will happily burn the whole context and the whole budget first.
11
- *
12
- * The shape: guard the same failure with a small retry cap and a "no-progress
13
- * dead end" that gives up instead of retrying forever. Applied to tool calls, which
14
- * is where the user actually pays for it.
15
- *
16
- * The rule: the same tool called with the same arguments `REPEAT_LIMIT` times in
17
- * a row is refused, with a reason that says what to do instead. A call with
18
- * different arguments is progress and resets that tool's counter, and the whole
19
- * guard resets when a new run starts, so repeating something across two prompts
20
- * is fine.
21
- *
22
- * Deliberately exempt: `think`. It is a pure reasoning channel with no side
23
- * effect, and re-using the same thought is sometimes how a model gets unstuck.
24
- *
25
- * Pure state, no session needed — the policy is testable on its own.
26
- */
27
- /** Consecutive identical calls allowed before the guard refuses. */
28
- export const REPEAT_LIMIT = 3;
29
- /** Tools whose repetition is harmless and sometimes deliberate. */
30
- const EXEMPT = new Set(["think"]);
31
- /**
32
- * The last non-exempt call, and how many times in a row it has been issued.
33
- *
34
- * Only *uninterrupted* repetition counts. A single counter (rather than one per
35
- * tool) is deliberate: `read → edit → read` to verify an edit is a normal
36
- * workflow, and a per-tool counter would see two reads and keep counting toward
37
- * a refusal. Any call that differs from the last one is progress and restarts
38
- * the chain.
39
- */
40
- let last;
41
- /**
42
- * A stable key for a call. Argument key order is normalised, because the same
43
- * call can arrive with its keys in a different order and must still count as a
44
- * repeat.
45
- */
46
- function signatureOf(input) {
47
- const seen = new WeakSet();
48
- const normalise = (value) => {
49
- if (value === null || typeof value !== "object")
50
- return value;
51
- if (seen.has(value))
52
- return "[circular]";
53
- seen.add(value);
54
- if (Array.isArray(value))
55
- return value.map(normalise);
56
- const record = value;
57
- const sorted = {};
58
- for (const key of Object.keys(record).sort())
59
- sorted[key] = normalise(record[key]);
60
- return sorted;
61
- };
62
- try {
63
- return JSON.stringify(normalise(input)) ?? "";
64
- }
65
- catch {
66
- return "";
67
- }
68
- }
69
- /**
70
- * Record a call and report whether it has now repeated too often.
71
- *
72
- * Returns `undefined` while the call may proceed. Once the same call has been
73
- * issued `REPEAT_LIMIT` times without anything else in between, returns the
74
- * count so the caller can refuse it and say why.
75
- */
76
- export function noteToolCall(toolName, input) {
77
- // An exempt tool is invisible to the guard: it neither counts as a repeat nor
78
- // breaks a chain, so `think → read → think → read` is still a read loop.
79
- if (EXEMPT.has(toolName))
80
- return undefined;
81
- const signature = `${toolName}\u0000${signatureOf(input)}`;
82
- const count = last && last.signature === signature ? last.count + 1 : 1;
83
- last = { signature, count };
84
- return count >= REPEAT_LIMIT ? { repeats: count } : undefined;
85
- }
86
- /** Forget the chain. Called when a new run starts, so a new prompt starts clean. */
87
- export function resetLoopGuard() {
88
- last = undefined;
89
- }
90
- /** The refusal message. It must tell the model what to do differently, not just "no". */
91
- export function repeatRefusal(toolName, repeats) {
92
- return (`Refused: ${toolName} has been called ${repeats} times in a row with identical arguments, ` +
93
- "so the last attempt cannot have changed anything. Do not repeat it. Either use different " +
94
- "arguments, use a different tool, or stop and report what you found and what is blocking you.");
95
- }
package/dist/lsp-tools.js DELETED
@@ -1,32 +0,0 @@
1
- import { StringEnum } from "@netrodev/hxf-ai";
2
- import { defineTool } from "@netrodev/hxf-coding-agent";
3
- import { Type } from "typebox";
4
- import { askLsp } from "./lsp.js";
5
- export const lspTool = defineTool({
6
- name: "lsp",
7
- label: "lsp",
8
- description: "Language-server query: definition, references, hover, symbols, or diagnostics for a file. Line is 1-based. Does not edit. If the server binary is missing the result says so; do not pretend grep is a substitute without telling the user.",
9
- promptSnippet: "lsp: definition, references, hover, symbols, diagnostics",
10
- parameters: Type.Object({
11
- action: StringEnum(["diagnostics", "definition", "references", "hover", "symbols"], {
12
- description: "Which language-server query to run",
13
- }),
14
- file: Type.String({ description: "File to query, relative to the project root" }),
15
- line: Type.Optional(Type.Number({ description: "1-based line (definition, references, hover)" })),
16
- symbol: Type.Optional(Type.String({ description: "Substring to locate on that line when column is omitted" })),
17
- }),
18
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
19
- const result = await askLsp({
20
- action: params.action,
21
- file: params.file,
22
- line: params.line,
23
- symbol: params.symbol,
24
- // The session project root: the server's rootUri and the child's cwd.
25
- root: ctx.cwd,
26
- });
27
- if (!result.ok) {
28
- return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
29
- }
30
- return { content: [{ type: "text", text: result.text }], details: { action: params.action, count: result.count } };
31
- },
32
- });