howtofixcli 0.7.6 → 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 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
@@ -1,5 +1,5 @@
1
1
  export const APP_TITLE = "HowToFix";
2
- export const APP_VERSION = "0.7.6";
2
+ export const APP_VERSION = "0.7.7";
3
3
  export const SYSTEM_APPEND = [
4
4
  // House style: RFC 2119 keywords in caps, one claim per line, the
5
5
  // highest-stakes rules at
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
- for (const row of rows) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howtofixcli",
3
- "version": "0.7.6",
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.19",
48
- "@netrodev/hxf-ai": "^1.0.19",
49
- "@netrodev/hxf-coding-agent": "^1.0.19",
50
- "@netrodev/hxf-tui": "^1.0.19",
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"