claude-plan-viewer 1.4.1 → 1.5.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 +8 -0
- package/index.ts +126 -162
- package/package.json +7 -3
- package/src/api-docs.html +14 -3
- package/src/index.html +30 -10
package/README.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Browse, search, and read your Claude Code plans in a clean web UI
|
|
4
4
|
|
|
5
|
+
[](https://claudeplans.dev)
|
|
5
6
|
[](https://www.npmjs.com/package/claude-plan-viewer)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](https://bun.sh)
|
|
8
9
|
|
|
10
|
+
**[📖 Documentation](https://claudeplans.dev)** | **[🚀 Quick Start](https://claudeplans.dev/getting-started/quickstart)** | **[📚 API Reference](https://claudeplans.dev/reference/api)**
|
|
11
|
+
|
|
9
12
|

|
|
10
13
|
|
|
11
14
|
## Features
|
|
@@ -66,6 +69,10 @@ claude-plan-viewer
|
|
|
66
69
|
# Start on specific port
|
|
67
70
|
claude-plan-viewer --port 8080
|
|
68
71
|
claude-plan-viewer -p 8080
|
|
72
|
+
|
|
73
|
+
# Listen on all network interfaces (accessible from other devices)
|
|
74
|
+
claude-plan-viewer --host 0.0.0.0
|
|
75
|
+
claude-plan-viewer -H 0.0.0.0
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
The server will automatically find an available port if the requested port is in use.
|
|
@@ -75,6 +82,7 @@ The server will automatically find an available port if the requested port is in
|
|
|
75
82
|
| Flag | Short | Description |
|
|
76
83
|
| --------------------- | ----- | --------------------------------------------------------- |
|
|
77
84
|
| `--port <number>` | `-p` | Port to start the server on (default: 3000) |
|
|
85
|
+
| `--host <address>` | `-H` | Host to bind to (default: localhost) |
|
|
78
86
|
| `--claude-dir <path>` | `-c` | Path to `.claude` directory (default: `~/.claude`) |
|
|
79
87
|
| `--json` | `-j` | Export all plans as JSON and exit |
|
|
80
88
|
| `--output <file>` | `-o` | Output file for JSON export (prints to stdout if omitted) |
|
package/index.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { readdir, stat, watch } from "node:fs/promises";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
5
7
|
import index from "./src/index.html";
|
|
6
8
|
import apiDocs from "./src/api-docs.html";
|
|
7
9
|
import pkg from "./package.json";
|
|
8
10
|
import openapi from "./openapi.json";
|
|
11
|
+
import getPort, { portNumbers } from "get-port";
|
|
9
12
|
|
|
10
13
|
// Resolved at startup based on --claude-dir flag or CLAUDE_DIR env var
|
|
11
14
|
let PLANS_DIR: string;
|
|
@@ -22,6 +25,7 @@ function initializeDirectories(claudeDir: string): void {
|
|
|
22
25
|
|
|
23
26
|
interface CliArgs {
|
|
24
27
|
port?: number;
|
|
28
|
+
host?: string;
|
|
25
29
|
json?: boolean;
|
|
26
30
|
output?: string;
|
|
27
31
|
fromFile?: string;
|
|
@@ -43,6 +47,11 @@ function parseCliArgs(): CliArgs {
|
|
|
43
47
|
args.port = parseInt(nextArg, 10);
|
|
44
48
|
i++;
|
|
45
49
|
}
|
|
50
|
+
} else if (arg === "--host" || arg === "-H") {
|
|
51
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
52
|
+
args.host = nextArg;
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
46
55
|
} else if (arg === "--json" || arg === "-j") {
|
|
47
56
|
args.json = true;
|
|
48
57
|
} else if (arg === "--output" || arg === "-o") {
|
|
@@ -78,6 +87,8 @@ Usage: claude-plan-viewer [options]
|
|
|
78
87
|
|
|
79
88
|
Options:
|
|
80
89
|
-p, --port <number> Port to start server on (default: 3000)
|
|
90
|
+
-H, --host <address> Host to bind to (default: localhost)
|
|
91
|
+
Use 0.0.0.0 to listen on all interfaces
|
|
81
92
|
-c, --claude-dir <path> Path to .claude directory (default: ~/.claude)
|
|
82
93
|
Can also be set via CLAUDE_DIR environment variable
|
|
83
94
|
-j, --json Export all plans as JSON and exit
|
|
@@ -89,6 +100,7 @@ Options:
|
|
|
89
100
|
Examples:
|
|
90
101
|
claude-plan-viewer Start viewer on default port
|
|
91
102
|
claude-plan-viewer -p 8080 Start on port 8080
|
|
103
|
+
claude-plan-viewer -H 0.0.0.0 Listen on all network interfaces
|
|
92
104
|
claude-plan-viewer -c /path/to/.claude Use custom .claude directory
|
|
93
105
|
claude-plan-viewer -j -o plans.json Export plans to file
|
|
94
106
|
claude-plan-viewer -f plans.json Load plans from exported file
|
|
@@ -147,24 +159,7 @@ async function loadPlansFromFile(filepath: string): Promise<PlanMetadata[]> {
|
|
|
147
159
|
|
|
148
160
|
// Find an available port starting from the requested port
|
|
149
161
|
async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
|
150
|
-
|
|
151
|
-
const maxAttempts = 100;
|
|
152
|
-
|
|
153
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
154
|
-
try {
|
|
155
|
-
const testServer = Bun.serve({
|
|
156
|
-
port,
|
|
157
|
-
fetch: () => new Response(),
|
|
158
|
-
});
|
|
159
|
-
testServer.stop();
|
|
160
|
-
return port;
|
|
161
|
-
} catch {
|
|
162
|
-
port++;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
throw new Error(
|
|
166
|
-
`No available port found in range ${startPort}-${startPort + maxAttempts}`,
|
|
167
|
-
);
|
|
162
|
+
return getPort({ port: portNumbers(startPort, startPort + 100) });
|
|
168
163
|
}
|
|
169
164
|
|
|
170
165
|
// Cross-platform open file in default editor
|
|
@@ -192,10 +187,6 @@ interface PlanMetadata {
|
|
|
192
187
|
sessionId: string | null;
|
|
193
188
|
}
|
|
194
189
|
|
|
195
|
-
interface Plan extends PlanMetadata {
|
|
196
|
-
content: string;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
190
|
// Extract project name from a full path (cross-platform)
|
|
200
191
|
// e.g., "/Users/helge/code/plans-viewer" -> "plans-viewer"
|
|
201
192
|
// e.g., "C:\Users\name\code\my-app" -> "my-app"
|
|
@@ -212,48 +203,27 @@ function extractProjectName(cwd: string): string {
|
|
|
212
203
|
return lastSlash === -1 ? trimmed : trimmed.slice(lastSlash + 1);
|
|
213
204
|
}
|
|
214
205
|
|
|
215
|
-
//
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
function extractSlugSessionMap(content: string): Map<string, string> {
|
|
227
|
-
if (!content) return new Map();
|
|
228
|
-
const slugSessionMap = new Map<string, string>();
|
|
229
|
-
|
|
230
|
-
// Process each line to find slug and sessionId pairs
|
|
231
|
-
const lines = content.split("\n");
|
|
232
|
-
for (const line of lines) {
|
|
233
|
-
if (!line.trim()) continue;
|
|
234
|
-
|
|
235
|
-
const slugMatch = line.match(/"slug":"([\w-]+)"/);
|
|
236
|
-
const sessionMatch = line.match(/"sessionId":"([^"]+)"/);
|
|
237
|
-
|
|
238
|
-
if (slugMatch && slugMatch[1] && sessionMatch && sessionMatch[1]) {
|
|
239
|
-
slugSessionMap.set(slugMatch[1], sessionMatch[1]);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return slugSessionMap;
|
|
244
|
-
}
|
|
206
|
+
// Stream a JSONL file line-by-line without loading the entire file into memory
|
|
207
|
+
async function processJsonlLineByLine(
|
|
208
|
+
path: string,
|
|
209
|
+
onLine: (line: string) => void
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const stream = createReadStream(path, {
|
|
213
|
+
encoding: "utf-8",
|
|
214
|
+
highWaterMark: 64 * 1024,
|
|
215
|
+
});
|
|
216
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
245
217
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
return Array.from(slugs);
|
|
218
|
+
rl.on("line", (line) => {
|
|
219
|
+
if (line.length > 0) onLine(line);
|
|
220
|
+
});
|
|
221
|
+
rl.on("close", resolve);
|
|
222
|
+
rl.on("error", (err) => {
|
|
223
|
+
rl.close();
|
|
224
|
+
reject(err);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
257
227
|
}
|
|
258
228
|
|
|
259
229
|
interface SlugMetadata {
|
|
@@ -265,74 +235,67 @@ interface ProjectMapping {
|
|
|
265
235
|
[slug: string]: SlugMetadata;
|
|
266
236
|
}
|
|
267
237
|
|
|
268
|
-
// Build a mapping of plan slugs to project names and session IDs by scanning Claude Code's project metadata
|
|
269
|
-
|
|
238
|
+
// Build a mapping of plan slugs to project names and session IDs by scanning Claude Code's project metadata.
|
|
239
|
+
// Streams JSONL files line-by-line to avoid loading multi-GB project data into memory.
|
|
240
|
+
// Only tracks slugs that correspond to actual plan files.
|
|
241
|
+
async function buildProjectMapping(
|
|
242
|
+
neededSlugs?: Set<string>
|
|
243
|
+
): Promise<ProjectMapping> {
|
|
270
244
|
const mapping: ProjectMapping = {};
|
|
271
245
|
|
|
272
246
|
try {
|
|
273
247
|
const projectDirs = await readdir(PROJECTS_DIR);
|
|
274
248
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const dirStats = await stat(dirPath);
|
|
281
|
-
if (!dirStats.isDirectory()) return null;
|
|
282
|
-
|
|
283
|
-
// Find JSONL files
|
|
284
|
-
const files = await readdir(dirPath);
|
|
285
|
-
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
286
|
-
if (jsonlFiles.length === 0) return null;
|
|
287
|
-
|
|
288
|
-
// Read all JSONL files in parallel
|
|
289
|
-
const fileContents = await Promise.all(
|
|
290
|
-
jsonlFiles.map(async (file) => {
|
|
291
|
-
try {
|
|
292
|
-
return await Bun.file(join(dirPath, file)).text();
|
|
293
|
-
} catch {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
})
|
|
297
|
-
);
|
|
249
|
+
for (const dir of projectDirs) {
|
|
250
|
+
const dirPath = join(PROJECTS_DIR, dir);
|
|
251
|
+
try {
|
|
252
|
+
const dirStats = await stat(dirPath);
|
|
253
|
+
if (!dirStats.isDirectory()) continue;
|
|
298
254
|
|
|
299
|
-
|
|
300
|
-
|
|
255
|
+
const files = await readdir(dirPath);
|
|
256
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
257
|
+
if (jsonlFiles.length === 0) continue;
|
|
301
258
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (!content) continue;
|
|
259
|
+
let projectName: string | null = null;
|
|
260
|
+
const slugSessionMap = new Map<string, string>();
|
|
305
261
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
262
|
+
// Process files sequentially to keep memory bounded
|
|
263
|
+
for (const file of jsonlFiles) {
|
|
264
|
+
try {
|
|
265
|
+
await processJsonlLineByLine(join(dirPath, file), (line) => {
|
|
266
|
+
// Extract project name from cwd (only need first occurrence)
|
|
267
|
+
if (!projectName) {
|
|
268
|
+
const cwdMatch = line.match(/"cwd":"([^"]+)"/);
|
|
269
|
+
if (cwdMatch?.[1]) {
|
|
270
|
+
const cwd = cwdMatch[1].replace(/\\\\/g, "\\");
|
|
271
|
+
projectName = extractProjectName(cwd);
|
|
272
|
+
}
|
|
311
273
|
}
|
|
312
|
-
}
|
|
313
274
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
275
|
+
// Extract slug-sessionId pairs, filtering to only needed slugs
|
|
276
|
+
const slugMatch = line.match(/"slug":"([\w-]+)"/);
|
|
277
|
+
if (
|
|
278
|
+
slugMatch?.[1] &&
|
|
279
|
+
(!neededSlugs || neededSlugs.has(slugMatch[1]))
|
|
280
|
+
) {
|
|
281
|
+
const sessionMatch = line.match(/"sessionId":"([^"]+)"/);
|
|
282
|
+
if (sessionMatch?.[1]) {
|
|
283
|
+
slugSessionMap.set(slugMatch[1], sessionMatch[1]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
} catch {
|
|
288
|
+
// Skip unreadable files
|
|
319
289
|
}
|
|
320
|
-
|
|
321
|
-
return { projectName, slugSessionMap };
|
|
322
|
-
} catch {
|
|
323
|
-
return null;
|
|
324
290
|
}
|
|
325
|
-
})
|
|
326
|
-
);
|
|
327
291
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
};
|
|
292
|
+
if (projectName) {
|
|
293
|
+
for (const [slug, sessionId] of slugSessionMap) {
|
|
294
|
+
mapping[slug] = { project: projectName, sessionId };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Skip inaccessible directories
|
|
336
299
|
}
|
|
337
300
|
}
|
|
338
301
|
} catch {
|
|
@@ -347,37 +310,39 @@ let cachedProjectMapping: ProjectMapping | null = null;
|
|
|
347
310
|
const contentCache = new Map<string, string>();
|
|
348
311
|
|
|
349
312
|
async function loadPlans(): Promise<PlanMetadata[]> {
|
|
350
|
-
|
|
313
|
+
const files = await readdir(PLANS_DIR);
|
|
314
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
315
|
+
|
|
316
|
+
// Build or use cached project mapping, passing needed slugs for targeted lookup
|
|
351
317
|
let projectMapping: ProjectMapping;
|
|
352
318
|
if (!cachedProjectMapping) {
|
|
353
|
-
|
|
319
|
+
const neededSlugs = new Set(mdFiles.map((f) => f.replace(".md", "")));
|
|
320
|
+
projectMapping = await buildProjectMapping(neededSlugs);
|
|
354
321
|
cachedProjectMapping = projectMapping;
|
|
355
322
|
} else {
|
|
356
323
|
projectMapping = cachedProjectMapping;
|
|
357
324
|
}
|
|
358
325
|
|
|
359
|
-
const files = await readdir(PLANS_DIR);
|
|
360
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
361
|
-
|
|
362
326
|
const plans = await Promise.all(
|
|
363
327
|
mdFiles.map(async (filename) => {
|
|
364
328
|
const filepath = join(PLANS_DIR, filename);
|
|
365
329
|
const file = Bun.file(filepath);
|
|
366
330
|
|
|
367
|
-
const [content, stats] = await Promise.all([
|
|
331
|
+
const [content, stats] = await Promise.all([
|
|
332
|
+
file.text(),
|
|
333
|
+
stat(filepath),
|
|
334
|
+
]);
|
|
368
335
|
|
|
369
336
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
370
337
|
const title = titleMatch?.[1]
|
|
371
338
|
? titleMatch[1].replace(/^Plan:\s*/i, "")
|
|
372
339
|
: filename.replace(".md", "");
|
|
373
340
|
|
|
374
|
-
// Look up project from metadata using plan slug (filename without .md)
|
|
375
341
|
const slug = filename.replace(".md", "");
|
|
376
342
|
const lineCount = content.split("\n").length;
|
|
377
343
|
const wordCount = content.split(/\s+/).filter(Boolean).length;
|
|
378
344
|
|
|
379
345
|
const metadata = projectMapping[slug];
|
|
380
|
-
// Cache content separately for search and lazy loading
|
|
381
346
|
contentCache.set(filename, content);
|
|
382
347
|
|
|
383
348
|
return {
|
|
@@ -440,12 +405,11 @@ async function watchPlansDirectory() {
|
|
|
440
405
|
}
|
|
441
406
|
|
|
442
407
|
// Main server startup
|
|
443
|
-
async function startServer() {
|
|
444
|
-
const args = parseCliArgs();
|
|
445
|
-
const port = await findAvailablePort(args.port ?? 3000);
|
|
408
|
+
async function startServer(port: number, host?: string) {
|
|
446
409
|
|
|
447
410
|
const server = Bun.serve({
|
|
448
411
|
port,
|
|
412
|
+
hostname: host,
|
|
449
413
|
routes: {
|
|
450
414
|
"/": index,
|
|
451
415
|
"/api": () => Response.redirect("/api/", 301),
|
|
@@ -509,7 +473,10 @@ async function startServer() {
|
|
|
509
473
|
"/api/refresh": {
|
|
510
474
|
POST: async () => {
|
|
511
475
|
const before = cachedPlans?.length ?? 0;
|
|
512
|
-
|
|
476
|
+
// Only invalidate plans and content; project mapping is expensive
|
|
477
|
+
// to rebuild (streams all JSONL files) and rarely changes
|
|
478
|
+
invalidatePlansCache();
|
|
479
|
+
invalidateContentCache();
|
|
513
480
|
await loadPlans();
|
|
514
481
|
const after = cachedPlans?.length ?? 0;
|
|
515
482
|
return Response.json({ success: true, before, after });
|
|
@@ -542,16 +509,10 @@ async function startServer() {
|
|
|
542
509
|
return server;
|
|
543
510
|
}
|
|
544
511
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
dim: "\x1b[2m",
|
|
550
|
-
green: "\x1b[32m",
|
|
551
|
-
cyan: "\x1b[36m",
|
|
552
|
-
yellow: "\x1b[33m",
|
|
553
|
-
magenta: "\x1b[35m",
|
|
554
|
-
};
|
|
512
|
+
// OSC 8 hyperlink escape sequence for clickable terminal URLs
|
|
513
|
+
function link(url: string, text?: string): string {
|
|
514
|
+
return `\x1b]8;;${url}\x07${text ?? url}\x1b]8;;\x07`;
|
|
515
|
+
}
|
|
555
516
|
|
|
556
517
|
// Main entry point
|
|
557
518
|
(async () => {
|
|
@@ -576,8 +537,13 @@ const c = {
|
|
|
576
537
|
process.exit(0);
|
|
577
538
|
}
|
|
578
539
|
|
|
579
|
-
//
|
|
540
|
+
// Start server first
|
|
541
|
+
const port = await findAvailablePort(args.port ?? 3000);
|
|
542
|
+
const server = await startServer(port, args.host);
|
|
543
|
+
|
|
544
|
+
// Pre-load plans from file or directory
|
|
580
545
|
let planCount: number;
|
|
546
|
+
let projectCount = 0;
|
|
581
547
|
let sourceDisplay: string;
|
|
582
548
|
|
|
583
549
|
if (args.fromFile) {
|
|
@@ -585,32 +551,30 @@ const c = {
|
|
|
585
551
|
planCount = plans.length;
|
|
586
552
|
sourceDisplay = args.fromFile;
|
|
587
553
|
} else {
|
|
588
|
-
planCount = (await readdir(PLANS_DIR)).filter((f) =>
|
|
589
|
-
f.endsWith(".md"),
|
|
590
|
-
).length;
|
|
591
554
|
sourceDisplay = PLANS_DIR;
|
|
592
555
|
// Only watch for file changes when not using --from-file
|
|
593
556
|
watchPlansDirectory();
|
|
557
|
+
|
|
558
|
+
await loadPlans();
|
|
559
|
+
|
|
560
|
+
const projects = new Set(
|
|
561
|
+
(cachedPlans || []).map((p) => p.project).filter(Boolean)
|
|
562
|
+
);
|
|
563
|
+
projectCount = projects.size;
|
|
564
|
+
planCount = cachedPlans?.length ?? 0;
|
|
594
565
|
}
|
|
595
566
|
|
|
596
|
-
const
|
|
567
|
+
const localUrl = `http://localhost:${server.port}/`;
|
|
568
|
+
const apiUrl = `http://localhost:${server.port}/api/`;
|
|
569
|
+
const dirUrl = `file://${claudeDir}`;
|
|
597
570
|
|
|
598
|
-
console.log();
|
|
599
|
-
console.log(
|
|
600
|
-
console.log(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
571
|
+
console.log(`\nclaude-plan-viewer v${pkg.version}\n`);
|
|
572
|
+
console.log(` ➜ Web: ${link(localUrl)}`);
|
|
573
|
+
console.log(` ➜ API: ${link(apiUrl)}`);
|
|
574
|
+
if (args.fromFile) {
|
|
575
|
+
console.log(` ➜ Source: ${sourceDisplay}`);
|
|
576
|
+
} else {
|
|
577
|
+
console.log(` ➜ Dir: ${link(dirUrl)} (${projectCount} projects)`);
|
|
604
578
|
}
|
|
605
|
-
console.log();
|
|
606
|
-
console.log(
|
|
607
|
-
`${c.dim} Local:${c.reset} ${c.cyan}${c.bold}http://localhost:${server.port}${c.reset}`,
|
|
608
|
-
);
|
|
609
|
-
console.log(
|
|
610
|
-
`${c.dim} API:${c.reset} ${c.cyan}http://localhost:${server.port}/api/${c.reset}`,
|
|
611
|
-
);
|
|
612
|
-
console.log(
|
|
613
|
-
`${c.dim} Plans:${c.reset} ${c.yellow}${planCount} plans${c.reset} in ${c.dim}${sourceDisplay}${c.reset}`,
|
|
614
|
-
);
|
|
615
|
-
console.log();
|
|
579
|
+
console.log(`\n Serving ${planCount} plans\n`);
|
|
616
580
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-plan-viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A web-based viewer for Claude Code plan files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"install:local": "bun scripts/install-local.ts",
|
|
30
30
|
"uninstall:link": "bun unlink",
|
|
31
31
|
"uninstall:local": "bun scripts/uninstall-local.ts",
|
|
32
|
-
"format": "bunx prettier --write src"
|
|
32
|
+
"format": "bunx prettier --write src",
|
|
33
|
+
"website:dev": "cd website/docs && bun run dev",
|
|
34
|
+
"website:build": "cd website/docs && bun run build",
|
|
35
|
+
"website:preview": "cd website/docs && bun run preview"
|
|
33
36
|
},
|
|
34
37
|
"keywords": [
|
|
35
38
|
"claude",
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
"bugs": {
|
|
48
51
|
"url": "https://github.com/HelgeSverre/claude-plan-viewer/issues"
|
|
49
52
|
},
|
|
50
|
-
"homepage": "https://
|
|
53
|
+
"homepage": "https://claudeplans.dev",
|
|
51
54
|
"engines": {
|
|
52
55
|
"bun": ">=1.0.0"
|
|
53
56
|
},
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
"typescript": "^5.9.3"
|
|
64
67
|
},
|
|
65
68
|
"dependencies": {
|
|
69
|
+
"get-port": "^7.1.0",
|
|
66
70
|
"react": "^19.2.3",
|
|
67
71
|
"react-dom": "^19.2.3",
|
|
68
72
|
"react-markdown": "^10.1.0",
|
package/src/api-docs.html
CHANGED
|
@@ -9,9 +9,20 @@
|
|
|
9
9
|
<div id="app"></div>
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
11
11
|
<script>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
13
|
+
|
|
14
|
+
function createApiReference() {
|
|
15
|
+
document.getElementById("app").innerHTML = "";
|
|
16
|
+
Scalar.createApiReference("#app", {
|
|
17
|
+
url: "/api/openapi.json",
|
|
18
|
+
darkMode: darkModeQuery.matches,
|
|
19
|
+
hideDarkModeToggle: true,
|
|
20
|
+
hideClientButton: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
createApiReference();
|
|
25
|
+
darkModeQuery.addEventListener("change", createApiReference);
|
|
15
26
|
</script>
|
|
16
27
|
</body>
|
|
17
28
|
</html>
|
package/src/index.html
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Claude Plans Viewer</title>
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="Browse, search, and read your Claude Code plans in a clean web UI"
|
|
10
|
+
/>
|
|
11
|
+
<meta name="author" content="Helge Sverre" />
|
|
12
|
+
<meta property="og:title" content="Claude Plans Viewer" />
|
|
13
|
+
<meta
|
|
14
|
+
property="og:description"
|
|
15
|
+
content="Browse, search, and read your Claude Code plans in a clean web UI"
|
|
16
|
+
/>
|
|
17
|
+
<meta property="og:type" content="website" />
|
|
18
|
+
<meta property="og:url" content="https://claudeplans.dev" />
|
|
19
|
+
<meta property="og:site_name" content="Claude Plans Viewer" />
|
|
20
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
21
|
+
<meta name="twitter:title" content="Claude Plans Viewer" />
|
|
22
|
+
<meta
|
|
23
|
+
name="twitter:description"
|
|
24
|
+
content="Browse, search, and read your Claude Code plans in a clean web UI"
|
|
25
|
+
/>
|
|
26
|
+
<link rel="canonical" href="https://claudeplans.dev" />
|
|
27
|
+
<link rel="stylesheet" href="./styles/styles.css" />
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="root"></div>
|
|
31
|
+
<script type="module" src="./client/index.tsx"></script>
|
|
32
|
+
</body>
|
|
13
33
|
</html>
|