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.
- package/dist/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-63ZCQDXH.js +6 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-ACOMNDUL.js +185 -0
- package/dist/chunk-AEVPET7X.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GIUYIKH3.js +109 -0
- package/dist/chunk-GQCAJ56I.js +5 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-IGEY3AWF.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-NBQFPCRT.js +1 -0
- package/dist/chunk-OIPSLIZE.js +5 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PDNBGUYP.js +2 -0
- package/dist/chunk-S656RE4A.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-U6CI6KXN.js +34 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-ZKZ6WCI3.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +15 -62
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -120
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -301
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -288
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -715
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -347
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -446
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4757
- package/dist/update.js +0 -533
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- package/dist/workflow.js +0 -349
package/dist/anim.js
DELETED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Micro-animation primitives.
|
|
3
|
-
*
|
|
4
|
-
* Every function here is pure in `(text, style, nowMs)`. The workbench already
|
|
5
|
-
* repaints on a 90 ms timer while a run is live, so an "animation" is only a
|
|
6
|
-
* different pixel for the same state: no timers, no per-frame state, and no
|
|
7
|
-
* allocation beyond the strings that get painted anyway.
|
|
8
|
-
*
|
|
9
|
-
* The one rule that matters for cost is that the sweep is emitted as a handful
|
|
10
|
-
* of painted *runs*, not one escape sequence per character — the band is
|
|
11
|
-
* quantised into levels so consecutive characters of the same level share a
|
|
12
|
-
* single `styleText` call, so the sweep costs one style computation per row.
|
|
13
|
-
*
|
|
14
|
-
* `HOWTOFIX_NO_MOTION=1` turns every effect into its static form. That is also
|
|
15
|
-
* what a non-TTY gets.
|
|
16
|
-
*/
|
|
17
|
-
import { mixColors, parseColor, styleText } from "@netrodev/hxf-tui";
|
|
18
|
-
import { MODE } from "./theme.js";
|
|
19
|
-
export const MOTION = process.env.HOWTOFIX_NO_MOTION !== "1";
|
|
20
|
-
/** Milliseconds per character the sweep travels, unless a caller overrides it. */
|
|
21
|
-
const DEFAULT_CHAR_MS = 45;
|
|
22
|
-
/** Half-width of the bright band, in characters. */
|
|
23
|
-
const BAND = 5;
|
|
24
|
-
/** Off-screen padding, so the band enters and leaves instead of snapping. */
|
|
25
|
-
const GAP = 8;
|
|
26
|
-
/**
|
|
27
|
-
* When the wait ramp starts and where it lands: a run that has produced nothing
|
|
28
|
-
* for ten seconds is still plausibly thinking, one that has produced nothing for
|
|
29
|
-
* a minute is worth interrupting.
|
|
30
|
-
*/
|
|
31
|
-
const WARN_AFTER_MS = 10_000;
|
|
32
|
-
const STALLED_AFTER_MS = 60_000;
|
|
33
|
-
/** Levels the band is quantised into: runs, not per-character escapes. */
|
|
34
|
-
const LEVELS = 3;
|
|
35
|
-
/**
|
|
36
|
-
* Off-word padding on each side of the glint's travel, in characters.
|
|
37
|
-
*
|
|
38
|
-
* The band enters from off the word and leaves past the far end, so the light is
|
|
39
|
-
* seen crossing rather than appearing on top of the text. The value is a
|
|
40
|
-
* compromise and was measured rather than guessed: a padding of ten — the width
|
|
41
|
-
* a continuous shimmer loop can afford, because the dead travel is just the gap
|
|
42
|
-
* before the next lap — left *nineteen of twenty-seven* frames with nothing lit,
|
|
43
|
-
* so a keypress was followed by most of a second of nothing before the light
|
|
44
|
-
* arrived. Four keeps the entry readable at roughly 200 ms and leaves the sweep
|
|
45
|
-
* itself at 50 ms per character.
|
|
46
|
-
*/
|
|
47
|
-
const GLIMMER_PAD = 4;
|
|
48
|
-
export const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);
|
|
49
|
-
/** Blend between two palette colours; the terminal has no alpha, so this is "fade". */
|
|
50
|
-
export function fade(from, to, amount) {
|
|
51
|
-
const t = clamp01(amount);
|
|
52
|
-
if (t >= 1)
|
|
53
|
-
return to;
|
|
54
|
-
if (t <= 0)
|
|
55
|
-
return from;
|
|
56
|
-
return mixColors(from, to, t);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* A meter that moves to its target instead of jumping to it.
|
|
60
|
-
*
|
|
61
|
-
* The workbench repaints on a timer, so this is stepped on each frame with the
|
|
62
|
-
* real elapsed time rather than a fixed frame length: a dropped frame shortens
|
|
63
|
-
* nothing and the meter arrives at the same time on a slow terminal. The step is
|
|
64
|
-
* clamped, because a resume after a suspend would otherwise integrate seconds of
|
|
65
|
-
* simulated time into one frame.
|
|
66
|
-
*
|
|
67
|
-
* Underneath it is a critically-damped spring: `velocity` accumulates toward the
|
|
68
|
-
* remaining distance and is bled off, which gives the small overshoot at the end
|
|
69
|
-
* that makes a number feel like it moved rather than teleported.
|
|
70
|
-
*/
|
|
71
|
-
export class Spring {
|
|
72
|
-
value;
|
|
73
|
-
velocity = 0;
|
|
74
|
-
last = Date.now();
|
|
75
|
-
target;
|
|
76
|
-
constructor(value, target = value) {
|
|
77
|
-
this.value = value;
|
|
78
|
-
this.target = target;
|
|
79
|
-
}
|
|
80
|
-
/** Point the spring at a new target; it keeps its speed on the way. */
|
|
81
|
-
set(target) {
|
|
82
|
-
this.target = target;
|
|
83
|
-
}
|
|
84
|
-
/** True while it is still moving, so a caller can keep the clock alive. */
|
|
85
|
-
get moving() {
|
|
86
|
-
return Math.abs(this.target - this.value) > 0.15;
|
|
87
|
-
}
|
|
88
|
-
/** Jump to a value, discarding any motion. For a new baseline, not a target. */
|
|
89
|
-
snap(value) {
|
|
90
|
-
this.value = value;
|
|
91
|
-
this.target = value;
|
|
92
|
-
this.velocity = 0;
|
|
93
|
-
this.last = Date.now();
|
|
94
|
-
}
|
|
95
|
-
/** Advance to `now` and return the current value. */
|
|
96
|
-
step(now = Date.now()) {
|
|
97
|
-
// The clamp exists for a gap (suspend, a blocked event loop), and it has to
|
|
98
|
-
// sit above the real frame length: the workbench tick is 90 ms, so clamping
|
|
99
|
-
// at 64 ms would silently throw away a quarter of every frame and make the
|
|
100
|
-
// meter settle later here than on a faster machine.
|
|
101
|
-
const raw = Math.max(0, now - this.last);
|
|
102
|
-
const dt = Math.min(250, raw) / 1000;
|
|
103
|
-
this.last = now;
|
|
104
|
-
if (!MOTION) {
|
|
105
|
-
this.value = this.target;
|
|
106
|
-
this.velocity = 0;
|
|
107
|
-
return this.value;
|
|
108
|
-
}
|
|
109
|
-
// A gap longer than the clamp is time nobody saw: there were no frames
|
|
110
|
-
// between, so nothing moved on screen and there is no momentum to carry.
|
|
111
|
-
// Keeping the old velocity across a suspend would fling the value past the
|
|
112
|
-
// target on the first frame back, which reads as a glitch rather than as
|
|
113
|
-
// motion.
|
|
114
|
-
if (raw > 250)
|
|
115
|
-
this.velocity = 0;
|
|
116
|
-
// Critically-damped spring, integrated in fixed-duration sub-steps: a
|
|
117
|
-
// 90 ms workbench frame and a 16 ms frame integrate the same curve, so a
|
|
118
|
-
// meter move takes the same wall-clock time on any machine. The frame
|
|
119
|
-
// itself is clamped, so a gap (suspend, a blocked event loop) integrates
|
|
120
|
-
// as one frame instead of one enormous step.
|
|
121
|
-
const stiffness = 150;
|
|
122
|
-
const damping = 2 * Math.sqrt(stiffness);
|
|
123
|
-
const substeps = Math.max(1, Math.ceil(dt / 0.008));
|
|
124
|
-
const h = dt / substeps;
|
|
125
|
-
for (let sub = 0; sub < substeps; sub += 1) {
|
|
126
|
-
this.velocity += (this.target - this.value) * stiffness * h - this.velocity * damping * h;
|
|
127
|
-
this.value += this.velocity * h;
|
|
128
|
-
}
|
|
129
|
-
if (!this.moving) {
|
|
130
|
-
this.value = this.target;
|
|
131
|
-
this.velocity = 0;
|
|
132
|
-
}
|
|
133
|
-
return this.value;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* A status colour that says how long the agent has been waiting on the model.
|
|
138
|
-
*
|
|
139
|
-
* A long silence is not automatically a problem — a big prompt with thinking on
|
|
140
|
-
* takes a while — so this is a ramp, not an alarm: it slides toward the warning
|
|
141
|
-
* colour and reaches the error colour only after the wait has run long enough to
|
|
142
|
-
* be worth interrupting. Claude Code does the same with its spinner gradient;
|
|
143
|
-
* the point is that the colour carries information the text does not.
|
|
144
|
-
*/
|
|
145
|
-
export function waitTone(waitedMs, calm, warning, stalled) {
|
|
146
|
-
if (!MOTION)
|
|
147
|
-
return calm;
|
|
148
|
-
if (waitedMs <= WARN_AFTER_MS)
|
|
149
|
-
return calm;
|
|
150
|
-
if (waitedMs >= STALLED_AFTER_MS)
|
|
151
|
-
return stalled;
|
|
152
|
-
return fade(warning, stalled, (waitedMs - WARN_AFTER_MS) / (STALLED_AFTER_MS - WARN_AFTER_MS));
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* A one-shot entrance: 0 → 1 over `durationMs`, eased out, then it stays at 1.
|
|
156
|
-
* Callers paint `fade(dim, final, entrance(age, duration))`. The terminal has no
|
|
157
|
-
* alpha, so this is a colour move, and the text is never re-flowed by it.
|
|
158
|
-
*/
|
|
159
|
-
export function entrance(ageMs, durationMs) {
|
|
160
|
-
// A negative age means the caller does not know when the item appeared (it
|
|
161
|
-
// came from the transcript, not from this session): draw it final.
|
|
162
|
-
if (!MOTION || durationMs <= 0 || ageMs < 0)
|
|
163
|
-
return 1;
|
|
164
|
-
return 1 - (1 - clamp01(ageMs / durationMs)) ** 3;
|
|
165
|
-
}
|
|
166
|
-
/** True while an entrance that began `ageMs` ago still needs repaints. */
|
|
167
|
-
export function entering(ageMs, durationMs) {
|
|
168
|
-
return MOTION && ageMs >= 0 && ageMs < durationMs;
|
|
169
|
-
}
|
|
170
|
-
/** How long a newly appended feed item takes to become fully opaque. */
|
|
171
|
-
export const ENTRANCE_MS = 260;
|
|
172
|
-
/** Delay between consecutive detail lines of one result, so they cascade. */
|
|
173
|
-
export const STAGGER_MS = 45;
|
|
174
|
-
/**
|
|
175
|
-
* How many lines of a block are visible `ageMs` after it appeared.
|
|
176
|
-
*
|
|
177
|
-
* A line that has not had its turn is not drawn at all — not drawn faint — so
|
|
178
|
-
* the block grows into place instead of showing its final height with ghost
|
|
179
|
-
* lines in it. `total` is returned once the cascade is over, and the cascade is
|
|
180
|
-
* capped so a 200-line diff does not take nine seconds to appear.
|
|
181
|
-
*/
|
|
182
|
-
export function revealedLines(ageMs, total, staggerMs = STAGGER_MS, maxDelayMs = 400) {
|
|
183
|
-
if (!MOTION || staggerMs <= 0 || ageMs < 0)
|
|
184
|
-
return total;
|
|
185
|
-
if (ageMs >= maxDelayMs)
|
|
186
|
-
return total;
|
|
187
|
-
return Math.min(total, Math.floor(ageMs / staggerMs) + 1);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* A slow oscillation between two colours, for a surface that is waiting on the
|
|
191
|
-
* user. It is a triangle wave rather than a sine: it never skips a frame at the
|
|
192
|
-
* turn, and the difference is invisible at this speed.
|
|
193
|
-
*/
|
|
194
|
-
export function pulse(from, to, now = Date.now(), periodMs = 1400) {
|
|
195
|
-
if (!MOTION || periodMs <= 0)
|
|
196
|
-
return from;
|
|
197
|
-
const phase = (now % periodMs) / periodMs;
|
|
198
|
-
return fade(from, to, phase < 0.5 ? phase * 2 : (1 - phase) * 2);
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* A band of light crossing plain text once, at `progress` through its travel.
|
|
202
|
-
*
|
|
203
|
-
* This is the level's change effect. It is a *glint*, not a gradient: the band is
|
|
204
|
-
* three characters wide with a bright crest and two half-lit shoulders, and it
|
|
205
|
-
* advances one character per tick. The first version blurred the light across
|
|
206
|
-
* five characters with three blended levels and crossed the word in 320 ms, and
|
|
207
|
-
* that is what made it read as a smear rather than as a highlight — a gradient
|
|
208
|
-
* spread over a six-character word leaves almost no pixel at full strength, so
|
|
209
|
-
* the eye gets a dim wobble instead of a moving point of light. Sharp is both
|
|
210
|
-
* more legible and cheaper: fewer distinct runs to paint.
|
|
211
|
-
*
|
|
212
|
-
* The travel is deliberately unhurried. The band enters from off the word and
|
|
213
|
-
* leaves past the far end, so it is seen crossing rather than appearing on top of
|
|
214
|
-
* the word, and the padding on each side is wide enough for that entry to be
|
|
215
|
-
* read. The caller owns the clock: `progress` runs 0 → 1 over the travel and a
|
|
216
|
-
* render never depends on when it happened to be called.
|
|
217
|
-
*
|
|
218
|
-
* Motion is confined to the three to seven characters of the word itself. A
|
|
219
|
-
* ripple that repainted the viewport would have to be painted onto a feed that is
|
|
220
|
-
* also scrolling, and under `screen` — where every frame is a diffed repaint — a
|
|
221
|
-
* large animated region is the exact case that tears.
|
|
222
|
-
*/
|
|
223
|
-
export function ripple(text, style, progress) {
|
|
224
|
-
const rest = styleText(text, { fg: style.rest }, MODE);
|
|
225
|
-
if (!MOTION || text.length === 0)
|
|
226
|
-
return rest;
|
|
227
|
-
const t = clamp01(progress);
|
|
228
|
-
if (t >= 1)
|
|
229
|
-
return rest;
|
|
230
|
-
const band = style.band ?? 1;
|
|
231
|
-
// The crest starts and ends off the text, so the band is seen crossing rather
|
|
232
|
-
// than appearing on top of the word.
|
|
233
|
-
const span = text.length + GLIMMER_PAD * 2;
|
|
234
|
-
// Quantised to whole characters, and this is the detail that decides whether
|
|
235
|
-
// the effect reads as a glint or as a smear. Stepping the centre continuously
|
|
236
|
-
// means that for most frames it sits *between* two characters, so both are half
|
|
237
|
-
// lit and neither reaches full strength — the band looks dim and soft at every
|
|
238
|
-
// instant, which is exactly the blur this replaced. Advancing one character at a
|
|
239
|
-
// time keeps one character at the crest on every frame the band is visible.
|
|
240
|
-
const step = Math.min(span - 1, Math.floor(t * span));
|
|
241
|
-
const centre = step - GLIMMER_PAD;
|
|
242
|
-
const levelAt = (i) => {
|
|
243
|
-
const distance = Math.abs(i - centre);
|
|
244
|
-
if (distance > band)
|
|
245
|
-
return 0;
|
|
246
|
-
return 1 - distance / (band + 1);
|
|
247
|
-
};
|
|
248
|
-
let out = "";
|
|
249
|
-
let runStart = 0;
|
|
250
|
-
let runLevel = null;
|
|
251
|
-
let offset = 0;
|
|
252
|
-
let index = 0;
|
|
253
|
-
for (const char of text) {
|
|
254
|
-
const level = levelAt(index);
|
|
255
|
-
if (runLevel === null) {
|
|
256
|
-
runLevel = level;
|
|
257
|
-
}
|
|
258
|
-
else if (level !== runLevel) {
|
|
259
|
-
out += styleText(text.slice(runStart, offset), { fg: fade(style.rest, style.lit, runLevel) }, MODE);
|
|
260
|
-
runStart = offset;
|
|
261
|
-
runLevel = level;
|
|
262
|
-
}
|
|
263
|
-
offset += char.length;
|
|
264
|
-
index += 1;
|
|
265
|
-
}
|
|
266
|
-
return out + styleText(text.slice(runStart), { fg: fade(style.rest, style.lit, runLevel ?? 0) }, MODE);
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Paint text with a travelling band of a colour ramp: a spectrum that flows
|
|
270
|
-
* across the word and never sits on a single flat colour.
|
|
271
|
-
*
|
|
272
|
-
* This is what the top of the effort ladder looks like. The ramp is the one the
|
|
273
|
-
* levels themselves are drawn from, so the highest setting carries every colour
|
|
274
|
-
* below it — the word is a picture of the ladder rather than a badge for its top
|
|
275
|
-
* rung. It runs continuously, which is the one animation here that does: a
|
|
276
|
-
* ceiling is a standing fact about the session, and a still word would say
|
|
277
|
-
* nothing about being at the limit.
|
|
278
|
-
*
|
|
279
|
-
* `charMs` is milliseconds per character the spectrum advances, so the flow
|
|
280
|
-
* speed is independent of the word's length and of the frame rate.
|
|
281
|
-
*/
|
|
282
|
-
export function spectrum(text, ramp, now = Date.now(), charMs = 90) {
|
|
283
|
-
const first = ramp[0] ?? parseColor("#e6e6e6");
|
|
284
|
-
if (ramp.length === 0 || text.length === 0)
|
|
285
|
-
return styleText(text, { fg: first }, MODE);
|
|
286
|
-
if (!MOTION || charMs <= 0)
|
|
287
|
-
return styleText(text, { fg: ramp[ramp.length - 1] ?? first }, MODE);
|
|
288
|
-
// One lap per `ramp.length * charMs`, each character offset by one step, so the
|
|
289
|
-
// band is exactly one ramp long and wraps without a seam.
|
|
290
|
-
const lap = ramp.length * charMs;
|
|
291
|
-
const step = ((now % lap) + lap) % lap;
|
|
292
|
-
let out = "";
|
|
293
|
-
let runStart = 0;
|
|
294
|
-
let runColor = null;
|
|
295
|
-
let offset = 0;
|
|
296
|
-
let index = 0;
|
|
297
|
-
const colourAt = (i) => {
|
|
298
|
-
const pos = (((step + i * charMs) / lap) % 1) * ramp.length;
|
|
299
|
-
const from = Math.floor(pos) % ramp.length;
|
|
300
|
-
const to = (from + 1) % ramp.length;
|
|
301
|
-
return fade(ramp[from] ?? first, ramp[to] ?? first, pos - Math.floor(pos));
|
|
302
|
-
};
|
|
303
|
-
for (const char of text) {
|
|
304
|
-
const colour = colourAt(index);
|
|
305
|
-
if (runColor === null) {
|
|
306
|
-
runColor = colour;
|
|
307
|
-
}
|
|
308
|
-
else if (colour !== runColor) {
|
|
309
|
-
out += styleText(text.slice(runStart, offset), { fg: runColor }, MODE);
|
|
310
|
-
runStart = offset;
|
|
311
|
-
runColor = colour;
|
|
312
|
-
}
|
|
313
|
-
offset += char.length;
|
|
314
|
-
index += 1;
|
|
315
|
-
}
|
|
316
|
-
return out + styleText(text.slice(runStart), { fg: runColor ?? first }, MODE);
|
|
317
|
-
}
|
|
318
|
-
/** Distance from the band centre, quantised so the sweep emits a few runs. */
|
|
319
|
-
function bandLevel(index, centre) {
|
|
320
|
-
const distance = Math.abs(index - centre);
|
|
321
|
-
if (distance >= BAND)
|
|
322
|
-
return 0;
|
|
323
|
-
return Math.ceil((1 - distance / BAND) * LEVELS) / LEVELS;
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Sweep a brightness band across plain text.
|
|
327
|
-
*
|
|
328
|
-
* `text` must be plain — no escape sequences — because the band is painted from
|
|
329
|
-
* code points. Visible width is preserved exactly: only colour codes are added.
|
|
330
|
-
* The band starts off-screen and ends off-screen, so a looping sweep never
|
|
331
|
-
* strobes at the boundary.
|
|
332
|
-
*/
|
|
333
|
-
export function shimmer(text, style, now = Date.now()) {
|
|
334
|
-
const charMs = style.charMs ?? DEFAULT_CHAR_MS;
|
|
335
|
-
if (!MOTION || text.length === 0 || charMs <= 0)
|
|
336
|
-
return styleText(text, { fg: style.base }, MODE);
|
|
337
|
-
const span = text.length + GAP * 2;
|
|
338
|
-
const centre = ((now / charMs) % span) - GAP;
|
|
339
|
-
const paint = (run, level) => styleText(run, { fg: fade(style.base, style.bright, level) }, MODE);
|
|
340
|
-
let out = "";
|
|
341
|
-
let runStart = 0;
|
|
342
|
-
let runLevel = null;
|
|
343
|
-
let offset = 0;
|
|
344
|
-
let index = 0;
|
|
345
|
-
// Code-point iteration with code-unit offsets: a surrogate pair crosses the
|
|
346
|
-
// band as one character and is never split between two runs.
|
|
347
|
-
for (const char of text) {
|
|
348
|
-
const level = bandLevel(index, centre);
|
|
349
|
-
if (runLevel === null) {
|
|
350
|
-
runLevel = level;
|
|
351
|
-
}
|
|
352
|
-
else if (level !== runLevel) {
|
|
353
|
-
out += paint(text.slice(runStart, offset), runLevel);
|
|
354
|
-
runStart = offset;
|
|
355
|
-
runLevel = level;
|
|
356
|
-
}
|
|
357
|
-
offset += char.length;
|
|
358
|
-
index += 1;
|
|
359
|
-
}
|
|
360
|
-
return out + paint(text.slice(runStart), runLevel ?? 0);
|
|
361
|
-
}
|
package/dist/args.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { APP_TITLE, APP_VERSION } from "./brand.js";
|
|
2
|
-
const THINKING = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
|
|
3
|
-
const APPROVAL = { suggest: true, "auto-edit": true, "full-auto": true };
|
|
4
|
-
/**
|
|
5
|
-
* `text` is for a person. `json` writes only the final result record, which is
|
|
6
|
-
* what a script that just wants the answer and the cost reaches for.
|
|
7
|
-
* `stream-json` writes every event as it happens, which is what a progress UI
|
|
8
|
-
* or a CI log needs. The split matches the contract scripts already expect.
|
|
9
|
-
*/
|
|
10
|
-
const OUTPUT_FORMATS = { text: true, json: true, "stream-json": true };
|
|
11
|
-
function parseBudget(raw) {
|
|
12
|
-
const dollars = Number(raw);
|
|
13
|
-
if (!Number.isFinite(dollars) || dollars <= 0) {
|
|
14
|
-
throw new Error(`--max-budget-usd needs a positive number of dollars, got '${raw}'`);
|
|
15
|
-
}
|
|
16
|
-
return dollars;
|
|
17
|
-
}
|
|
18
|
-
function takeValue(argv, index, flag) {
|
|
19
|
-
const inline = argv[index];
|
|
20
|
-
if (inline?.includes("="))
|
|
21
|
-
return inline.slice(inline.indexOf("=") + 1);
|
|
22
|
-
const next = argv[index + 1];
|
|
23
|
-
if (!next || next.startsWith("-")) {
|
|
24
|
-
throw new Error(`${flag} needs a value`);
|
|
25
|
-
}
|
|
26
|
-
argv.splice(index + 1, 1);
|
|
27
|
-
return next;
|
|
28
|
-
}
|
|
29
|
-
export function parseArgs(argv) {
|
|
30
|
-
const args = {
|
|
31
|
-
print: false,
|
|
32
|
-
continueSession: false,
|
|
33
|
-
noSession: false,
|
|
34
|
-
help: false,
|
|
35
|
-
version: false,
|
|
36
|
-
update: false,
|
|
37
|
-
checkUpdate: false,
|
|
38
|
-
outputFormat: "text",
|
|
39
|
-
dangerouslySkipPermissions: false,
|
|
40
|
-
sandbox: false,
|
|
41
|
-
prompt: "",
|
|
42
|
-
};
|
|
43
|
-
const rest = [];
|
|
44
|
-
const tokens = [...argv];
|
|
45
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
46
|
-
const token = tokens[i] ?? "";
|
|
47
|
-
if (token === "--") {
|
|
48
|
-
rest.push(...tokens.slice(i + 1));
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
if (token === "-h" || token === "--help")
|
|
52
|
-
args.help = true;
|
|
53
|
-
else if (token === "-v" || token === "--version")
|
|
54
|
-
args.version = true;
|
|
55
|
-
else if (token === "-p" || token === "--print")
|
|
56
|
-
args.print = true;
|
|
57
|
-
else if (token === "-c" || token === "--continue")
|
|
58
|
-
args.continueSession = true;
|
|
59
|
-
else if (token === "--no-session")
|
|
60
|
-
args.noSession = true;
|
|
61
|
-
else if (token === "--sandbox")
|
|
62
|
-
args.sandbox = true;
|
|
63
|
-
else if (token === "--update")
|
|
64
|
-
args.update = true;
|
|
65
|
-
else if (token === "--check-update")
|
|
66
|
-
args.checkUpdate = true;
|
|
67
|
-
else if (token === "-r" || token === "--resume")
|
|
68
|
-
args.continueSession = true;
|
|
69
|
-
else if (token === "--session" || token.startsWith("--session=")) {
|
|
70
|
-
args.sessionPath = takeValue(tokens, i, "--session");
|
|
71
|
-
}
|
|
72
|
-
else if (token === "-m" || token === "--model" || token.startsWith("--model=")) {
|
|
73
|
-
args.model = takeValue(tokens, i, "--model");
|
|
74
|
-
}
|
|
75
|
-
else if (token === "--thinking" || token.startsWith("--thinking=")) {
|
|
76
|
-
args.thinking = takeValue(tokens, i, "--thinking");
|
|
77
|
-
}
|
|
78
|
-
else if (token === "--approval" || token.startsWith("--approval=")) {
|
|
79
|
-
args.approval = takeValue(tokens, i, "--approval");
|
|
80
|
-
}
|
|
81
|
-
else if (token === "--output-format" || token.startsWith("--output-format=")) {
|
|
82
|
-
args.outputFormat = takeValue(tokens, i, "--output-format");
|
|
83
|
-
}
|
|
84
|
-
else if (token === "--max-budget-usd" || token.startsWith("--max-budget-usd=")) {
|
|
85
|
-
args.maxBudgetUsd = parseBudget(takeValue(tokens, i, "--max-budget-usd"));
|
|
86
|
-
}
|
|
87
|
-
else if (token === "--dangerously-skip-permissions") {
|
|
88
|
-
args.dangerouslySkipPermissions = true;
|
|
89
|
-
}
|
|
90
|
-
else if (token.startsWith("-")) {
|
|
91
|
-
throw new Error(`unknown option ${token}`);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
rest.push(token);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (args.thinking && !THINKING[args.thinking]) {
|
|
98
|
-
throw new Error(`thinking must be one of: ${Object.keys(THINKING).join(", ")}`);
|
|
99
|
-
}
|
|
100
|
-
if (args.approval && !APPROVAL[args.approval]) {
|
|
101
|
-
throw new Error(`approval must be one of: ${Object.keys(APPROVAL).join(", ")}`);
|
|
102
|
-
}
|
|
103
|
-
if (!OUTPUT_FORMATS[args.outputFormat]) {
|
|
104
|
-
throw new Error(`output-format must be one of: ${Object.keys(OUTPUT_FORMATS).join(", ")}`);
|
|
105
|
-
}
|
|
106
|
-
if (args.outputFormat !== "text" && !args.print) {
|
|
107
|
-
throw new Error(`--output-format ${args.outputFormat} is a machine contract; it needs --print`);
|
|
108
|
-
}
|
|
109
|
-
// `--max-budget-usd` used to demand `--print` here, on the theory that only
|
|
110
|
-
// a one-shot run has an end to stop at. The workbench has ends too — every
|
|
111
|
-
// turn settles — and it reads the same `getSessionStats().cost` that `/stats`
|
|
112
|
-
// shows, so the cap is enforced there as well (see spend-cap.ts). No guard.
|
|
113
|
-
if (args.dangerouslySkipPermissions && !args.print) {
|
|
114
|
-
throw new Error("--dangerously-skip-permissions is for unattended runs; it needs --print. " +
|
|
115
|
-
"interactively, pick an approval mode instead (/approval, or --approval)");
|
|
116
|
-
}
|
|
117
|
-
// Assigned before the guards: a guard that reads `args.prompt` while it is
|
|
118
|
-
// still empty never fires, which is how `--update "do something"` sailed past.
|
|
119
|
-
args.prompt = rest.join(" ").trim();
|
|
120
|
-
if (args.update && args.checkUpdate) {
|
|
121
|
-
throw new Error("--update installs; --check-update only looks. pick one");
|
|
122
|
-
}
|
|
123
|
-
if ((args.update || args.checkUpdate) && (args.print || args.continueSession || args.sessionPath || args.prompt)) {
|
|
124
|
-
throw new Error("--update and --check-update act on the installation, not a session; they take no prompt");
|
|
125
|
-
}
|
|
126
|
-
if (args.continueSession && args.noSession) {
|
|
127
|
-
throw new Error("--continue and --no-session cannot be combined");
|
|
128
|
-
}
|
|
129
|
-
if (args.sessionPath && args.noSession) {
|
|
130
|
-
throw new Error("--session and --no-session cannot be combined");
|
|
131
|
-
}
|
|
132
|
-
if (args.sessionPath && args.continueSession) {
|
|
133
|
-
throw new Error("--session and --continue cannot be combined");
|
|
134
|
-
}
|
|
135
|
-
if (args.print && (args.continueSession || args.sessionPath)) {
|
|
136
|
-
throw new Error("--print runs one prompt in a throwaway session; it cannot be combined with --continue or --session");
|
|
137
|
-
}
|
|
138
|
-
if (args.sessionPath && !args.sessionPath.endsWith(".jsonl")) {
|
|
139
|
-
throw new Error(`--session expects a session file, got '${args.sessionPath}'`);
|
|
140
|
-
}
|
|
141
|
-
return args;
|
|
142
|
-
}
|
|
143
|
-
export function helpText() {
|
|
144
|
-
return `${APP_TITLE} ${APP_VERSION}
|
|
145
|
-
coding agent. layout is a workbench, not a chat log.
|
|
146
|
-
|
|
147
|
-
usage:
|
|
148
|
-
howtofix [options] [prompt]
|
|
149
|
-
howtofix -p "explain this repo"
|
|
150
|
-
howtofix preset new|check|pack author a preset
|
|
151
|
-
howtofix doctor|models|sessions inspect the installation
|
|
152
|
-
howtofix sessions --search <text> find a session by what was said in it
|
|
153
|
-
howtofix sessions --export <n> print session n from the list as markdown
|
|
154
|
-
|
|
155
|
-
options:
|
|
156
|
-
-p, --print run one prompt and print the answer
|
|
157
|
-
-c, --continue resume the latest session in this folder
|
|
158
|
-
-r, --resume alias for --continue
|
|
159
|
-
--session <file> run inside a specific saved session file
|
|
160
|
-
--no-session do not write a session file
|
|
161
|
-
-m, --model <id> provider/model, or a fuzzy model id
|
|
162
|
-
--thinking <lv> off | minimal | low | medium | high | xhigh | max
|
|
163
|
-
--approval <m> suggest | auto-edit | full-auto
|
|
164
|
-
--output-format <f> text | json | stream-json
|
|
165
|
-
text for a person; json writes the final result only;
|
|
166
|
-
stream-json writes every event as it happens
|
|
167
|
-
--max-budget-usd <n> stop the run once it has spent n dollars
|
|
168
|
-
--sandbox run the whole session inside a filesystem sandbox:
|
|
169
|
-
the project is writable, the rest is read-only.
|
|
170
|
-
needs bubblewrap (Linux) or sandbox-exec (macOS)
|
|
171
|
-
--dangerously-skip-permissions
|
|
172
|
-
run everything, including commands otherwise refused.
|
|
173
|
-
unattended runs only; pairs with --print
|
|
174
|
-
--update install the newest release and exit
|
|
175
|
-
--check-update say whether a newer release exists
|
|
176
|
-
-h, --help
|
|
177
|
-
-v, --version
|
|
178
|
-
|
|
179
|
-
keys:
|
|
180
|
-
enter send
|
|
181
|
-
shift+enter new line
|
|
182
|
-
ctrl+c stop the run, or quit when idle
|
|
183
|
-
ctrl+d quit
|
|
184
|
-
ctrl+l jump to the latest line
|
|
185
|
-
ctrl+o clear the screen (history is kept)
|
|
186
|
-
ctrl+t raise the thinking level (wraps to off)
|
|
187
|
-
ctrl+shift+t lower the thinking level
|
|
188
|
-
esc clear the draft
|
|
189
|
-
ctrl+v paste from the clipboard
|
|
190
|
-
pgup / pgdn scroll the transcript a page
|
|
191
|
-
home / end jump to the top / back to the latest line
|
|
192
|
-
wheel scroll the transcript
|
|
193
|
-
|
|
194
|
-
commands:
|
|
195
|
-
/init /model /providers /thinking /approval /login /connect /logout /new
|
|
196
|
-
/resume /compact /name /stats /rtk /diff /commit /undo /rewind /review /tools /export
|
|
197
|
-
/copy /agents /goal /plan /workflow /session /help /quit /exit
|
|
198
|
-
/dcp /context /decompress /sweep
|
|
199
|
-
/update /usage /sync
|
|
200
|
-
/skills /preset /settings /proxy /roblox
|
|
201
|
-
|
|
202
|
-
/stats shows session tokens, cost, and the prompt-cache hit rate; a high read
|
|
203
|
-
total alone hides re-billed context, so the rate is what tells you the truth.
|
|
204
|
-
/rtk compresses long tool output (logs, diffs, greps) before the model reads it.
|
|
205
|
-
Off by default: /rtk on to save tokens when skimming, /rtk off to keep every line.
|
|
206
|
-
Nothing is invented by it — it drops redundant lines and says how many it dropped,
|
|
207
|
-
and never touches a failed command or one already truncated.
|
|
208
|
-
|
|
209
|
-
/preset lists skills bundled with this build and skills from the registry, by
|
|
210
|
-
category; picking one downloads it into the project and loads it as a skill.
|
|
211
|
-
An integration preset installs disabled: its skills and MCP server stay dormant
|
|
212
|
-
until /preset enable <name>, and /preset disable <name> takes the server back
|
|
213
|
-
out of the live session. /roblox reports that state for RoToolbox in one line.
|
|
214
|
-
/skills lists what is loaded now.
|
|
215
|
-
|
|
216
|
-
/proxy <url> routes requests through a proxy, /proxy off stops, /proxy env uses
|
|
217
|
-
HTTPS_PROXY / HTTP_PROXY / ALL_PROXY; /proxy alone shows what is set.
|
|
218
|
-
|
|
219
|
-
/agents <id> <message> messages a parked agent; /agents <id> shows one.
|
|
220
|
-
|
|
221
|
-
a line starting with ! runs in the project shell.
|
|
222
|
-
|
|
223
|
-
an npm install checks for a newer release at startup and can install it itself;
|
|
224
|
-
/update does it on demand. config and sessions live in ~/.howtofix/agent.
|
|
225
|
-
/connect signs in to howtofix.id in your browser, and falls back to a pasted
|
|
226
|
-
token; /login takes another provider's key.
|
|
227
|
-
`;
|
|
228
|
-
}
|
package/dist/at-file.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, resolve } from "node:path";
|
|
3
|
-
/** Files larger than this are referenced but not inlined. */
|
|
4
|
-
const MAX_INLINE_BYTES = 64 * 1024;
|
|
5
|
-
/**
|
|
6
|
-
* Expand `@path` tokens into `<file>` blocks, the way aider, gemini-cli, and
|
|
7
|
-
* codex do. Unknown or binary-looking paths are left untouched for the model
|
|
8
|
-
* to read with its own tools.
|
|
9
|
-
*/
|
|
10
|
-
export function expandFileReferences(text, cwd) {
|
|
11
|
-
return text.replace(/(^|\s)@([^\s]+)/g, (match, lead, raw) => {
|
|
12
|
-
const token = raw.replace(/[),.;:]+$/, "");
|
|
13
|
-
const trailing = raw.slice(token.length);
|
|
14
|
-
const target = isAbsolute(token) ? token : resolve(cwd, token);
|
|
15
|
-
try {
|
|
16
|
-
const stats = statSync(target);
|
|
17
|
-
if (!stats.isFile() || stats.size === 0 || stats.size > MAX_INLINE_BYTES)
|
|
18
|
-
return match;
|
|
19
|
-
const content = readFileSync(target);
|
|
20
|
-
if (content.includes(0))
|
|
21
|
-
return match;
|
|
22
|
-
return `${lead}<file name="${target}">\n${content.toString("utf8").trimEnd()}\n</file>${trailing}`;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return match;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
}
|