howtofixcli 0.7.13 → 0.7.15
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-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-7DTQKDO4.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-AUDMRGWF.js +189 -0
- package/dist/chunk-CPJKKHGH.js +1 -0
- package/dist/chunk-DAPXGSGD.js +12 -0
- package/dist/chunk-DOD7MUGX.js +1 -0
- package/dist/chunk-DV2BBBBJ.js +2 -0
- package/dist/chunk-EIBOS5KO.js +5 -0
- package/dist/chunk-EYWOVCSS.js +6 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-HT3MFLAV.js +34 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-JMKK26AT.js +2 -0
- package/dist/chunk-LAHVHRCS.js +2 -0
- package/dist/chunk-LOOFOBTS.js +1 -0
- package/dist/chunk-LSJC73DG.js +1 -0
- package/dist/chunk-MOKUBKE7.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-ONZLNFQ2.js +3 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-SX5VE6XA.js +110 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-UNK5HRQR.js +1 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V2RPZMLK.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-VLMQSD25.js +1 -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-Y7YM5R75.js +5 -0
- package/dist/chunk-YW7NORH3.js +2 -0
- package/dist/main.js +99 -540
- package/package.json +17 -64
- 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 -294
- 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 -295
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/middle-prune.js +0 -76
- 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 -735
- 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 -429
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-facts.js +0 -35
- 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 -447
- 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 -4778
- package/dist/update.js +0 -551
- 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/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howtofixcli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
|
|
3
|
+
"version": "0.7.15",
|
|
5
4
|
"type": "module",
|
|
5
|
+
"description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"homepage": "https://howtofix.id",
|
|
8
|
+
"author": "HowToFix",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"coding-agent",
|
|
11
|
+
"cli",
|
|
12
|
+
"tui",
|
|
13
|
+
"howtofix"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.19"
|
|
17
|
+
},
|
|
6
18
|
"bin": {
|
|
7
19
|
"howtofix": "dist/main.js"
|
|
8
20
|
},
|
|
@@ -11,72 +23,13 @@
|
|
|
11
23
|
"README.md",
|
|
12
24
|
"LICENSE"
|
|
13
25
|
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc -p tsconfig.json && node --input-type=module -e \"import { rmSync, cpSync } from 'node:fs'; rmSync('dist/presets', { recursive: true, force: true }); cpSync('presets', 'dist/presets', { recursive: true });\" && node scripts/set-executable.mjs",
|
|
16
|
-
"start": "node dist/main.js",
|
|
17
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
-
"check": "node scripts/check-all.mjs",
|
|
19
|
-
"check:hub": "node scripts/check-hub-install.mjs",
|
|
20
|
-
"check:tar": "node scripts/check-tar.mjs --online",
|
|
21
|
-
"quality": "node scripts/quality.mjs",
|
|
22
|
-
"frame": "node scripts/render-frame.mjs",
|
|
23
|
-
"frame:workflow": "node scripts/render-workflow-frame.mjs",
|
|
24
|
-
"probe:commands": "python3 scripts/probe-slash-commands.py",
|
|
25
|
-
"build:release": "node scripts/build-release.mjs",
|
|
26
|
-
"version": "node scripts/sync-version.mjs",
|
|
27
|
-
"slop": "node scripts/slop.mjs",
|
|
28
|
-
"slop:commits": "node scripts/slop.mjs --commits",
|
|
29
|
-
"hooks:install": "git config core.hooksPath .githooks && echo 'hooks installed from .githooks'",
|
|
30
|
-
"check:sync": "node scripts/check-hub-sync.mjs --online",
|
|
31
|
-
"check:level-color": "python3 scripts/probe-level-color.py",
|
|
32
|
-
"check:skills-wire": "node scripts/check-skill-wire.mjs",
|
|
33
|
-
"check:agents-live": "node scripts/check-subagent-live.mjs",
|
|
34
|
-
"release:patch": "node scripts/release.mjs patch",
|
|
35
|
-
"release:minor": "node scripts/release.mjs minor",
|
|
36
|
-
"release:major": "node scripts/release.mjs major",
|
|
37
|
-
"release:dry": "node scripts/release.mjs patch --dry",
|
|
38
|
-
"test": "node --test test/*.test.mjs",
|
|
39
|
-
"format": "prettier --write \"src/**/*.ts\" \"scripts/**/*.mjs\" \"test/**/*.mjs\"",
|
|
40
|
-
"format:check": "prettier --check \"src/**/*.ts\" \"scripts/**/*.mjs\" \"test/**/*.mjs\""
|
|
41
|
-
},
|
|
42
|
-
"license": "UNLICENSED",
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=22.19"
|
|
45
|
-
},
|
|
46
26
|
"dependencies": {
|
|
47
27
|
"@netrodev/hxf-agent-core": "^1.0.22",
|
|
48
28
|
"@netrodev/hxf-ai": "^1.0.22",
|
|
49
29
|
"@netrodev/hxf-coding-agent": "^1.0.27",
|
|
50
30
|
"@netrodev/hxf-tui": "^1.0.22",
|
|
51
|
-
"typebox": "1.3
|
|
31
|
+
"typebox": "^1.4.3",
|
|
52
32
|
"typescript": "7.0.2",
|
|
53
|
-
"undici": "^8.
|
|
54
|
-
}
|
|
55
|
-
"devDependencies": {
|
|
56
|
-
"@types/node": "^22.20.5",
|
|
57
|
-
"esbuild": "^0.28.2",
|
|
58
|
-
"prettier": "^3.9.10"
|
|
59
|
-
},
|
|
60
|
-
"homepage": "https://howtofix.id",
|
|
61
|
-
"author": "HowToFix",
|
|
62
|
-
"keywords": [
|
|
63
|
-
"coding-agent",
|
|
64
|
-
"cli",
|
|
65
|
-
"tui",
|
|
66
|
-
"howtofix"
|
|
67
|
-
],
|
|
68
|
-
"contributors": [
|
|
69
|
-
{
|
|
70
|
-
"name": "hshinosa",
|
|
71
|
-
"url": "https://github.com/hshinosa"
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"name": "yhagus",
|
|
75
|
-
"url": "https://github.com/yhagus"
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
"name": "mmatttttt",
|
|
79
|
-
"url": "https://github.com/mmatttttt"
|
|
80
|
-
}
|
|
81
|
-
]
|
|
33
|
+
"undici": "^8.12.3"
|
|
34
|
+
}
|
|
82
35
|
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Signing in with the account itself, not with a key made for the occasion.
|
|
3
|
-
*
|
|
4
|
-
* ## The problem this replaces
|
|
5
|
-
*
|
|
6
|
-
* `/connect` ended in a browser flow whose approval minted a fresh `sk-` key
|
|
7
|
-
* every time. A user who reconnected a few times found a panel full of
|
|
8
|
-
* near-identical rows, could not tell which one the CLI was using, and had to
|
|
9
|
-
* revoke them one by one. The account was already the thing being proven; the
|
|
10
|
-
* key was only a side effect of proving it.
|
|
11
|
-
*
|
|
12
|
-
* ## What happens instead
|
|
13
|
-
*
|
|
14
|
-
* The credential — `email:password`, base64-encoded — is stored once in the agent
|
|
15
|
-
* directory, mode 600, and sent to `POST /api/auth/cli/credential`. The panel
|
|
16
|
-
* verifies it and answers with the CLI key this account already owns; only an
|
|
17
|
-
* account with no CLI key gets a new one. A headless install therefore needs no
|
|
18
|
-
* second device and no pasted key.
|
|
19
|
-
*
|
|
20
|
-
* ## Why base64
|
|
21
|
-
*
|
|
22
|
-
* It is what people paste: one opaque string, no shell quoting, no `@` or `:` to
|
|
23
|
-
* trip over. It is not encryption and is not presented as such — the file is
|
|
24
|
-
* protected by its permissions, and the value is never printed.
|
|
25
|
-
*/
|
|
26
|
-
import { chmodSync, existsSync, readFileSync } from "node:fs";
|
|
27
|
-
import { join } from "node:path";
|
|
28
|
-
import { writeFileAtomicSync } from "./fs-atomic.js";
|
|
29
|
-
/** Where the stored credential lives. */
|
|
30
|
-
export function accountCredentialPath(agentDir) {
|
|
31
|
-
return join(agentDir, "account-credential.json");
|
|
32
|
-
}
|
|
33
|
-
/** base64 of `username:password`. */
|
|
34
|
-
export function encodeCredential(username, password) {
|
|
35
|
-
return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Read either a base64 blob or a plain `user:pass` line.
|
|
39
|
-
*
|
|
40
|
-
* A pasted credential is almost always one of the two, and refusing the plain
|
|
41
|
-
* form only makes the user encode it by hand to satisfy the parser.
|
|
42
|
-
*/
|
|
43
|
-
export function parseCredentialInput(raw) {
|
|
44
|
-
const trimmed = raw.trim();
|
|
45
|
-
if (!trimmed)
|
|
46
|
-
return null;
|
|
47
|
-
const fromPlain = (value) => {
|
|
48
|
-
const at = value.indexOf(":");
|
|
49
|
-
if (at <= 0 || at === value.length - 1)
|
|
50
|
-
return null;
|
|
51
|
-
return { username: value.slice(0, at), password: value.slice(at + 1) };
|
|
52
|
-
};
|
|
53
|
-
const plain = fromPlain(trimmed);
|
|
54
|
-
if (plain)
|
|
55
|
-
return plain;
|
|
56
|
-
// Not `user:pass`, so try base64 and re-read the decoded text.
|
|
57
|
-
try {
|
|
58
|
-
const decoded = Buffer.from(trimmed, "base64").toString("utf8");
|
|
59
|
-
if (!decoded.includes(":"))
|
|
60
|
-
return null;
|
|
61
|
-
return fromPlain(decoded);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Store the credential so a later `/connect` needs nothing typed. */
|
|
68
|
-
export function saveAccountCredential(agentDir, username, password) {
|
|
69
|
-
try {
|
|
70
|
-
const payload = {
|
|
71
|
-
credential: encodeCredential(username, password),
|
|
72
|
-
username,
|
|
73
|
-
savedAt: Math.floor(Date.now() / 1000),
|
|
74
|
-
};
|
|
75
|
-
const target = accountCredentialPath(agentDir);
|
|
76
|
-
writeFileAtomicSync(target, JSON.stringify(payload, null, 2));
|
|
77
|
-
chmodSync(target, 0o600);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/** The stored credential, if one was saved and is still readable. */
|
|
85
|
-
export function loadAccountCredential(agentDir) {
|
|
86
|
-
try {
|
|
87
|
-
const target = accountCredentialPath(agentDir);
|
|
88
|
-
if (!existsSync(target))
|
|
89
|
-
return null;
|
|
90
|
-
const parsed = JSON.parse(readFileSync(target, "utf8"));
|
|
91
|
-
if (!parsed?.credential || !parsed?.username)
|
|
92
|
-
return null;
|
|
93
|
-
return parsed;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/** A JSON POST against the panel. */
|
|
100
|
-
async function panelPost(url, body) {
|
|
101
|
-
const response = await fetch(url, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: { "Content-Type": "application/json" },
|
|
104
|
-
body: JSON.stringify(body),
|
|
105
|
-
signal: AbortSignal.timeout(20_000),
|
|
106
|
-
});
|
|
107
|
-
const text = await response.text();
|
|
108
|
-
let parsed;
|
|
109
|
-
try {
|
|
110
|
-
parsed = JSON.parse(text);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
throw new Error(`the panel answered ${response.status} with something that is not JSON`);
|
|
114
|
-
}
|
|
115
|
-
const envelope = parsed;
|
|
116
|
-
if (!response.ok || envelope.success === false) {
|
|
117
|
-
throw new Error(envelope.message || `the panel answered ${response.status}`);
|
|
118
|
-
}
|
|
119
|
-
return envelope.data;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Exchange the account credential for the CLI key this account owns.
|
|
123
|
-
*
|
|
124
|
-
* The panel decides whether that key already existed; the CLI never creates a
|
|
125
|
-
* token of its own, so a reconnect cannot add a row to the panel.
|
|
126
|
-
*/
|
|
127
|
-
export async function loginWithCredential(panelUrl, username, password, clientInfo) {
|
|
128
|
-
const data = await panelPost(`${panelUrl}/api/auth/cli/credential`, {
|
|
129
|
-
credential: encodeCredential(username, password),
|
|
130
|
-
client_info: clientInfo,
|
|
131
|
-
});
|
|
132
|
-
if (!data?.token) {
|
|
133
|
-
throw new Error("the panel accepted the credential but returned no key");
|
|
134
|
-
}
|
|
135
|
-
return { token: data.token };
|
|
136
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import { encodeCredential, parseCredentialInput } from "./account-credential.js";
|
|
4
|
-
test("reads a plain email:password credential", () => {
|
|
5
|
-
assert.deepEqual(parseCredentialInput("netro@example.com:secret123"), {
|
|
6
|
-
username: "netro@example.com",
|
|
7
|
-
password: "secret123",
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
test("reads the base64 form of the same credential", () => {
|
|
11
|
-
const encoded = encodeCredential("netro@example.com", "secret123");
|
|
12
|
-
assert.deepEqual(parseCredentialInput(encoded), {
|
|
13
|
-
username: "netro@example.com",
|
|
14
|
-
password: "secret123",
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
test("keeps a password that contains a colon", () => {
|
|
18
|
-
assert.deepEqual(parseCredentialInput("user:p:a:ss"), {
|
|
19
|
-
username: "user",
|
|
20
|
-
password: "p:a:ss",
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
test("refuses a relay key", () => {
|
|
24
|
-
assert.equal(parseCredentialInput("sk-abcdef0123456789"), null);
|
|
25
|
-
});
|
|
26
|
-
test("refuses text with no separator", () => {
|
|
27
|
-
assert.equal(parseCredentialInput("just-a-word"), null);
|
|
28
|
-
assert.equal(parseCredentialInput(""), null);
|
|
29
|
-
});
|
|
30
|
-
test("refuses an empty username or password", () => {
|
|
31
|
-
assert.equal(parseCredentialInput(":secret"), null);
|
|
32
|
-
assert.equal(parseCredentialInput("user:"), null);
|
|
33
|
-
});
|
|
34
|
-
test("encodeCredential is what parseCredentialInput decodes", () => {
|
|
35
|
-
const encoded = encodeCredential("a@b.co", "p:w");
|
|
36
|
-
assert.equal(Buffer.from(encoded, "base64").toString("utf8"), "a@b.co:p:w");
|
|
37
|
-
assert.deepEqual(parseCredentialInput(encoded), { username: "a@b.co", password: "p:w" });
|
|
38
|
-
});
|
package/dist/activity.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one line under the input box, made consistent.
|
|
3
|
-
*
|
|
4
|
-
* That line used to be a single `hud.activity` string written from twenty-three
|
|
5
|
-
* places, each with its own idea of what it looked like:
|
|
6
|
-
*
|
|
7
|
-
* `⠋ thinking 12s` — spinner and a word
|
|
8
|
-
* `✓ Read ×1 | Workflow ×1` — a tool tally
|
|
9
|
-
* `* shell 4096 bytes` — a bullet
|
|
10
|
-
* `API key for nrimo. Paste it, then press enter.` — a sentence
|
|
11
|
-
* `""` — blank, from eight different places
|
|
12
|
-
*
|
|
13
|
-
* The result flickered between shapes and erased itself whenever an unrelated
|
|
14
|
-
* event landed, which is what "the text keeps changing and is not consistent"
|
|
15
|
-
* describes. The cause is not the number of writers; it is that each writer
|
|
16
|
-
* owned the *whole* line.
|
|
17
|
-
*
|
|
18
|
-
* So writers now own a **layer**, and the line renders the highest-priority
|
|
19
|
-
* non-empty one:
|
|
20
|
-
*
|
|
21
|
-
* prompt > retry > note > run > tally
|
|
22
|
-
*
|
|
23
|
-
* A prompt waiting for you outranks a transient note, a note outranks the live
|
|
24
|
-
* run, and the tool tally is a floor that survives the run ending. Clearing a
|
|
25
|
-
* layer only clears that layer: a finished run no longer wipes a message that
|
|
26
|
-
* was waiting for the user.
|
|
27
|
-
*
|
|
28
|
-
* The retry sits above the note because the two describe the same silence — a
|
|
29
|
-
* provider that did not answer — and the retry is the one that says what is
|
|
30
|
-
* being done about it. A note that is still pinned while a retry counts down is
|
|
31
|
-
* the older, vaguer half of the same story.
|
|
32
|
-
*
|
|
33
|
-
* Time-varying text is produced here, not stored, so the label stays put while
|
|
34
|
-
* only the elapsed seconds tick.
|
|
35
|
-
*/
|
|
36
|
-
export function emptyActivity() {
|
|
37
|
-
return {};
|
|
38
|
-
}
|
|
39
|
-
/** Set or clear one layer. Returns a new state; the caller assigns it. */
|
|
40
|
-
export function withLayer(state, layer, text, now = Date.now()) {
|
|
41
|
-
const next = { ...state };
|
|
42
|
-
if (layer === "run") {
|
|
43
|
-
if (text === undefined || text === "")
|
|
44
|
-
delete next.run;
|
|
45
|
-
// Keep the original start when only the label changes, so a run that moves
|
|
46
|
-
// from "thinking" to "running agents" does not reset its own clock.
|
|
47
|
-
else
|
|
48
|
-
next.run = { label: text, since: state.run?.since ?? now };
|
|
49
|
-
return next;
|
|
50
|
-
}
|
|
51
|
-
if (text === undefined || text === "")
|
|
52
|
-
delete next[layer];
|
|
53
|
-
else
|
|
54
|
-
next[layer] = text;
|
|
55
|
-
return next;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Set or clear the retry layer.
|
|
59
|
-
*
|
|
60
|
-
* Separate from `withLayer` because the value is a deadline rather than a
|
|
61
|
-
* string: the caller hands over when the next attempt fires and what went
|
|
62
|
-
* wrong, and the text is derived from the clock on every draw.
|
|
63
|
-
*/
|
|
64
|
-
export function withRetry(state, retry) {
|
|
65
|
-
const next = { ...state };
|
|
66
|
-
if (retry === undefined)
|
|
67
|
-
delete next.retry;
|
|
68
|
-
else
|
|
69
|
-
next.retry = retry;
|
|
70
|
-
return next;
|
|
71
|
-
}
|
|
72
|
-
/** `42s` / `1m 05s`, the same shape the rest of the UI uses. */
|
|
73
|
-
function formatElapsed(ms) {
|
|
74
|
-
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
75
|
-
const minutes = Math.floor(seconds / 60);
|
|
76
|
-
return minutes > 0 ? `${minutes}m ${String(seconds % 60).padStart(2, "0")}s` : `${seconds}s`;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The provider's message, cut to what one line can hold.
|
|
80
|
-
*
|
|
81
|
-
* A failed request reports itself in a paragraph, sometimes several — a status
|
|
82
|
-
* line, a JSON body, a stack. The activity line is one row wide, so the first
|
|
83
|
-
* non-empty line is the part that says what happened.
|
|
84
|
-
*/
|
|
85
|
-
export function firstLine(text, limit = 72) {
|
|
86
|
-
const line = text.split("\n").map((part) => part.trim()).find((part) => part !== "") ?? "";
|
|
87
|
-
return line.length > limit ? `${line.slice(0, limit - 1)}…` : line;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* `retry 1/3 in 2s · upstream 502` — the wait counts down as the clock moves.
|
|
91
|
-
*
|
|
92
|
-
* The countdown is the point: a bare `retry 1/3` says something is happening
|
|
93
|
-
* but not whether to wait or to reach for ctrl+c, and a retry that has already
|
|
94
|
-
* fired must not keep reading like a countdown. Past the deadline the text says
|
|
95
|
-
* so and stays there until the attempt reports back.
|
|
96
|
-
*/
|
|
97
|
-
function retryText(retry, now) {
|
|
98
|
-
const left = Math.max(0, Math.ceil((retry.until - now) / 1000));
|
|
99
|
-
const when = left > 0 ? `in ${left}s` : "now";
|
|
100
|
-
const head = `retry ${retry.attempt}/${retry.maxAttempts} ${when}`;
|
|
101
|
-
const reason = firstLine(retry.error);
|
|
102
|
-
return reason === "" ? head : `${head} · ${reason}`;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* The line to draw, or `""` when nothing is happening.
|
|
106
|
-
*
|
|
107
|
-
* One marker per layer, one separator (` · `), and the text never carries its
|
|
108
|
-
* own prefix — so two different states can never look like two different tools.
|
|
109
|
-
*/
|
|
110
|
-
export function activityText(state, spinner, now = Date.now()) {
|
|
111
|
-
if (state.prompt)
|
|
112
|
-
return `? ${state.prompt}`;
|
|
113
|
-
if (state.retry)
|
|
114
|
-
return `* ${retryText(state.retry, now)}`;
|
|
115
|
-
if (state.note)
|
|
116
|
-
return `* ${state.note}`;
|
|
117
|
-
if (state.run)
|
|
118
|
-
return `${spinner} ${state.run.label} · ${formatElapsed(now - state.run.since)}`;
|
|
119
|
-
// No marker of its own: the tally is already a list of per-tool markers
|
|
120
|
-
// (`✓ Read ×1 | ! Workflow ×1`), so prefixing it drew `✓ ✓ Read ×1`.
|
|
121
|
-
if (state.tally)
|
|
122
|
-
return state.tally;
|
|
123
|
-
return "";
|
|
124
|
-
}
|
package/dist/agent-defs.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { getAgentDir, parseFrontmatter } from "@netrodev/hxf-coding-agent";
|
|
4
|
-
import { BUILTIN_AGENT_NAMES } from "./agents.js";
|
|
5
|
-
/**
|
|
6
|
-
* Subagent definitions written by the user, as markdown.
|
|
7
|
-
*
|
|
8
|
-
* One file is one agent. The frontmatter carries what the model needs to choose
|
|
9
|
-
* it and what the run needs to be set up, and the body becomes the child's
|
|
10
|
-
* standing system prompt:
|
|
11
|
-
*
|
|
12
|
-
* ---
|
|
13
|
-
* name: migration
|
|
14
|
-
* description: moves one module at a time and tests after each
|
|
15
|
-
* tools: read, bash, edit, write
|
|
16
|
-
* model: gemini-3-flash
|
|
17
|
-
* effort: medium
|
|
18
|
-
* ---
|
|
19
|
-
* Migrate exactly one module per turn. Run its tests before moving on, and
|
|
20
|
-
* report the file you changed and the test result.
|
|
21
|
-
*
|
|
22
|
-
* The parser is the harness's own, so a file means the same thing here and in
|
|
23
|
-
* the harness. The locations follow this CLI's rule: everything a user writes
|
|
24
|
-
* for a project lives under one directory, `.howtofix`, the same one that holds
|
|
25
|
-
* project skills and presets.
|
|
26
|
-
*
|
|
27
|
-
* <project>/.howtofix/agents/*.md nearest ancestor, found by walking up
|
|
28
|
-
* ~/.howtofix/agent/agents/*.md the CLI's own user scope
|
|
29
|
-
*
|
|
30
|
-
* `parseFrontmatter` is the SDK's YAML parser, which means quoted values,
|
|
31
|
-
* arrays, comments and anything else YAML allows are read the same way the
|
|
32
|
-
* harness reads them. The user-scope directory is the one the run store also
|
|
33
|
-
* uses (`AGENTS_STORE_DIR`); the two do not collide because the store reads only
|
|
34
|
-
* `*.json` and this reads only `*.md`.
|
|
35
|
-
*
|
|
36
|
-
* A definition replaces a built-in of the same name. That is the point of having
|
|
37
|
-
* them: `scout`, `architect`, `task` and `reviewer` are a starting set, not a
|
|
38
|
-
* fixed menu, and an agent whose prompt is wrong for a project should be fixable
|
|
39
|
-
* without patching this package.
|
|
40
|
-
*/
|
|
41
|
-
/** The effort levels a definition may declare. Ours, not the harness's. */
|
|
42
|
-
const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
43
|
-
/** Frontmatter keys this loader reads; anything else is reported and ignored. */
|
|
44
|
-
const KEYS = ["name", "description", "tools", "model", "effort"];
|
|
45
|
-
/**
|
|
46
|
-
* Tools a definition may name.
|
|
47
|
-
*
|
|
48
|
-
* Not a permission list — the permission extension still decides what a child
|
|
49
|
-
* may actually do — but a spelling list, so a typo is reported at load instead of
|
|
50
|
-
* silently resolving to nothing at call time. `mcp__<server>__<tool>` names are
|
|
51
|
-
* accepted as written because they cannot be known in advance.
|
|
52
|
-
*/
|
|
53
|
-
export const KNOWN_AGENT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "bash_output", "kill_shell", "todo", "context", "goal", "task", "workflow", "web_search", "web_fetch", "lsp", "quality", "claim", "repo_map"];
|
|
54
|
-
/**
|
|
55
|
-
* What a definition gets when it names no tools: the same set the built-in `task`
|
|
56
|
-
* agent has.
|
|
57
|
-
*
|
|
58
|
-
* The harness leaves an absent `tools` meaning "no restriction". Here the field
|
|
59
|
-
* is the child's allowlist, so absent has to mean something concrete, and a
|
|
60
|
-
* working set is the useful reading: the permission extension already refuses
|
|
61
|
-
* anything above the parent's approval mode inside a child, and a spawned agent
|
|
62
|
-
* has no interactive UI, so the gate fails closed. Narrowing is what `tools` is
|
|
63
|
-
* for.
|
|
64
|
-
*/
|
|
65
|
-
export const DEFAULT_AGENT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "context", "todo", "web_search", "web_fetch", "lsp", "claim"];
|
|
66
|
-
/** `a, b` and `[a, b]` both mean two tools; anything else means none declared. */
|
|
67
|
-
function asToolList(value) {
|
|
68
|
-
const raw = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
|
|
69
|
-
const tools = raw
|
|
70
|
-
.map((item) => String(item)
|
|
71
|
-
.trim()
|
|
72
|
-
.replace(/^["']|["']$/g, ""))
|
|
73
|
-
.filter(Boolean);
|
|
74
|
-
return tools.length > 0 ? tools : undefined;
|
|
75
|
-
}
|
|
76
|
-
function isDirectory(path) {
|
|
77
|
-
try {
|
|
78
|
-
return statSync(path).isDirectory();
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The nearest project agent directory, walking up from `cwd`.
|
|
86
|
-
*
|
|
87
|
-
* Walking up is the harness's rule and the useful one: a definition written for a
|
|
88
|
-
* monorepo root applies to every package under it.
|
|
89
|
-
*/
|
|
90
|
-
export function findProjectAgentDir(cwd) {
|
|
91
|
-
let current = cwd;
|
|
92
|
-
for (;;) {
|
|
93
|
-
// Beside project skills and presets: one project directory, .howtofix, so a
|
|
94
|
-
// repository does not end up with two dot-directories holding the same kind
|
|
95
|
-
// of file.
|
|
96
|
-
const candidate = join(current, ".howtofix", "agents");
|
|
97
|
-
if (isDirectory(candidate))
|
|
98
|
-
return candidate;
|
|
99
|
-
const parent = dirname(current);
|
|
100
|
-
if (parent === current)
|
|
101
|
-
return undefined;
|
|
102
|
-
current = parent;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* The directories definitions are read from, in load order.
|
|
107
|
-
*
|
|
108
|
-
* User first, project last, and a later definition replaces an earlier one — so a
|
|
109
|
-
* project agent wins a name collision, the precedence the harness's own subagent
|
|
110
|
-
* discovery uses.
|
|
111
|
-
*/
|
|
112
|
-
export function subagentDirs(cwd) {
|
|
113
|
-
// One user location, the one this CLI owns. The harness default, ~/.hxf/agent,
|
|
114
|
-
// is not read: two folders holding the same kind of file is how a definition
|
|
115
|
-
// ends up in the place nothing looks at.
|
|
116
|
-
const dirs = [{ scope: "user", dir: join(getAgentDir(), "agents") }];
|
|
117
|
-
const project = findProjectAgentDir(cwd);
|
|
118
|
-
if (project)
|
|
119
|
-
dirs.push({ scope: "project", dir: project });
|
|
120
|
-
return dirs;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Parse one definition file. Pure apart from the SDK's YAML parser, so the rules
|
|
124
|
-
* can be tested without touching a filesystem.
|
|
125
|
-
*/
|
|
126
|
-
export function parseAgentDef(text, file) {
|
|
127
|
-
const notes = [];
|
|
128
|
-
const problems = [];
|
|
129
|
-
const source = text.replace(/^\uFEFF/, "");
|
|
130
|
-
if (!/^---\r?\n/.test(source)) {
|
|
131
|
-
return { notes, problems: [`${file}: no frontmatter — the file must start with --- and end the block with ---`] };
|
|
132
|
-
}
|
|
133
|
-
let frontmatter = {};
|
|
134
|
-
let body = "";
|
|
135
|
-
try {
|
|
136
|
-
const parsed = parseFrontmatter(source);
|
|
137
|
-
frontmatter = parsed.frontmatter ?? {};
|
|
138
|
-
body = (parsed.body ?? "").trim();
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
return { notes, problems: [`${file}: the frontmatter is not readable YAML — ${error instanceof Error ? error.message : String(error)}`] };
|
|
142
|
-
}
|
|
143
|
-
for (const key of Object.keys(frontmatter)) {
|
|
144
|
-
if (!KEYS.includes(key))
|
|
145
|
-
notes.push(`${file}: ignoring the unknown frontmatter key '${key}'`);
|
|
146
|
-
}
|
|
147
|
-
// A file's own name is the natural default, so `reviewer.md` needs no `name:`.
|
|
148
|
-
const stem = (file.split(/[\\/]/).pop() ?? file).replace(/\.md$/i, "");
|
|
149
|
-
const rawName = frontmatter.name;
|
|
150
|
-
const name = (typeof rawName === "string" && rawName.trim() ? rawName : stem).trim();
|
|
151
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) {
|
|
152
|
-
problems.push(`${file}: '${name}' is not a usable agent name — letters, digits, dot, dash and underscore only`);
|
|
153
|
-
}
|
|
154
|
-
const rawDescription = frontmatter.description;
|
|
155
|
-
const description = typeof rawDescription === "string" ? rawDescription.trim() : "";
|
|
156
|
-
if (!description)
|
|
157
|
-
problems.push(`${file}: a description is required — it is how the model decides to use this agent`);
|
|
158
|
-
if (!body)
|
|
159
|
-
problems.push(`${file}: no prompt body — the text below the frontmatter is the agent's standing instruction`);
|
|
160
|
-
const rawEffort = frontmatter.effort;
|
|
161
|
-
const effort = typeof rawEffort === "string" ? rawEffort.trim().toLowerCase() : "";
|
|
162
|
-
if (effort && !EFFORTS.includes(effort)) {
|
|
163
|
-
problems.push(`${file}: effort '${effort}' is not one of ${EFFORTS.join(", ")}`);
|
|
164
|
-
}
|
|
165
|
-
const declared = asToolList(frontmatter.tools);
|
|
166
|
-
const tools = declared ?? [...DEFAULT_AGENT_TOOLS];
|
|
167
|
-
if (declared) {
|
|
168
|
-
const unknown = declared.filter((tool) => !KNOWN_AGENT_TOOLS.includes(tool) && !tool.startsWith("mcp__"));
|
|
169
|
-
if (unknown.length > 0) {
|
|
170
|
-
notes.push(`${file}: unknown tool${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")} — kept, but nothing registers them yet`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const rawModel = frontmatter.model;
|
|
174
|
-
const model = typeof rawModel === "string" && rawModel.trim() ? rawModel.trim() : undefined;
|
|
175
|
-
if (problems.length > 0)
|
|
176
|
-
return { notes, problems };
|
|
177
|
-
return {
|
|
178
|
-
def: { name, description, tools, prompt: body, effort: (effort || "medium"), model, source: file },
|
|
179
|
-
notes,
|
|
180
|
-
problems,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Load every definition into `into`.
|
|
185
|
-
*
|
|
186
|
-
* Re-running cannot shuffle which definition a session runs with: the same files
|
|
187
|
-
* produce the same map, and a definition only ever replaces one that loaded
|
|
188
|
-
* before it.
|
|
189
|
-
*/
|
|
190
|
-
export function loadAgentDefs(cwd, into) {
|
|
191
|
-
const loaded = [];
|
|
192
|
-
const notes = [];
|
|
193
|
-
const problems = [];
|
|
194
|
-
const scopes = [];
|
|
195
|
-
for (const { scope, dir } of subagentDirs(cwd)) {
|
|
196
|
-
let entries;
|
|
197
|
-
try {
|
|
198
|
-
entries = readdirSync(dir);
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// No directory is the normal case, not a problem to report.
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
for (const entry of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
|
|
205
|
-
const file = join(dir, entry);
|
|
206
|
-
let text;
|
|
207
|
-
try {
|
|
208
|
-
text = readFileSync(file, "utf8");
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const parsed = parseAgentDef(text, file);
|
|
215
|
-
notes.push(...parsed.notes);
|
|
216
|
-
problems.push(...parsed.problems);
|
|
217
|
-
const def = parsed.def;
|
|
218
|
-
if (!def)
|
|
219
|
-
continue;
|
|
220
|
-
if (into.has(def.name)) {
|
|
221
|
-
notes.push(`${file}: replaces ${BUILTIN_AGENT_NAMES.has(def.name) ? "the built-in" : "an earlier"} '${def.name}'`);
|
|
222
|
-
}
|
|
223
|
-
into.set(def.name, def);
|
|
224
|
-
loaded.push(def);
|
|
225
|
-
if (!scopes.includes(scope))
|
|
226
|
-
scopes.push(scope);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (loaded.length > 0) {
|
|
230
|
-
// The effective set, not every file read: a definition that a later one
|
|
231
|
-
// replaced is not what a session will run with, and naming it twice would
|
|
232
|
-
// read as two agents.
|
|
233
|
-
const effective = loaded.filter((def) => into.get(def.name) === def);
|
|
234
|
-
notes.unshift(`loaded ${effective.length} subagent definition${effective.length === 1 ? "" : "s"} (${scopes.join(" and ")}): ${effective.map((def) => def.name).join(", ")}`);
|
|
235
|
-
}
|
|
236
|
-
return { loaded, notes, problems };
|
|
237
|
-
}
|