howtofixcli 0.7.5 → 0.7.7
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/args.js +2 -0
- package/dist/brand.js +1 -1
- package/dist/commands.js +133 -6
- package/dist/errors.js +63 -0
- package/dist/fatal.js +4 -4
- package/dist/main.js +24 -16
- package/package.json +5 -5
package/dist/args.js
CHANGED
|
@@ -149,6 +149,8 @@ usage:
|
|
|
149
149
|
howtofix -p "explain this repo"
|
|
150
150
|
howtofix preset new|check|pack author a preset
|
|
151
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
|
|
152
154
|
|
|
153
155
|
options:
|
|
154
156
|
-p, --print run one prompt and print the answer
|
package/dist/brand.js
CHANGED
package/dist/commands.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { AGENT_DIR } from "./paths.js";
|
|
5
|
+
import { APP_TITLE } from "./brand.js";
|
|
6
|
+
import { reportError } from "./errors.js";
|
|
5
7
|
import { detectSandbox } from "./sandbox.js";
|
|
6
8
|
const MIN_NODE = [22, 19];
|
|
7
9
|
/** Everything the CLI needs from the machine, checked without a model call. */
|
|
@@ -89,12 +91,30 @@ export async function runDoctor() {
|
|
|
89
91
|
/** Configured providers and their models, straight from models.json. */
|
|
90
92
|
export function runModels(root = AGENT_DIR) {
|
|
91
93
|
const path = join(root, "models.json");
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(path, "utf8");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const code = error.code;
|
|
100
|
+
if (code === "ENOENT") {
|
|
101
|
+
console.log(` no models.json at ${path} — run \`howtofix /connect\` to pull the catalogue`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
reportError(APP_TITLE, "models", error);
|
|
105
|
+
}
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
92
108
|
let parsed;
|
|
93
109
|
try {
|
|
94
|
-
parsed = JSON.parse(
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
95
111
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
112
|
+
catch (error) {
|
|
113
|
+
// A file that exists but does not parse is a different problem from a
|
|
114
|
+
// missing one: /connect will not fix it, and saying "no models.json" sends
|
|
115
|
+
// the user looking for a file that is already there.
|
|
116
|
+
console.error(`${APP_TITLE}: models: ${path} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
117
|
+
console.error(" fix the file, or delete it and run `howtofix /connect` to write a new one");
|
|
98
118
|
return 1;
|
|
99
119
|
}
|
|
100
120
|
const providers = parsed.providers ?? {};
|
|
@@ -148,6 +168,26 @@ export function listSessionFiles(limit = 15, root = AGENT_DIR) {
|
|
|
148
168
|
return rows.sort((a, b) => b.modified - a.modified).slice(0, limit);
|
|
149
169
|
}
|
|
150
170
|
export function runSessions(argv) {
|
|
171
|
+
const searchIndex = argv.indexOf("--search");
|
|
172
|
+
if (searchIndex >= 0) {
|
|
173
|
+
const query = argv[searchIndex + 1]?.trim();
|
|
174
|
+
if (!query) {
|
|
175
|
+
console.error(`${APP_TITLE}: sessions: --search needs a word or phrase`);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
const hits = searchSessions(query);
|
|
179
|
+
if (hits.length === 0) {
|
|
180
|
+
console.log(` no session mentions "${query}"`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
for (const hit of hits) {
|
|
184
|
+
const when = new Date(hit.row.modified).toISOString().replace("T", " ").slice(0, 16);
|
|
185
|
+
console.log(` ${when} ${hit.row.project} (${hit.matches} hit${hit.matches === 1 ? "" : "s"})`);
|
|
186
|
+
console.log(` ${hit.row.file}`);
|
|
187
|
+
console.log(` ${hit.snippet}`);
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
151
191
|
const limitIndex = argv.indexOf("--limit");
|
|
152
192
|
const limit = limitIndex >= 0 ? Number(argv[limitIndex + 1]) || 15 : 15;
|
|
153
193
|
const rows = listSessionFiles(limit);
|
|
@@ -155,14 +195,101 @@ export function runSessions(argv) {
|
|
|
155
195
|
console.log(` no saved sessions under ${join(AGENT_DIR, "sessions")}`);
|
|
156
196
|
return 0;
|
|
157
197
|
}
|
|
158
|
-
|
|
198
|
+
const exportIndex = argv.indexOf("--export");
|
|
199
|
+
if (exportIndex >= 0) {
|
|
200
|
+
const which = Number(argv[exportIndex + 1]);
|
|
201
|
+
if (!Number.isInteger(which) || which < 1 || which > rows.length) {
|
|
202
|
+
console.error(`${APP_TITLE}: sessions: --export needs a number from the list (1-${rows.length})`);
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
const row = rows[which - 1];
|
|
206
|
+
try {
|
|
207
|
+
console.log(exportSessionMarkdown(row.file));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
reportError(APP_TITLE, "sessions", error);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
rows.forEach((row, index) => {
|
|
159
216
|
const when = new Date(row.modified).toISOString().replace("T", " ").slice(0, 16);
|
|
160
217
|
const kb = Math.round(row.bytes / 1024);
|
|
161
|
-
console.log(` ${when} ${String(kb).padStart(6)}k ${row.project}`);
|
|
218
|
+
console.log(` ${String(index + 1).padStart(2)} ${when} ${String(kb).padStart(6)}k ${row.project}`);
|
|
162
219
|
console.log(` ${row.file}`);
|
|
163
|
-
}
|
|
220
|
+
});
|
|
164
221
|
return 0;
|
|
165
222
|
}
|
|
223
|
+
/** Text of one JSONL line, whether its content is a string or content blocks. */
|
|
224
|
+
function lineText(line) {
|
|
225
|
+
try {
|
|
226
|
+
const entry = JSON.parse(line);
|
|
227
|
+
const content = entry.message?.content;
|
|
228
|
+
if (typeof content === "string")
|
|
229
|
+
return content.replace(/\s+/g, " ").trim();
|
|
230
|
+
if (Array.isArray(content)) {
|
|
231
|
+
return content
|
|
232
|
+
.map((block) => (block && typeof block === "object" && "text" in block ? String(block.text) : ""))
|
|
233
|
+
.join(" ")
|
|
234
|
+
.replace(/\s+/g, " ")
|
|
235
|
+
.trim();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// A line that does not parse is still searchable as raw text.
|
|
240
|
+
}
|
|
241
|
+
return line.replace(/\s+/g, " ").trim();
|
|
242
|
+
}
|
|
243
|
+
/** Sessions whose text mentions `query`, with the first matching line as evidence. */
|
|
244
|
+
export function searchSessions(query, limit = 10, root = AGENT_DIR) {
|
|
245
|
+
const needle = query.toLowerCase();
|
|
246
|
+
const hits = [];
|
|
247
|
+
for (const row of listSessionFiles(500, root)) {
|
|
248
|
+
let text;
|
|
249
|
+
try {
|
|
250
|
+
text = readFileSync(row.file, "utf8");
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let matches = 0;
|
|
256
|
+
let snippet = "";
|
|
257
|
+
for (const line of text.split("\n")) {
|
|
258
|
+
if (!line.toLowerCase().includes(needle))
|
|
259
|
+
continue;
|
|
260
|
+
matches++;
|
|
261
|
+
if (!snippet)
|
|
262
|
+
snippet = lineText(line).slice(0, 160);
|
|
263
|
+
}
|
|
264
|
+
if (matches > 0)
|
|
265
|
+
hits.push({ row, matches, snippet });
|
|
266
|
+
if (hits.length >= limit)
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
return hits;
|
|
270
|
+
}
|
|
271
|
+
/** A session as markdown, for pasting into an issue or a chat. */
|
|
272
|
+
export function exportSessionMarkdown(file) {
|
|
273
|
+
const out = [`# Session ${file.split(/[\\/]/).pop() ?? file}`, ""];
|
|
274
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
275
|
+
if (!line.trim())
|
|
276
|
+
continue;
|
|
277
|
+
let entry;
|
|
278
|
+
try {
|
|
279
|
+
entry = JSON.parse(line);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (entry.type !== "message")
|
|
285
|
+
continue;
|
|
286
|
+
const text = lineText(line);
|
|
287
|
+
if (!text)
|
|
288
|
+
continue;
|
|
289
|
+
out.push(`## ${entry.message?.role ?? "unknown"}`, "", text, "");
|
|
290
|
+
}
|
|
291
|
+
return out.join("\n").trimEnd();
|
|
292
|
+
}
|
|
166
293
|
/** True when the file exists and is a directory — used by doctor's callers. */
|
|
167
294
|
export function stateDirExists() {
|
|
168
295
|
try {
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One way to tell a user that something failed.
|
|
3
|
+
*
|
|
4
|
+
* Every command used to phrase its own failure, which meant a missing file, a
|
|
5
|
+
* refused connection and a typo in a flag could all arrive in the same shape:
|
|
6
|
+
* a sentence that says what broke but not what to do. This module answers both
|
|
7
|
+
* questions from the error itself, so the answer does not depend on which code
|
|
8
|
+
* path happened to throw.
|
|
9
|
+
*/
|
|
10
|
+
/** The action a user can take, inferred from the code Node attached. */
|
|
11
|
+
function hintFor(code, message) {
|
|
12
|
+
const evidence = `${code ?? ""} ${message}`;
|
|
13
|
+
if (/EACCES|EPERM/.test(evidence))
|
|
14
|
+
return "the file or directory is not writable; check permissions and try again";
|
|
15
|
+
if (/ENOENT/.test(evidence))
|
|
16
|
+
return "a file or directory this command needs is missing";
|
|
17
|
+
if (/ENOSPC/.test(evidence))
|
|
18
|
+
return "the disk is full";
|
|
19
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|UND_ERR|fetch failed/i.test(evidence)) {
|
|
20
|
+
return "the network request failed; check the connection and the gateway address";
|
|
21
|
+
}
|
|
22
|
+
if (/EADDRINUSE/.test(evidence))
|
|
23
|
+
return "another process is already using that port";
|
|
24
|
+
if (/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(evidence)) {
|
|
25
|
+
return "a required file or package is missing; reinstall HowToFix";
|
|
26
|
+
}
|
|
27
|
+
if (/JSON/i.test(evidence) && /parse|unexpected token|end of input/i.test(evidence)) {
|
|
28
|
+
return "a configuration file is not valid JSON";
|
|
29
|
+
}
|
|
30
|
+
if (/not a git repository/i.test(evidence))
|
|
31
|
+
return "run the command inside a project, or pass --cwd";
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The message, and the action that follows from it. */
|
|
35
|
+
export function describeError(error) {
|
|
36
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
37
|
+
const message = raw.trim() || "unknown error";
|
|
38
|
+
const code = error?.code;
|
|
39
|
+
return { message, hint: hintFor(code, message) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The line(s) a failure prints.
|
|
43
|
+
*
|
|
44
|
+
* `context` names the thing that failed ("doctor", "update"), because "not
|
|
45
|
+
* permitted" alone does not say what was not permitted. The stack is evidence,
|
|
46
|
+
* and evidence only appears when `HOWTOFIX_DEBUG` asks for it.
|
|
47
|
+
*/
|
|
48
|
+
export function formatError(appTitle, context, error, debug = false) {
|
|
49
|
+
const { message, hint } = describeError(error);
|
|
50
|
+
const head = context ? `${appTitle}: ${context}: ${message}` : `${appTitle}: ${message}`;
|
|
51
|
+
const lines = [head];
|
|
52
|
+
if (hint)
|
|
53
|
+
lines.push(` ${hint}`);
|
|
54
|
+
if (debug && error instanceof Error && error.stack)
|
|
55
|
+
lines.push(error.stack);
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/** Print a failure the same way everywhere. */
|
|
59
|
+
export function reportError(appTitle, context, error, options) {
|
|
60
|
+
const debug = options?.debug ?? process.env.HOWTOFIX_DEBUG === "1";
|
|
61
|
+
const write = options?.write ?? ((text) => console.error(text));
|
|
62
|
+
write(formatError(appTitle, context, error, debug));
|
|
63
|
+
}
|
package/dist/fatal.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* frames the user cannot act on. The message is what matters; the stack is
|
|
7
7
|
* evidence, and evidence belongs behind a flag.
|
|
8
8
|
*/
|
|
9
|
+
import { describeError } from "./errors.js";
|
|
9
10
|
/** The single line a failure deserves, with the stack only when asked for. */
|
|
10
11
|
export function describeFatal(error, debug = false) {
|
|
11
|
-
const message =
|
|
12
|
-
const text = message.trim() || "unknown error";
|
|
12
|
+
const { message } = describeError(error);
|
|
13
13
|
if (!debug)
|
|
14
|
-
return
|
|
14
|
+
return message;
|
|
15
15
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
16
|
-
return stack && stack.includes("\n") ? `${
|
|
16
|
+
return stack && stack.includes("\n") ? `${message}\n${stack}` : message;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Catch what escapes `main()`.
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { helpText, parseArgs } from "./args.js";
|
|
3
3
|
import { APP_TITLE, APP_VERSION } from "./brand.js";
|
|
4
4
|
import { installCrashHandlers } from "./fatal.js";
|
|
5
|
+
import { reportError } from "./errors.js";
|
|
5
6
|
import { AGENT_DIR, pinAgentDir } from "./paths.js";
|
|
6
7
|
import { jsonLine } from "./json-events.js";
|
|
7
8
|
import { spendCapMessage, unpricedBudgetMessage } from "./spend-cap.js";
|
|
@@ -95,6 +96,28 @@ async function main() {
|
|
|
95
96
|
}
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
99
|
+
// Questions about the installation, answered without a session, a model, or
|
|
100
|
+
// any of the repairs below. They run first on purpose: `howtofix models` used
|
|
101
|
+
// to rewrite settings.json and print two notices about it before listing a
|
|
102
|
+
// single model, which reads like the command did something it did not.
|
|
103
|
+
const subArgv = process.argv.slice(2);
|
|
104
|
+
if (subArgv[0] === "doctor" || subArgv[0] === "models" || subArgv[0] === "sessions") {
|
|
105
|
+
const context = subArgv[0];
|
|
106
|
+
try {
|
|
107
|
+
const commands = await import("./commands.js");
|
|
108
|
+
if (context === "doctor")
|
|
109
|
+
process.exitCode = await commands.runDoctor();
|
|
110
|
+
else if (context === "models")
|
|
111
|
+
process.exitCode = commands.runModels();
|
|
112
|
+
else
|
|
113
|
+
process.exitCode = commands.runSessions(subArgv.slice(1));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
reportError(APP_TITLE, context, error);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
98
121
|
// Repair a howtofix gateway entry that predates `compat.supportsDeveloperRole`.
|
|
99
122
|
// The gateway refuses the `developer` role that the SDK selects for every
|
|
100
123
|
// reasoning model, so such an entry fails every prompt silently — the spinner
|
|
@@ -157,20 +180,6 @@ async function main() {
|
|
|
157
180
|
// A start-time re-sync that fails costs the previous list, which still
|
|
158
181
|
// works; `/sync` remains the manual way to try again.
|
|
159
182
|
}
|
|
160
|
-
// Questions about the installation, answered without a session or a model.
|
|
161
|
-
// Dispatched before option parsing so their own flags (--limit) are not read
|
|
162
|
-
// as unknown options for the agent.
|
|
163
|
-
const subArgv = process.argv.slice(2);
|
|
164
|
-
if (subArgv[0] === "doctor" || subArgv[0] === "models" || subArgv[0] === "sessions") {
|
|
165
|
-
const commands = await import("./commands.js");
|
|
166
|
-
if (subArgv[0] === "doctor")
|
|
167
|
-
process.exitCode = await commands.runDoctor();
|
|
168
|
-
else if (subArgv[0] === "models")
|
|
169
|
-
process.exitCode = commands.runModels();
|
|
170
|
-
else
|
|
171
|
-
process.exitCode = commands.runSessions(subArgv.slice(1));
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
183
|
let args;
|
|
175
184
|
try {
|
|
176
185
|
args = parseArgs(process.argv.slice(2));
|
|
@@ -523,7 +532,6 @@ async function main() {
|
|
|
523
532
|
}
|
|
524
533
|
installCrashHandlers(APP_TITLE);
|
|
525
534
|
main().catch((error) => {
|
|
526
|
-
|
|
527
|
-
console.error(`${APP_TITLE}: ${message}`);
|
|
535
|
+
reportError(APP_TITLE, undefined, error);
|
|
528
536
|
process.exitCode = 1;
|
|
529
537
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howtofixcli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"node": ">=22.19"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@netrodev/hxf-agent-core": "^1.0.
|
|
48
|
-
"@netrodev/hxf-ai": "^1.0.
|
|
49
|
-
"@netrodev/hxf-coding-agent": "^1.0.
|
|
50
|
-
"@netrodev/hxf-tui": "^1.0.
|
|
47
|
+
"@netrodev/hxf-agent-core": "^1.0.22",
|
|
48
|
+
"@netrodev/hxf-ai": "^1.0.22",
|
|
49
|
+
"@netrodev/hxf-coding-agent": "^1.0.22",
|
|
50
|
+
"@netrodev/hxf-tui": "^1.0.22",
|
|
51
51
|
"typebox": "1.3.27",
|
|
52
52
|
"typescript": "7.0.2",
|
|
53
53
|
"undici": "^8.10.2"
|