howtofixcli 0.7.14 → 0.7.16

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 (165) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-2XG6XQZR.js +1 -0
  3. package/dist/chunk-3FYCFLNK.js +1 -0
  4. package/dist/chunk-56SEAGAQ.js +1 -0
  5. package/dist/chunk-5HR4WOW7.js +2 -0
  6. package/dist/chunk-6FUWMLWO.js +1 -0
  7. package/dist/chunk-7CHDNTH6.js +1 -0
  8. package/dist/chunk-A5WOL324.js +34 -0
  9. package/dist/chunk-AGGIYXN7.js +2 -0
  10. package/dist/chunk-AUDMRGWF.js +189 -0
  11. package/dist/chunk-BX5KIC4R.js +2 -0
  12. package/dist/chunk-DAPXGSGD.js +12 -0
  13. package/dist/chunk-DOD7MUGX.js +1 -0
  14. package/dist/chunk-DVYH677E.js +5 -0
  15. package/dist/chunk-FFNDHUCQ.js +5 -0
  16. package/dist/chunk-FPXVDP32.js +8 -0
  17. package/dist/chunk-GXB7RZHH.js +1 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-LAHVHRCS.js +2 -0
  21. package/dist/chunk-LJEKOS4S.js +6 -0
  22. package/dist/chunk-LSJC73DG.js +1 -0
  23. package/dist/chunk-MOKUBKE7.js +1 -0
  24. package/dist/chunk-MVTU4M3W.js +2 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-Q7VRW74X.js +1 -0
  30. package/dist/chunk-QJ6MIM5Z.js +1 -0
  31. package/dist/chunk-TSEKPGK4.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V3R4FYTJ.js +1 -0
  34. package/dist/chunk-V656XVXV.js +8 -0
  35. package/dist/chunk-VLMQSD25.js +1 -0
  36. package/dist/chunk-WKUJ7CFO.js +1 -0
  37. package/dist/chunk-WLU3WFCK.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-YW7NORH3.js +2 -0
  42. package/dist/chunk-ZNS6H44M.js +110 -0
  43. package/dist/main.js +99 -551
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/acp.js +0 -234
  48. package/dist/activity.js +0 -124
  49. package/dist/agent-defs.js +0 -237
  50. package/dist/agent-limiter.js +0 -68
  51. package/dist/agents.js +0 -554
  52. package/dist/anim.js +0 -361
  53. package/dist/args.js +0 -250
  54. package/dist/at-file.js +0 -28
  55. package/dist/brand-extension.js +0 -87
  56. package/dist/brand.js +0 -76
  57. package/dist/browser.js +0 -58
  58. package/dist/cache-report.js +0 -139
  59. package/dist/checkpoint.js +0 -437
  60. package/dist/claims.js +0 -167
  61. package/dist/cli-auth.js +0 -142
  62. package/dist/cli-checkin.js +0 -120
  63. package/dist/cli-credentials.js +0 -254
  64. package/dist/clipboard.js +0 -149
  65. package/dist/collapse.js +0 -129
  66. package/dist/commands.js +0 -294
  67. package/dist/compaction-guard.js +0 -81
  68. package/dist/context-extension.js +0 -17
  69. package/dist/dcp/commands.js +0 -172
  70. package/dist/dcp/compress-state.js +0 -139
  71. package/dist/dcp/compress.js +0 -329
  72. package/dist/dcp/dcp-extension.js +0 -295
  73. package/dist/dcp/dedup.js +0 -93
  74. package/dist/dcp/ids.js +0 -189
  75. package/dist/dcp/index.js +0 -25
  76. package/dist/dcp/middle-prune.js +0 -76
  77. package/dist/dcp/prompts.js +0 -68
  78. package/dist/dcp/protected-content.js +0 -221
  79. package/dist/dcp/protected.js +0 -161
  80. package/dist/dcp/prune.js +0 -418
  81. package/dist/dcp/state.js +0 -176
  82. package/dist/device-id.js +0 -90
  83. package/dist/device-id.test.js +0 -23
  84. package/dist/errors.js +0 -63
  85. package/dist/extensions.js +0 -48
  86. package/dist/fatal.js +0 -35
  87. package/dist/feed.js +0 -498
  88. package/dist/fs-atomic.js +0 -54
  89. package/dist/gateway-errors.js +0 -90
  90. package/dist/git.js +0 -230
  91. package/dist/goal-extension.js +0 -88
  92. package/dist/goal.js +0 -222
  93. package/dist/hooks-extension.js +0 -176
  94. package/dist/hooks.js +0 -443
  95. package/dist/howtofix-account.js +0 -735
  96. package/dist/hud-line.js +0 -153
  97. package/dist/hwid.js +0 -130
  98. package/dist/interrupt.js +0 -60
  99. package/dist/json-events.js +0 -50
  100. package/dist/level-style.js +0 -112
  101. package/dist/loop-guard.js +0 -95
  102. package/dist/lsp-tools.js +0 -32
  103. package/dist/lsp.js +0 -429
  104. package/dist/mcp-config.js +0 -85
  105. package/dist/mcp-lifecycle.js +0 -147
  106. package/dist/model-catalog.js +0 -217
  107. package/dist/model-facts.js +0 -35
  108. package/dist/model-resolve.js +0 -65
  109. package/dist/notes.js +0 -78
  110. package/dist/oauth-login.js +0 -568
  111. package/dist/paths.js +0 -61
  112. package/dist/permissions.js +0 -298
  113. package/dist/plan-mode.js +0 -363
  114. package/dist/preset-command.js +0 -227
  115. package/dist/preset-extension.js +0 -35
  116. package/dist/preset-mcp.js +0 -292
  117. package/dist/presets.js +0 -727
  118. package/dist/prompt-files.js +0 -103
  119. package/dist/prompt-text.js +0 -41
  120. package/dist/proxy.js +0 -137
  121. package/dist/quality/ast.js +0 -196
  122. package/dist/quality/cognitive.js +0 -83
  123. package/dist/quality/cyclomatic.js +0 -30
  124. package/dist/quality/halstead.js +0 -83
  125. package/dist/quality/index.js +0 -18
  126. package/dist/quality/length.js +0 -3
  127. package/dist/quality/maintainability.js +0 -15
  128. package/dist/quality/nesting.js +0 -57
  129. package/dist/quality/report.js +0 -803
  130. package/dist/quality/smells.js +0 -67
  131. package/dist/quality-tool.js +0 -63
  132. package/dist/read-only-commands.js +0 -597
  133. package/dist/repo-map.js +0 -292
  134. package/dist/roblox-connect.js +0 -125
  135. package/dist/rpc.js +0 -91
  136. package/dist/rtk.js +0 -193
  137. package/dist/sandbox.js +0 -105
  138. package/dist/schema.js +0 -214
  139. package/dist/serve.js +0 -50
  140. package/dist/session-lifecycle.js +0 -19
  141. package/dist/session-retention.js +0 -111
  142. package/dist/session.js +0 -447
  143. package/dist/sessions.js +0 -100
  144. package/dist/settings.js +0 -148
  145. package/dist/skills.js +0 -35
  146. package/dist/slop-tool.js +0 -96
  147. package/dist/spend-cap.js +0 -43
  148. package/dist/subagent.js +0 -171
  149. package/dist/tar.js +0 -146
  150. package/dist/theme.js +0 -79
  151. package/dist/thinking.js +0 -45
  152. package/dist/todo.js +0 -114
  153. package/dist/tool-loadout.js +0 -97
  154. package/dist/tool-render.js +0 -769
  155. package/dist/tools.js +0 -176
  156. package/dist/ui.js +0 -4814
  157. package/dist/update.js +0 -551
  158. package/dist/usage-segment.js +0 -69
  159. package/dist/web-tools.js +0 -46
  160. package/dist/web.js +0 -265
  161. package/dist/welcome.js +0 -179
  162. package/dist/workflow-engine.js +0 -246
  163. package/dist/workflow-progress.js +0 -446
  164. package/dist/workflow-view.js +0 -195
  165. package/dist/workflow.js +0 -349
