@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 +11 -0
- package/dist/index.js +190 -1
- package/dist/web/assets/index-BgICkv1W.js +318 -0
- package/dist/web/assets/index-DM8xkSFM.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BFGsV3Tx.css +0 -1
- package/dist/web/assets/index-C0duTJht.js +0 -318
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 {
|