howtofixcli 0.7.6 → 0.7.8
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/cli-checkin.js +9 -5
- package/dist/commands.js +110 -3
- package/dist/ui.js +6 -0
- 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/cli-checkin.js
CHANGED
|
@@ -96,21 +96,25 @@ export async function claimCliCheckin(token, options) {
|
|
|
96
96
|
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
97
97
|
const trimmed = token.trim();
|
|
98
98
|
if (!trimmed)
|
|
99
|
-
return { ok: false, alreadyClaimed: false, quotaAwarded: 0, message: "no token given" };
|
|
99
|
+
return { ok: false, alreadyClaimed: false, blocked: false, quotaAwarded: 0, message: "no token given" };
|
|
100
100
|
try {
|
|
101
101
|
const { status, body } = await callCheckin(trimmed, "POST", baseUrl, fetchImpl, timeoutMs);
|
|
102
102
|
if (body.success === true) {
|
|
103
103
|
const data = (body.data ?? {});
|
|
104
|
-
return { ok: true, alreadyClaimed: false, quotaAwarded: Number(data.quota_awarded ?? 0) || 0, message: "" };
|
|
104
|
+
return { ok: true, alreadyClaimed: false, blocked: false, quotaAwarded: Number(data.quota_awarded ?? 0) || 0, message: "" };
|
|
105
105
|
}
|
|
106
106
|
const message = typeof body.message === "string" ? body.message : `the gateway answered ${status}`;
|
|
107
|
+
// A device that already claimed for another account is a block, not the
|
|
108
|
+
// everyday "today is spent" state: the upstream is closed until staff clear
|
|
109
|
+
// it, so this must not be swallowed as a normal second start.
|
|
110
|
+
const blocked = /device already claimed|account blocked|appeal/i.test(message);
|
|
107
111
|
// The server's own sentence for "today is spent" — recognised so a normal
|
|
108
112
|
// second start never reads as an error.
|
|
109
|
-
const alreadyClaimed = /签到|check-?in|already|今日/.test(message) && !/not enabled/i.test(message);
|
|
110
|
-
return { ok: false, alreadyClaimed, quotaAwarded: 0, message };
|
|
113
|
+
const alreadyClaimed = !blocked && /签到|check-?in|already|今日/.test(message) && !/not enabled/i.test(message);
|
|
114
|
+
return { ok: false, alreadyClaimed, blocked, quotaAwarded: 0, message };
|
|
111
115
|
}
|
|
112
116
|
catch (cause) {
|
|
113
117
|
const aborted = cause instanceof Error && cause.name === "AbortError";
|
|
114
|
-
return { ok: false, alreadyClaimed: false, quotaAwarded: 0, message: aborted ? "the gateway did not answer the check-in in time" : "could not reach the gateway" };
|
|
118
|
+
return { ok: false, alreadyClaimed: false, blocked: false, quotaAwarded: 0, message: aborted ? "the gateway did not answer the check-in in time" : "could not reach the gateway" };
|
|
115
119
|
}
|
|
116
120
|
}
|
package/dist/commands.js
CHANGED
|
@@ -168,6 +168,26 @@ export function listSessionFiles(limit = 15, root = AGENT_DIR) {
|
|
|
168
168
|
return rows.sort((a, b) => b.modified - a.modified).slice(0, limit);
|
|
169
169
|
}
|
|
170
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
|
+
}
|
|
171
191
|
const limitIndex = argv.indexOf("--limit");
|
|
172
192
|
const limit = limitIndex >= 0 ? Number(argv[limitIndex + 1]) || 15 : 15;
|
|
173
193
|
const rows = listSessionFiles(limit);
|
|
@@ -175,14 +195,101 @@ export function runSessions(argv) {
|
|
|
175
195
|
console.log(` no saved sessions under ${join(AGENT_DIR, "sessions")}`);
|
|
176
196
|
return 0;
|
|
177
197
|
}
|
|
178
|
-
|
|
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) => {
|
|
179
216
|
const when = new Date(row.modified).toISOString().replace("T", " ").slice(0, 16);
|
|
180
217
|
const kb = Math.round(row.bytes / 1024);
|
|
181
|
-
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}`);
|
|
182
219
|
console.log(` ${row.file}`);
|
|
183
|
-
}
|
|
220
|
+
});
|
|
184
221
|
return 0;
|
|
185
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
|
+
}
|
|
186
293
|
/** True when the file exists and is a directory — used by doctor's callers. */
|
|
187
294
|
export function stateDirExists() {
|
|
188
295
|
try {
|
package/dist/ui.js
CHANGED
|
@@ -2255,6 +2255,12 @@ export async function runWorkbench(options) {
|
|
|
2255
2255
|
note("check-in", `daily check-in claimed: +${formatQuotaUsd(claim.quotaAwarded)} credited to your howtofix.id account`);
|
|
2256
2256
|
refresh();
|
|
2257
2257
|
}
|
|
2258
|
+
else if (claim.blocked) {
|
|
2259
|
+
// The account lost the upstream for device abuse. Say it once, with
|
|
2260
|
+
// the way to appeal, instead of failing silently until the first
|
|
2261
|
+
// model call returns a 403.
|
|
2262
|
+
note("check-in", claim.message);
|
|
2263
|
+
}
|
|
2258
2264
|
return true;
|
|
2259
2265
|
}
|
|
2260
2266
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "howtofixcli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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"
|