@teddysc/claude-run 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kamran Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ <div align="center">
2
+
3
+ # Claude Run
4
+
5
+ Browse your Claude Code conversation history in a beautiful web UI
6
+
7
+ [![npm version](https://img.shields.io/npm/v/claude-run.svg)](https://www.npmjs.com/package/claude-run)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ <img src=".github/claude-run.gif" alt="Claude Run Demo" width="800" />
11
+
12
+ </div>
13
+
14
+ <br />
15
+
16
+ Run the project simply by executing
17
+
18
+ ```bash
19
+ npx claude-run
20
+ ```
21
+
22
+ The browser will open automatically at http://localhost:12001.
23
+
24
+ ## Features
25
+
26
+ - **Real-time streaming** - Watch conversations update live as Claude responds
27
+ - **Search** - Find sessions by prompt text or project name
28
+ - **Filter by project** - Focus on specific projects
29
+ - **Resume sessions** - Copy the resume command to continue any conversation in your terminal
30
+ - **Collapsible sidebar** - Maximize your viewing area
31
+ - **Dark mode** - Easy on the eyes
32
+ - **Clean UI** - Familiar chat interface with collapsible tool calls
33
+
34
+ ## Usage
35
+
36
+ Install globally via npm:
37
+
38
+ ```bash
39
+ npm install -g claude-run
40
+ ```
41
+
42
+ Then run it from any directory:
43
+
44
+ ```bash
45
+ claude-run
46
+ ```
47
+
48
+ The browser will open automatically at http://localhost:12001, showing all your Claude Code conversations.
49
+
50
+ ```bash
51
+ claude-run [options]
52
+
53
+ Options:
54
+ -V, --version Show version number
55
+ -p, --port <number> Port to listen on (default: 12001)
56
+ -d, --dir <path> Claude directory (default: ~/.claude)
57
+ --no-open Do not open browser automatically
58
+ -h, --help Show help
59
+ ```
60
+
61
+ ## How It Works
62
+
63
+ Claude Code stores conversation history in `~/.claude/`. This tool reads that data and presents it in a web interface with:
64
+
65
+ - **Session list** - All your conversations, sorted by recency
66
+ - **Project filter** - Focus on a specific project
67
+ - **Conversation view** - Full message history with tool calls
68
+ - **Session header** - Shows conversation title, project name, and timestamp
69
+ - **Resume command** - Copies the command to resume the conversation
70
+ - **Real-time updates** - SSE streaming for live conversations
71
+
72
+ ## Requirements
73
+
74
+ - Node.js 20+
75
+ - Claude Code installed and used at least once
76
+
77
+ ## Development
78
+
79
+ ```bash
80
+ # Clone the repo
81
+ git clone https://github.com/kamranahmedse/claude-run.git
82
+ cd claude-run
83
+
84
+ # Install dependencies
85
+ pnpm install
86
+
87
+ # Start development servers
88
+ pnpm dev
89
+
90
+ # Build for production
91
+ pnpm build
92
+ ```
93
+
94
+ ## License
95
+
96
+ MIT © Kamran Ahmed
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+
3
+ // api/index.ts
4
+ import { program } from "commander";
5
+
6
+ // api/server.ts
7
+ import { Hono } from "hono";
8
+ import { cors } from "hono/cors";
9
+ import { serveStatic } from "@hono/node-server/serve-static";
10
+ import { streamSSE } from "hono/streaming";
11
+ import { serve } from "@hono/node-server";
12
+
13
+ // api/storage.ts
14
+ import { readdir, readFile, stat, open } from "fs/promises";
15
+ import { join, basename } from "path";
16
+ import { homedir } from "os";
17
+ import { createInterface } from "readline";
18
+ var claudeDir = join(homedir(), ".claude");
19
+ var projectsDir = join(claudeDir, "projects");
20
+ var fileIndex = /* @__PURE__ */ new Map();
21
+ var historyCache = null;
22
+ var pendingRequests = /* @__PURE__ */ new Map();
23
+ function initStorage(dir) {
24
+ claudeDir = dir ?? join(homedir(), ".claude");
25
+ projectsDir = join(claudeDir, "projects");
26
+ }
27
+ function getClaudeDir() {
28
+ return claudeDir;
29
+ }
30
+ function invalidateHistoryCache() {
31
+ historyCache = null;
32
+ }
33
+ function addToFileIndex(sessionId, filePath) {
34
+ fileIndex.set(sessionId, filePath);
35
+ }
36
+ function encodeProjectPath(path) {
37
+ return path.replace(/[/.]/g, "-");
38
+ }
39
+ function getProjectName(projectPath) {
40
+ const parts = projectPath.split("/").filter(Boolean);
41
+ return parts[parts.length - 1] || projectPath;
42
+ }
43
+ async function buildFileIndex() {
44
+ try {
45
+ const projectDirs = await readdir(projectsDir, { withFileTypes: true });
46
+ const directories = projectDirs.filter((d) => d.isDirectory());
47
+ await Promise.all(
48
+ directories.map(async (dir) => {
49
+ try {
50
+ const projectPath = join(projectsDir, dir.name);
51
+ const files = await readdir(projectPath);
52
+ for (const file of files) {
53
+ if (file.endsWith(".jsonl")) {
54
+ const sessionId = basename(file, ".jsonl");
55
+ fileIndex.set(sessionId, join(projectPath, file));
56
+ }
57
+ }
58
+ } catch {
59
+ }
60
+ })
61
+ );
62
+ } catch {
63
+ }
64
+ }
65
+ async function loadHistoryCache() {
66
+ try {
67
+ const historyPath = join(claudeDir, "history.jsonl");
68
+ const content = await readFile(historyPath, "utf-8");
69
+ const lines = content.trim().split("\n").filter(Boolean);
70
+ const entries = [];
71
+ for (const line of lines) {
72
+ try {
73
+ entries.push(JSON.parse(line));
74
+ } catch {
75
+ }
76
+ }
77
+ historyCache = entries;
78
+ return entries;
79
+ } catch {
80
+ historyCache = [];
81
+ return [];
82
+ }
83
+ }
84
+ async function dedupe(key, fn) {
85
+ const existing = pendingRequests.get(key);
86
+ if (existing) {
87
+ return existing;
88
+ }
89
+ const promise = fn().finally(() => {
90
+ pendingRequests.delete(key);
91
+ });
92
+ pendingRequests.set(key, promise);
93
+ return promise;
94
+ }
95
+ async function findSessionByTimestamp(encodedProject, timestamp) {
96
+ try {
97
+ const projectPath = join(projectsDir, encodedProject);
98
+ const files = await readdir(projectPath);
99
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
100
+ const fileStats = await Promise.all(
101
+ jsonlFiles.map(async (file) => {
102
+ const filePath = join(projectPath, file);
103
+ const fileStat = await stat(filePath);
104
+ return { file, mtime: fileStat.mtimeMs };
105
+ })
106
+ );
107
+ let closestFile = null;
108
+ let closestTimeDiff = Infinity;
109
+ for (const { file, mtime } of fileStats) {
110
+ const timeDiff = Math.abs(mtime - timestamp);
111
+ if (timeDiff < closestTimeDiff) {
112
+ closestTimeDiff = timeDiff;
113
+ closestFile = file;
114
+ }
115
+ }
116
+ if (closestFile) {
117
+ return basename(closestFile, ".jsonl");
118
+ }
119
+ } catch {
120
+ }
121
+ return void 0;
122
+ }
123
+ async function findSessionFile(sessionId) {
124
+ if (fileIndex.has(sessionId)) {
125
+ return fileIndex.get(sessionId);
126
+ }
127
+ const targetFile = `${sessionId}.jsonl`;
128
+ try {
129
+ const projectDirs = await readdir(projectsDir, { withFileTypes: true });
130
+ const directories = projectDirs.filter((d) => d.isDirectory());
131
+ const results = await Promise.all(
132
+ directories.map(async (dir) => {
133
+ try {
134
+ const projectPath = join(projectsDir, dir.name);
135
+ const files = await readdir(projectPath);
136
+ if (files.includes(targetFile)) {
137
+ return join(projectPath, targetFile);
138
+ }
139
+ } catch {
140
+ }
141
+ return null;
142
+ })
143
+ );
144
+ const filePath = results.find((r) => r !== null);
145
+ if (filePath) {
146
+ fileIndex.set(sessionId, filePath);
147
+ return filePath;
148
+ }
149
+ } catch (err) {
150
+ console.error("Error finding session file:", err);
151
+ }
152
+ return null;
153
+ }
154
+ async function loadStorage() {
155
+ await Promise.all([buildFileIndex(), loadHistoryCache()]);
156
+ }
157
+ async function getSessions() {
158
+ return dedupe("getSessions", async () => {
159
+ const entries = historyCache ?? await loadHistoryCache();
160
+ const sessions = [];
161
+ const seenIds = /* @__PURE__ */ new Set();
162
+ for (const entry of entries) {
163
+ let sessionId = entry.sessionId;
164
+ if (!sessionId) {
165
+ const encodedProject = encodeProjectPath(entry.project);
166
+ sessionId = await findSessionByTimestamp(encodedProject, entry.timestamp);
167
+ }
168
+ if (!sessionId || seenIds.has(sessionId)) {
169
+ continue;
170
+ }
171
+ seenIds.add(sessionId);
172
+ sessions.push({
173
+ id: sessionId,
174
+ display: entry.display,
175
+ timestamp: entry.timestamp,
176
+ project: entry.project,
177
+ projectName: getProjectName(entry.project)
178
+ });
179
+ }
180
+ return sessions.sort((a, b) => b.timestamp - a.timestamp);
181
+ });
182
+ }
183
+ async function getProjects() {
184
+ const entries = historyCache ?? await loadHistoryCache();
185
+ const projects = /* @__PURE__ */ new Set();
186
+ for (const entry of entries) {
187
+ if (entry.project) {
188
+ projects.add(entry.project);
189
+ }
190
+ }
191
+ return [...projects].sort();
192
+ }
193
+ async function getConversation(sessionId) {
194
+ return dedupe(`getConversation:${sessionId}`, async () => {
195
+ const filePath = await findSessionFile(sessionId);
196
+ if (!filePath) {
197
+ return [];
198
+ }
199
+ const messages = [];
200
+ try {
201
+ const content = await readFile(filePath, "utf-8");
202
+ const lines = content.trim().split("\n").filter(Boolean);
203
+ for (const line of lines) {
204
+ try {
205
+ const msg = JSON.parse(line);
206
+ if (msg.type === "user" || msg.type === "assistant") {
207
+ messages.push(msg);
208
+ } else if (msg.type === "summary") {
209
+ messages.unshift(msg);
210
+ }
211
+ } catch {
212
+ }
213
+ }
214
+ } catch (err) {
215
+ console.error("Error reading conversation:", err);
216
+ }
217
+ return messages;
218
+ });
219
+ }
220
+ async function getConversationStream(sessionId, fromOffset = 0) {
221
+ const filePath = await findSessionFile(sessionId);
222
+ if (!filePath) {
223
+ return { messages: [], nextOffset: 0 };
224
+ }
225
+ const messages = [];
226
+ let fileHandle;
227
+ try {
228
+ const fileStat = await stat(filePath);
229
+ const fileSize = fileStat.size;
230
+ if (fromOffset >= fileSize) {
231
+ return { messages: [], nextOffset: fromOffset };
232
+ }
233
+ fileHandle = await open(filePath, "r");
234
+ const stream = fileHandle.createReadStream({
235
+ start: fromOffset,
236
+ encoding: "utf-8"
237
+ });
238
+ const rl = createInterface({
239
+ input: stream,
240
+ crlfDelay: Infinity
241
+ });
242
+ let bytesConsumed = 0;
243
+ for await (const line of rl) {
244
+ const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
245
+ if (line.trim()) {
246
+ try {
247
+ const msg = JSON.parse(line);
248
+ if (msg.type === "user" || msg.type === "assistant") {
249
+ messages.push(msg);
250
+ }
251
+ bytesConsumed += lineBytes;
252
+ } catch {
253
+ break;
254
+ }
255
+ } else {
256
+ bytesConsumed += lineBytes;
257
+ }
258
+ }
259
+ const actualOffset = fromOffset + bytesConsumed;
260
+ const nextOffset = actualOffset > fileSize ? fileSize : actualOffset;
261
+ return { messages, nextOffset };
262
+ } catch (err) {
263
+ console.error("Error reading conversation stream:", err);
264
+ return { messages: [], nextOffset: fromOffset };
265
+ } finally {
266
+ if (fileHandle) {
267
+ await fileHandle.close();
268
+ }
269
+ }
270
+ }
271
+
272
+ // api/watcher.ts
273
+ import { watch } from "chokidar";
274
+ import { basename as basename2, join as join2 } from "path";
275
+ var watcher = null;
276
+ var claudeDir2 = "";
277
+ var debounceTimers = /* @__PURE__ */ new Map();
278
+ var debounceMs = 20;
279
+ var historyChangeListeners = /* @__PURE__ */ new Set();
280
+ var sessionChangeListeners = /* @__PURE__ */ new Set();
281
+ function initWatcher(dir) {
282
+ claudeDir2 = dir;
283
+ }
284
+ function emitChange(filePath) {
285
+ if (filePath.endsWith("history.jsonl")) {
286
+ for (const callback of historyChangeListeners) {
287
+ callback();
288
+ }
289
+ } else if (filePath.endsWith(".jsonl")) {
290
+ const sessionId = basename2(filePath, ".jsonl");
291
+ for (const callback of sessionChangeListeners) {
292
+ callback(sessionId, filePath);
293
+ }
294
+ }
295
+ }
296
+ function handleChange(path) {
297
+ const existing = debounceTimers.get(path);
298
+ if (existing) {
299
+ clearTimeout(existing);
300
+ }
301
+ const timer = setTimeout(() => {
302
+ debounceTimers.delete(path);
303
+ emitChange(path);
304
+ }, debounceMs);
305
+ debounceTimers.set(path, timer);
306
+ }
307
+ function startWatcher() {
308
+ if (watcher) {
309
+ return;
310
+ }
311
+ const historyPath = join2(claudeDir2, "history.jsonl");
312
+ const projectsDir2 = join2(claudeDir2, "projects");
313
+ const usePolling = process.env.CLAUDE_RUN_USE_POLLING === "1";
314
+ watcher = watch([historyPath, projectsDir2], {
315
+ persistent: true,
316
+ ignoreInitial: true,
317
+ usePolling,
318
+ ...usePolling && { interval: 100 },
319
+ depth: 2
320
+ });
321
+ watcher.on("change", handleChange);
322
+ watcher.on("add", handleChange);
323
+ watcher.on("error", (error) => {
324
+ console.error("Watcher error:", error);
325
+ });
326
+ }
327
+ function stopWatcher() {
328
+ if (watcher) {
329
+ watcher.close();
330
+ watcher = null;
331
+ }
332
+ for (const timer of debounceTimers.values()) {
333
+ clearTimeout(timer);
334
+ }
335
+ debounceTimers.clear();
336
+ }
337
+ function onHistoryChange(callback) {
338
+ historyChangeListeners.add(callback);
339
+ }
340
+ function offHistoryChange(callback) {
341
+ historyChangeListeners.delete(callback);
342
+ }
343
+ function onSessionChange(callback) {
344
+ sessionChangeListeners.add(callback);
345
+ }
346
+ function offSessionChange(callback) {
347
+ sessionChangeListeners.delete(callback);
348
+ }
349
+
350
+ // api/server.ts
351
+ import { join as join3, dirname } from "path";
352
+ import { fileURLToPath } from "url";
353
+ import { readFileSync, existsSync } from "fs";
354
+ import open2 from "open";
355
+ var __filename = fileURLToPath(import.meta.url);
356
+ var __dirname = dirname(__filename);
357
+ function getWebDistPath() {
358
+ const prodPath = join3(__dirname, "web");
359
+ if (existsSync(prodPath)) {
360
+ return prodPath;
361
+ }
362
+ return join3(__dirname, "..", "dist", "web");
363
+ }
364
+ function createServer(options) {
365
+ const {
366
+ port,
367
+ host = "127.0.0.1",
368
+ claudeDir: claudeDir3,
369
+ dev = false,
370
+ open: shouldOpen = true
371
+ } = options;
372
+ initStorage(claudeDir3);
373
+ initWatcher(getClaudeDir());
374
+ const app = new Hono();
375
+ if (dev) {
376
+ app.use(
377
+ "*",
378
+ cors({
379
+ origin: ["http://localhost:12000"],
380
+ allowMethods: ["GET", "POST", "OPTIONS"],
381
+ allowHeaders: ["Content-Type"]
382
+ })
383
+ );
384
+ }
385
+ app.get("/api/sessions", async (c) => {
386
+ const sessions = await getSessions();
387
+ return c.json(sessions);
388
+ });
389
+ app.get("/api/projects", async (c) => {
390
+ const projects = await getProjects();
391
+ return c.json(projects);
392
+ });
393
+ app.get("/api/sessions/stream", async (c) => {
394
+ return streamSSE(c, async (stream) => {
395
+ let isConnected = true;
396
+ const knownSessions = /* @__PURE__ */ new Map();
397
+ const cleanup = () => {
398
+ isConnected = false;
399
+ offHistoryChange(handleHistoryChange);
400
+ };
401
+ const handleHistoryChange = async () => {
402
+ if (!isConnected) {
403
+ return;
404
+ }
405
+ try {
406
+ const sessions = await getSessions();
407
+ const newOrUpdated = sessions.filter((s) => {
408
+ const known = knownSessions.get(s.id);
409
+ return known === void 0 || known !== s.timestamp;
410
+ });
411
+ for (const s of sessions) {
412
+ knownSessions.set(s.id, s.timestamp);
413
+ }
414
+ if (newOrUpdated.length > 0) {
415
+ await stream.writeSSE({
416
+ event: "sessionsUpdate",
417
+ data: JSON.stringify(newOrUpdated)
418
+ });
419
+ }
420
+ } catch {
421
+ cleanup();
422
+ }
423
+ };
424
+ onHistoryChange(handleHistoryChange);
425
+ c.req.raw.signal.addEventListener("abort", cleanup);
426
+ try {
427
+ const sessions = await getSessions();
428
+ for (const s of sessions) {
429
+ knownSessions.set(s.id, s.timestamp);
430
+ }
431
+ await stream.writeSSE({
432
+ event: "sessions",
433
+ data: JSON.stringify(sessions)
434
+ });
435
+ while (isConnected) {
436
+ await stream.writeSSE({
437
+ event: "heartbeat",
438
+ data: JSON.stringify({ timestamp: Date.now() })
439
+ });
440
+ await stream.sleep(3e4);
441
+ }
442
+ } catch {
443
+ } finally {
444
+ cleanup();
445
+ }
446
+ });
447
+ });
448
+ app.get("/api/conversation/:id", async (c) => {
449
+ const sessionId = c.req.param("id");
450
+ const messages = await getConversation(sessionId);
451
+ return c.json(messages);
452
+ });
453
+ app.get("/api/conversation/:id/stream", async (c) => {
454
+ const sessionId = c.req.param("id");
455
+ const offsetParam = c.req.query("offset");
456
+ let offset = offsetParam ? parseInt(offsetParam, 10) : 0;
457
+ return streamSSE(c, async (stream) => {
458
+ let isConnected = true;
459
+ const cleanup = () => {
460
+ isConnected = false;
461
+ offSessionChange(handleSessionChange);
462
+ };
463
+ const handleSessionChange = async (changedSessionId) => {
464
+ if (changedSessionId !== sessionId || !isConnected) {
465
+ return;
466
+ }
467
+ const { messages: newMessages, nextOffset: newOffset } = await getConversationStream(sessionId, offset);
468
+ offset = newOffset;
469
+ if (newMessages.length > 0) {
470
+ try {
471
+ await stream.writeSSE({
472
+ event: "messages",
473
+ data: JSON.stringify(newMessages)
474
+ });
475
+ } catch {
476
+ cleanup();
477
+ }
478
+ }
479
+ };
480
+ onSessionChange(handleSessionChange);
481
+ c.req.raw.signal.addEventListener("abort", cleanup);
482
+ try {
483
+ const { messages, nextOffset } = await getConversationStream(
484
+ sessionId,
485
+ offset
486
+ );
487
+ offset = nextOffset;
488
+ await stream.writeSSE({
489
+ event: "messages",
490
+ data: JSON.stringify(messages)
491
+ });
492
+ while (isConnected) {
493
+ await stream.writeSSE({
494
+ event: "heartbeat",
495
+ data: JSON.stringify({ timestamp: Date.now() })
496
+ });
497
+ await stream.sleep(3e4);
498
+ }
499
+ } catch {
500
+ } finally {
501
+ cleanup();
502
+ }
503
+ });
504
+ });
505
+ const webDistPath = getWebDistPath();
506
+ app.use("/*", serveStatic({ root: webDistPath }));
507
+ app.get("/*", async (c) => {
508
+ const indexPath = join3(webDistPath, "index.html");
509
+ try {
510
+ const html = readFileSync(indexPath, "utf-8");
511
+ return c.html(html);
512
+ } catch {
513
+ return c.text("UI not found. Run 'pnpm build' first.", 404);
514
+ }
515
+ });
516
+ onHistoryChange(() => {
517
+ invalidateHistoryCache();
518
+ });
519
+ onSessionChange((sessionId, filePath) => {
520
+ addToFileIndex(sessionId, filePath);
521
+ });
522
+ startWatcher();
523
+ let httpServer = null;
524
+ return {
525
+ app,
526
+ port,
527
+ start: async () => {
528
+ await loadStorage();
529
+ const openUrl = `http://${host}:${dev ? 12e3 : port}/`;
530
+ console.log(`
531
+ claude-run is running at ${openUrl}
532
+ `);
533
+ if (!dev && shouldOpen) {
534
+ open2(openUrl).catch(console.error);
535
+ }
536
+ httpServer = serve({
537
+ fetch: app.fetch,
538
+ port,
539
+ hostname: host
540
+ });
541
+ return httpServer;
542
+ },
543
+ stop: () => {
544
+ stopWatcher();
545
+ if (httpServer) {
546
+ httpServer.close();
547
+ }
548
+ }
549
+ };
550
+ }
551
+
552
+ // api/index.ts
553
+ import { homedir as homedir2 } from "os";
554
+ import { join as join4 } from "path";
555
+ import { readFileSync as readFileSync2 } from "fs";
556
+ import { fileURLToPath as fileURLToPath2 } from "url";
557
+ import { dirname as dirname2 } from "path";
558
+ var __filename2 = fileURLToPath2(import.meta.url);
559
+ var __dirname2 = dirname2(__filename2);
560
+ function getVersion() {
561
+ try {
562
+ const pkgPath = join4(__dirname2, "..", "package.json");
563
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
564
+ return pkg.version;
565
+ } catch {
566
+ return "0.1.0";
567
+ }
568
+ }
569
+ program.name("claude-run").description(
570
+ "A beautiful web UI for browsing Claude Code conversation history"
571
+ ).version(getVersion()).option("-p, --port <number>", "Port to listen on", "12001").option("-H, --host <address>", "Host address to listen on", "127.0.0.1").option(
572
+ "-d, --dir <path>",
573
+ "Claude directory path",
574
+ join4(homedir2(), ".claude")
575
+ ).option("--dev", "Enable CORS for development").option("--no-open", "Do not open browser automatically").parse();
576
+ var opts = program.opts();
577
+ var server = createServer({
578
+ port: parseInt(opts.port, 10),
579
+ host: opts.host,
580
+ claudeDir: opts.dir,
581
+ dev: opts.dev,
582
+ open: opts.open
583
+ });
584
+ process.on("SIGINT", () => {
585
+ console.log("\nShutting down...");
586
+ server.stop();
587
+ process.exit(0);
588
+ });
589
+ process.on("SIGTERM", () => {
590
+ server.stop();
591
+ process.exit(0);
592
+ });
593
+ server.start().catch((err) => {
594
+ console.error("Failed to start server:", err);
595
+ process.exit(1);
596
+ });