@teddysc/claude-run 0.10.1 → 0.11.0

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/README.md CHANGED
@@ -32,6 +32,15 @@ The browser will open automatically at http://localhost:12001.
32
32
 
33
33
  ## Changelog
34
34
 
35
+ ### 0.11.0
36
+ - Display session metadata (model, start/end times) in conversation header
37
+ - Show full project path with ~ shorthand in header and sidebar tooltips
38
+ - Add "Hide Unknown model" filter to hide sessions without detected models
39
+ - Add "Delete Unknown" button to remove blank/orphaned sessions from disk
40
+ - Automatic cleanup of orphaned history entries on startup
41
+ - Add comprehensive logging for delete operations
42
+ - Fix URL state sync when unchecking checkboxes
43
+
35
44
  ### 0.10.0
36
45
  - Enhance message handling: add message ID to ConversationMessage
37
46
  - Normalize content blocks in message processing
@@ -81,9 +90,11 @@ The browser will open automatically at http://localhost:12001.
81
90
  - **Filter by project** - Focus on specific projects
82
91
  - **Resume sessions** - Copy the resume command to continue any conversation in your terminal
83
92
  - **Copy messages** - Click the copy button on any message to copy its text content
93
+ - **Session metadata** - See model and start/end times in the conversation header
84
94
  - **Collapsible sidebar** - Maximize your viewing area
85
95
  - **Dark mode** - Easy on the eyes
86
96
  - **Clean UI** - Familiar chat interface with collapsible tool calls
97
+ - **Filter & cleanup** - Hide Unknown-model sessions and delete blank sessions from disk
87
98
 
88
99
  ## Usage
89
100
 
package/dist/index.js CHANGED
@@ -11,7 +11,8 @@ import { streamSSE } from "hono/streaming";
11
11
  import { serve } from "@hono/node-server";
12
12
 
13
13
  // api/storage.ts
14
- import { readdir, readFile, stat, open } from "fs/promises";
14
+ import { readdir, readFile, stat, open, unlink, writeFile } from "fs/promises";
15
+ import { createReadStream } from "fs";
15
16
  import { join, basename } from "path";
16
17
  import { homedir } from "os";
17
18
  import { createInterface } from "readline";
@@ -220,8 +221,85 @@ async function findSessionFile(sessionId) {
220
221
  }
221
222
  return null;
222
223
  }
224
+ async function getSessionModel(sessionId) {
225
+ const filePath = await findSessionFile(sessionId);
226
+ if (!filePath) {
227
+ return null;
228
+ }
229
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
230
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
231
+ try {
232
+ for await (const line of rl) {
233
+ if (!line.trim()) {
234
+ continue;
235
+ }
236
+ try {
237
+ const msg = JSON.parse(line);
238
+ if (msg.type !== "user" && msg.type !== "assistant") {
239
+ continue;
240
+ }
241
+ const model = msg.message?.model;
242
+ if (model) {
243
+ return model;
244
+ }
245
+ } catch {
246
+ continue;
247
+ }
248
+ }
249
+ } finally {
250
+ rl.close();
251
+ stream.close();
252
+ }
253
+ return null;
254
+ }
223
255
  async function loadStorage() {
224
256
  await Promise.all([buildFileIndex(), loadHistoryCache()]);
257
+ await cleanupOrphanedHistoryEntries();
258
+ }
259
+ async function cleanupOrphanedHistoryEntries() {
260
+ const history = historyCache ?? await loadHistoryCache();
261
+ const orphanedIds = [];
262
+ for (const entry of history) {
263
+ if (entry.sessionId && !await findSessionFile(entry.sessionId)) {
264
+ orphanedIds.push(entry.sessionId);
265
+ }
266
+ }
267
+ if (orphanedIds.length === 0) {
268
+ return;
269
+ }
270
+ console.log(
271
+ `[cleanup] Found ${orphanedIds.length} orphaned history entries, cleaning up...`
272
+ );
273
+ const historyPath = join(claudeDir, "history.jsonl");
274
+ const orphanedSet = new Set(orphanedIds);
275
+ try {
276
+ const content = await readFile(historyPath, "utf-8");
277
+ const lines = content.split("\n");
278
+ const filtered = [];
279
+ for (const line of lines) {
280
+ if (!line.trim()) continue;
281
+ try {
282
+ const entry = JSON.parse(line);
283
+ if (entry.sessionId && orphanedSet.has(entry.sessionId)) {
284
+ continue;
285
+ }
286
+ } catch {
287
+ }
288
+ filtered.push(line);
289
+ }
290
+ await writeFile(historyPath, filtered.join("\n") + "\n", "utf-8");
291
+ if (historyCache) {
292
+ historyCache = historyCache.filter(
293
+ (entry) => !(entry.sessionId && orphanedSet.has(entry.sessionId))
294
+ );
295
+ }
296
+ for (const id of orphanedIds) {
297
+ fileIndex.delete(id);
298
+ }
299
+ console.log(`[cleanup] Removed ${orphanedIds.length} orphaned entries`);
300
+ } catch (err) {
301
+ console.error("[cleanup] Failed to clean up orphaned entries:", err);
302
+ }
225
303
  }