package/package.json CHANGED
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "name": "howtofixcli",
3
- "version": "0.7.14",
4
- "description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
3
+ "version": "0.7.16",
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.27",
31
+ "typebox": "^1.4.3",
52
32
  "typescript": "7.0.2",
53
- "undici": "^8.10.2"
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/acp.js DELETED
@@ -1,234 +0,0 @@
1
- /**
2
- * `howtofix acp`: the Agent Client Protocol over stdio.
3
- *
4
- * ACP is how an editor (Zed and others) or any other host drives a coding agent:
5
- * JSON-RPC 2.0, one message per line, the host as client and this process as the
6
- * agent. Speaking it means the CLI plugs into those hosts without each one
7
- * needing its own adapter — and it is also the seam the hosted Roblox workspace
8
- * uses to steer a local session.
9
- *
10
- * What is implemented is the part a host needs to hold a conversation:
11
- * `initialize`, `session/new`, `session/prompt` (with streamed `session/update`
12
- * notifications for text, thinking and tool calls) and `session/cancel`. Anything
13
- * else is answered `-32601` rather than ignored, so a host finds out a capability
14
- * is missing instead of waiting on a reply that never comes.
15
- *
16
- * ## Per-turn context
17
- *
18
- * A host can attach `_meta.howtofix.context` to a prompt: the Studio place, the
19
- * selection, what is connected. It is rendered into a `<session_context>` block
20
- * ahead of the user's words *each turn*, not once at session start, because the
21
- * selection the user had when they typed is the one that matters, and a context
22
- * frozen at `session/new` would describe a place that has since changed.
23
- *
24
- * The agent is reached through `AcpAgent` so this file does not import the SDK:
25
- * the protocol layer is testable with a fake, and `main.ts` supplies the real one.
26
- */
27
- import { createInterface } from "node:readline";
28
- export const ACP_PROTOCOL_VERSION = 1;
29
- /**
30
- * Render per-turn context as text the model reads before the prompt.
31
- *
32
- * Returns an empty string for no context, so a plain prompt is passed through
33
- * untouched. Values are JSON-encoded rather than interpolated: a place named
34
- * `</session_context>` must not be able to close the block and speak as the user.
35
- */
36
- export function renderSessionContext(context) {
37
- if (!context || Object.keys(context).length === 0)
38
- return "";
39
- // JSON quoting alone does not stop `</session_context>` inside a string from
40
- // reading as the closing tag, so `<` is escaped too. `\u003c` is still valid
41
- // JSON and decodes to the same text; it just cannot be mistaken for markup.
42
- const encode = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
43
- const lines = [];
44
- if (context.place)
45
- lines.push(`place: ${encode(context.place)}`);
46
- if (context.selection && context.selection.length > 0)
47
- lines.push(`selection: ${encode(context.selection)}`);
48
- if (context.connected)
49
- lines.push(`connected: ${encode(context.connected)}`);
50
- for (const [key, value] of Object.entries(context)) {
51
- if (key === "place" || key === "selection" || key === "connected")
52
- continue;
53
- lines.push(`${key.replace(/[^\w.-]/g, "_")}: ${encode(value)}`);
54
- }
55
- return `<session_context>\n${lines.join("\n")}\n</session_context>\n\n`;
56
- }
57
- /** A tool's kind for the host's icon, from its name. */
58
- function toolKind(name) {
59
- if (/^(get_|list_|read_|snapshot|diff_|audit|datastore_get|datastore_list|roblox_status)/.test(name))
60
- return "read";
61
- if (/^(search|grep|find)/.test(name))
62
- return "search";
63
- if (/(execute|run_|luau)/.test(name))
64
- return "execute";
65
- if (/^(set_|create_|delete_|clone_|reparent_|edit_|insert_|send_|publish_|upload_|generate_|datastore_set|datastore_delete)/.test(name))
66
- return "edit";
67
- return "other";
68
- }
69
- function resultText(result) {
70
- if (typeof result === "string")
71
- return result;
72
- const content = result?.content;
73
- if (Array.isArray(content))
74
- return content.map((block) => (block.type === "text" ? (block.text ?? "") : `[${block.type ?? "?"}]`)).join("\n");
75
- return JSON.stringify(result ?? null);
76
- }
77
- /** Run the protocol until the input closes. Resolves when it has. */
78
- export function runAcp(options) {
79
- const sessions = new Map();
80
- const running = new Map();
81
- let counter = 0;
82
- const write = (message) => {
83
- options.output.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
84
- };
85
- const reply = (id, result) => write({ id, result });
86
- const fail = (id, code, message) => write({ id, error: { code, message } });
87
- const update = (sessionId, body) => write({ method: "session/update", params: { sessionId, update: body } });
88
- async function handle(request) {
89
- const params = request.params ?? {};
90
- const isNotification = request.id === undefined;
91
- switch (request.method) {
92
- case "initialize":
93
- return reply(request.id, {
94
- protocolVersion: ACP_PROTOCOL_VERSION,
95
- agentCapabilities: { loadSession: false, promptCapabilities: { image: false, audio: false, embeddedContext: true } },
96
- authMethods: [],
97
- agentInfo: { name: options.name ?? "howtofix", title: "HowToFix", version: options.version },
98
- });
99
- case "session/new": {
100
- const cwd = typeof params.cwd === "string" ? params.cwd : process.cwd();
101
- try {
102
- const agent = await options.createAgent(cwd);
103
- counter += 1;
104
- const sessionId = `sess_${Date.now().toString(36)}_${counter}`;
105
- sessions.set(sessionId, agent);
106
- return reply(request.id, { sessionId });
107
- }
108
- catch (error) {
109
- return fail(request.id, -32000, error instanceof Error ? error.message : String(error));
110
- }
111
- }
112
- case "session/prompt": {
113
- const sessionId = String(params.sessionId ?? "");
114
- const agent = sessions.get(sessionId);
115
- if (!agent)
116
- return fail(request.id, -32602, `unknown session ${sessionId}`);
117
- if (running.has(sessionId))
118
- return fail(request.id, -32000, "a prompt is already running in this session");
119
- const blocks = Array.isArray(params.prompt) ? params.prompt : [];
120
- const text = blocks
121
- .map((block) => {
122
- if (block.type === "text")
123
- return block.text ?? "";
124
- if (block.type === "resource" && block.resource?.text !== undefined)
125
- return `<file uri=${JSON.stringify(block.resource.uri ?? "")}>\n${block.resource.text}\n</file>`;
126
- return "";
127
- })
128
- .filter((part) => part !== "")
129
- .join("\n\n");
130
- if (text === "")
131
- return fail(request.id, -32602, "the prompt has no text");
132
- const meta = params._meta;
133
- const full = renderSessionContext(meta?.howtofix?.context) + text;
134
- let cancelled = false;
135
- const unsubscribe = agent.subscribe((event) => {
136
- if (event.type === "message_update") {
137
- const inner = event.assistantMessageEvent;
138
- if (inner?.type === "text_delta" && inner.delta) {
139
- update(sessionId, { sessionUpdate: "agent_message_chunk", content: { type: "text", text: inner.delta } });
140
- }
141
- else if (inner?.type === "thinking_delta" && inner.delta) {
142
- update(sessionId, { sessionUpdate: "agent_thought_chunk", content: { type: "text", text: inner.delta } });
143
- }
144
- }
145
- else if (event.type === "tool_execution_start") {
146
- const e = event;
147
- update(sessionId, {
148
- sessionUpdate: "tool_call",
149
- toolCallId: e.toolCallId,
150
- title: e.toolName,
151
- kind: toolKind(e.toolName),
152
- status: "in_progress",
153
- rawInput: e.args ?? {},
154
- });
155
- }
156
- else if (event.type === "tool_execution_end") {
157
- const e = event;
158
- update(sessionId, {
159
- sessionUpdate: "tool_call_update",
160
- toolCallId: e.toolCallId,
161
- status: e.isError ? "failed" : "completed",
162
- content: [{ type: "content", content: { type: "text", text: resultText(e.result) } }],
163
- });
164
- }
165
- });
166
- const turn = agent
167
- .prompt(full)
168
- .then(() => {
169
- const stop = agent.lastStopReason?.();
170
- const stopReason = cancelled || stop === "aborted" ? "cancelled" : stop === "length" ? "max_tokens" : "end_turn";
171
- reply(request.id, { stopReason });
172
- })
173
- .catch((error) => {
174
- if (cancelled)
175
- reply(request.id, { stopReason: "cancelled" });
176
- else
177
- fail(request.id, -32000, error instanceof Error ? error.message : String(error));
178
- })
179
- .finally(() => {
180
- unsubscribe();
181
- running.delete(sessionId);
182
- });
183
- running.set(sessionId, turn);
184
- // `cancel` needs to reach this turn's flag, so it is looked up by session.
185
- cancels.set(sessionId, () => {
186
- cancelled = true;
187
- });
188
- return;
189
- }
190
- case "session/cancel": {
191
- const sessionId = String(params.sessionId ?? "");
192
- cancels.get(sessionId)?.();
193
- await sessions.get(sessionId)?.abort();
194
- return;
195
- }
196
- default:
197
- // A notification has no reply to carry an error, so it is dropped.
198
- if (!isNotification)
199
- fail(request.id, -32601, `method not found: ${request.method}`);
200
- }
201
- }
202
- const cancels = new Map();
203
- return new Promise((resolve) => {
204
- const lines = createInterface({ input: options.input });
205
- const pending = new Set();
206
- lines.on("line", (line) => {
207
- if (line.trim() === "")
208
- return;
209
- let request;
210
- try {
211
- request = JSON.parse(line);
212
- }
213
- catch {
214
- fail(null, -32700, "parse error");
215
- return;
216
- }
217
- const task = handle(request)
218
- .catch((error) => {
219
- if (request.id !== undefined)
220
- fail(request.id, -32603, error instanceof Error ? error.message : String(error));
221
- })
222
- .finally(() => pending.delete(task));
223
- pending.add(task);
224
- });
225
- lines.on("close", () => {
226
- // Let in-flight turns finish writing before sessions are torn down: closing
227
- // stdin after sending a prompt is how a one-shot host says "and then stop".
228
- void Promise.allSettled([...pending, ...running.values()]).then(async () => {
229
- await Promise.allSettled([...sessions.values()].map((agent) => agent.dispose()));
230
- resolve();
231
- });
232
- });
233
- });
234
- }
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
- }