226
304
  async function getSessions() {
227
305
  return dedupe("getSessions", async () => {
@@ -259,6 +337,74 @@ async function getProjects() {
259
337
  }
260
338
  return [...projects].sort();
261
339
  }
340
+ async function getSessionsMetadata(ids) {
341
+ const unique = Array.from(new Set(ids.filter(Boolean)));
342
+ return Promise.all(
343
+ unique.map(async (id) => ({
344
+ id,
345
+ model: await getSessionModel(id)
346
+ }))
347
+ );
348
+ }
349
+ async function deleteSession(sessionId) {
350
+ console.log(`[deleteSession] Attempting to delete: ${sessionId}`);
351
+ const filePath = await findSessionFile(sessionId);
352
+ let fileDeleted = false;
353
+ if (filePath) {
354
+ console.log(`[deleteSession] Found file: ${filePath}`);
355
+ try {
356
+ await unlink(filePath);
357
+ console.log(`[deleteSession] Successfully deleted: ${filePath}`);
358
+ fileDeleted = true;
359
+ } catch (err) {
360
+ if (err.code === "ENOENT") {
361
+ console.log(`[deleteSession] File already deleted: ${filePath}`);
362
+ fileDeleted = true;
363
+ } else {
364
+ console.error(`[deleteSession] Failed to delete ${filePath}:`, err);
365
+ return false;
366
+ }
367
+ }
368
+ } else {
369
+ console.log(`[deleteSession] No file found for session: ${sessionId}`);
370
+ }
371
+ fileIndex.delete(sessionId);
372
+ const historyPath = join(claudeDir, "history.jsonl");
373
+ let historyChanged = false;
374
+ try {
375
+ const content = await readFile(historyPath, "utf-8");
376
+ const lines = content.split("\n");
377
+ const filtered = [];
378
+ let changed = false;
379
+ for (const line of lines) {
380
+ if (!line.trim()) {
381
+ continue;
382
+ }
383
+ try {
384
+ const entry = JSON.parse(line);
385
+ if (entry.sessionId && entry.sessionId === sessionId) {
386
+ changed = true;
387
+ continue;
388
+ }
389
+ } catch {
390
+ }
391
+ filtered.push(line);
392
+ }
393
+ if (changed) {
394
+ await writeFile(historyPath, filtered.join("\n") + "\n", "utf-8");
395
+ console.log(`[deleteSession] Removed from history.jsonl: ${sessionId}`);
396
+ if (historyCache) {
397
+ historyCache = historyCache.filter(
398
+ (entry) => entry.sessionId !== sessionId
399
+ );
400
+ }
401
+ historyChanged = true;
402
+ }
403
+ } catch (err) {
404
+ console.error(`[deleteSession] Failed to update history.jsonl for ${sessionId}:`, err);
405
+ }
406
+ return fileDeleted || historyChanged;
407
+ }
262
408
  async function getConversation(sessionId) {
263
409
  return dedupe(`getConversation:${sessionId}`, async () => {
264
410
  const filePath = await findSessionFile(sessionId);
@@ -582,6 +728,49 @@ function createServer(options) {
582
728
  const projects = await getProjects();
583
729
  return c.json(projects);
584
730
  });
731
+ app.post("/api/sessions/metadata", async (c) => {
732
+ let body;
733
+ try {
734
+ body = await c.req.json();
735
+ } catch {
736
+ return c.json({ error: "Invalid JSON body" }, 400);
737
+ }
738
+ if (!body || !Array.isArray(body.ids)) {
739
+ return c.json({ error: "Missing ids" }, 400);
740
+ }
741
+ const sessions = await getSessionsMetadata(body.ids);
742
+ return c.json({ sessions });
743
+ });
744
+ app.post("/api/sessions/delete", async (c) => {
745
+ let body;
746
+ try {
747
+ body = await c.req.json();
748
+ } catch {
749
+ return c.json({ error: "Invalid JSON body" }, 400);
750
+ }
751
+ if (!body || !Array.isArray(body.ids)) {
752
+ return c.json({ error: "Missing ids" }, 400);
753
+ }
754
+ console.log(`[/api/sessions/delete] Received request to delete ${body.ids.length} sessions:`, body.ids);
755
+ const deleted = [];
756
+ const failed = [];
757
+ for (const id of body.ids) {
758
+ if (typeof id !== "string" || !id) {
759
+ console.log(`[/api/sessions/delete] Skipping invalid id:`, id);
760
+ continue;
761
+ }
762
+ const ok = await deleteSession(id);
763
+ if (ok) {
764
+ deleted.push(id);
765
+ } else {
766
+ failed.push(id);
767
+ }
768
+ }
769
+ console.log(`[/api/sessions/delete] Result: ${deleted.length} deleted, ${failed.length} failed`);
770
+ console.log(`[/api/sessions/delete] Deleted:`, deleted);
771
+ console.log(`[/api/sessions/delete] Failed:`, failed);
772
+ return c.json({ deleted, failed });
773
+ });
585
774
  app.post("/api/search", async (c) => {
586
775
  let body;
587
776
  try {