claude-plan-viewer 1.3.0 → 1.4.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
@@ -52,7 +52,7 @@ Download a pre-built binary from the [releases page](https://github.com/HelgeSve
52
52
 
53
53
  ```bash
54
54
  bun run build
55
- ./dist/plans-viewer
55
+ ./dist/claude-plan-viewer
56
56
  ```
57
57
 
58
58
  The binary is fully self-contained (~57MB) and works offline.
@@ -60,15 +60,79 @@ The binary is fully self-contained (~57MB) and works offline.
60
60
  ## Usage
61
61
 
62
62
  ```bash
63
- # Start on auto-assigned port
63
+ # Start the web viewer
64
64
  claude-plan-viewer
65
65
 
66
66
  # Start on specific port
67
67
  claude-plan-viewer --port 8080
68
+ claude-plan-viewer -p 8080
68
69
  ```
69
70
 
70
71
  The server will automatically find an available port if the requested port is in use.
71
72
 
73
+ ### CLI Options
74
+
75
+ | Flag | Short | Description |
76
+ | --------------------- | ----- | --------------------------------------------------------- |
77
+ | `--port <number>` | `-p` | Port to start the server on (default: 3000) |
78
+ | `--claude-dir <path>` | `-c` | Path to `.claude` directory (default: `~/.claude`) |
79
+ | `--json` | `-j` | Export all plans as JSON and exit |
80
+ | `--output <file>` | `-o` | Output file for JSON export (prints to stdout if omitted) |
81
+ | `--from-file <file>` | `-f` | Load plans from a JSON file instead of `~/.claude/plans` |
82
+ | `--version` | `-v` | Show version number |
83
+ | `--help` | `-h` | Show help message |
84
+
85
+ The `--claude-dir` option can also be set via the `CLAUDE_DIR` environment variable. CLI flag takes precedence over the environment variable.
86
+
87
+ ### Export plans to JSON
88
+
89
+ ```bash
90
+ # Print all plans as JSON to stdout
91
+ claude-plan-viewer --json
92
+
93
+ # Export to a file
94
+ claude-plan-viewer --json --output plans.json
95
+ claude-plan-viewer -j -o plans.json
96
+ ```
97
+
98
+ The JSON export includes all plan metadata and content, useful for backup or processing.
99
+
100
+ ### Load plans from file
101
+
102
+ ```bash
103
+ # Start viewer with plans from an exported JSON file
104
+ claude-plan-viewer --from-file plans.json
105
+ claude-plan-viewer -f plans.json
106
+ ```
107
+
108
+ This allows viewing plans offline or from a different machine. When using `--from-file`, file watching is disabled since the plans are loaded from the static JSON file.
109
+
110
+ ### Custom .claude directory
111
+
112
+ ```bash
113
+ # Use a custom .claude directory
114
+ claude-plan-viewer --claude-dir /path/to/.claude
115
+ claude-plan-viewer -c /path/to/.claude
116
+
117
+ # Or set via environment variable
118
+ CLAUDE_DIR=/path/to/.claude claude-plan-viewer
119
+ ```
120
+
121
+ This is useful when your Claude Code data is stored in a non-standard location.
122
+
123
+ ## API
124
+
125
+ The viewer exposes a REST API for programmatic access:
126
+
127
+ | Endpoint | Method | Description |
128
+ | ------------------------------- | ------ | -------------------------- |
129
+ | `/api/plans` | GET | List all plans (metadata) |
130
+ | `/api/plans/{filename}/content` | GET | Get plan markdown content |
131
+ | `/api/projects` | GET | List all project names |
132
+ | `/api/refresh` | POST | Force cache refresh |
133
+ | `/api/open` | POST | Open plan in system editor |
134
+ | `/api/openapi.json` | GET | OpenAPI 3.0 specification |
135
+
72
136
  ## Development
73
137
 
74
138
  ```bash
@@ -77,6 +141,22 @@ bun install
77
141
 
78
142
  # Run in development mode with hot reload
79
143
  bun run dev
144
+
145
+ # Or run directly with hot reload
146
+ bun --hot index.ts
147
+
148
+ # To use flags in dev mode, add them after `--`
149
+ # Example: using --from-file
150
+ bun run index.ts -- --from-file plans.json
151
+ ```
152
+
153
+ ## Running tests
154
+
155
+ ```bash
156
+ bun run test
157
+
158
+ # Note: Use `bun run test` instead of `bun test` directly,
159
+ # as the script specifies the test directory path
80
160
  ```
81
161
 
82
162
  ## Building
@@ -85,18 +165,18 @@ Build standalone binaries for different platforms:
85
165
 
86
166
  ```bash
87
167
  bun run build # Current platform
168
+ bun run build:all # All platforms
88
169
  bun run build:macos-arm64 # macOS Apple Silicon
89
170
  bun run build:macos-x64 # macOS Intel
90
171
  bun run build:linux-x64 # Linux x64
91
172
  bun run build:linux-arm64 # Linux ARM64
92
173
  bun run build:windows # Windows x64
93
- bun run build:all # All platforms
94
174
  ```
95
175
 
96
176
  ## Requirements
97
177
 
98
178
  - [Bun](https://bun.sh) runtime (for development/npx usage)
99
- - Claude Code with plan files in `~/.claude/plans`
179
+ - Claude Code with plan files in `~/.claude/plans` (or custom location via `--claude-dir`)
100
180
 
101
181
  Standalone binaries have no external dependencies.
102
182
 
package/index.ts CHANGED
@@ -2,16 +2,32 @@
2
2
  import { readdir, stat, watch } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
- import index from "./index.html";
6
- import prismBundlePath from "./prism.bundle.js" with { type: "file" };
5
+ import index from "./src/index.html";
6
+ import apiDocs from "./src/api-docs.html";
7
+ import pkg from "./package.json";
8
+ import openapi from "./openapi.json";
7
9
 
8
- const PLANS_DIR = join(homedir(), ".claude", "plans");
9
- const PROJECTS_DIR = join(homedir(), ".claude", "projects");
10
+ // Resolved at startup based on --claude-dir flag or CLAUDE_DIR env var
11
+ let PLANS_DIR: string;
12
+ let PROJECTS_DIR: string;
13
+
14
+ function resolveClaudeDir(cliArg?: string): string {
15
+ return cliArg || process.env.CLAUDE_DIR || join(homedir(), ".claude");
16
+ }
17
+
18
+ function initializeDirectories(claudeDir: string): void {
19
+ PLANS_DIR = join(claudeDir, "plans");
20
+ PROJECTS_DIR = join(claudeDir, "projects");
21
+ }
10
22
 
11
23
  interface CliArgs {
12
24
  port?: number;
13
25
  json?: boolean;
14
26
  output?: string;
27
+ fromFile?: string;
28
+ claudeDir?: string;
29
+ version?: boolean;
30
+ help?: boolean;
15
31
  }
16
32
 
17
33
  function parseCliArgs(): CliArgs {
@@ -34,12 +50,51 @@ function parseCliArgs(): CliArgs {
34
50
  args.output = nextArg;
35
51
  i++;
36
52
  }
53
+ } else if (arg === "--from-file" || arg === "-f") {
54
+ if (nextArg && !nextArg.startsWith("-")) {
55
+ args.fromFile = nextArg;
56
+ i++;
57
+ }
58
+ } else if (arg === "--claude-dir" || arg === "-c") {
59
+ if (nextArg && !nextArg.startsWith("-")) {
60
+ args.claudeDir = nextArg;
61
+ i++;
62
+ }
63
+ } else if (arg === "--version" || arg === "-v") {
64
+ args.version = true;
65
+ } else if (arg === "--help" || arg === "-h") {
66
+ args.help = true;
37
67
  }
38
68
  }
39
69
 
40
70
  return args;
41
71
  }
42
72
 
73
+ function printHelp(): void {
74
+ console.log(`
75
+ claude-plan-viewer - Browse and search Claude Code plans
76
+
77
+ Usage: claude-plan-viewer [options]
78
+
79
+ Options:
80
+ -p, --port <number> Port to start server on (default: 3000)
81
+ -c, --claude-dir <path> Path to .claude directory (default: ~/.claude)
82
+ Can also be set via CLAUDE_DIR environment variable
83
+ -j, --json Export all plans as JSON and exit
84
+ -o, --output <file> Output file for JSON export (stdout if omitted)
85
+ -f, --from-file <file> Load plans from JSON file instead of ~/.claude/plans
86
+ -v, --version Show version number
87
+ -h, --help Show this help message
88
+
89
+ Examples:
90
+ claude-plan-viewer Start viewer on default port
91
+ claude-plan-viewer -p 8080 Start on port 8080
92
+ claude-plan-viewer -c /path/to/.claude Use custom .claude directory
93
+ claude-plan-viewer -j -o plans.json Export plans to file
94
+ claude-plan-viewer -f plans.json Load plans from exported file
95
+ `);
96
+ }
97
+
43
98
  async function exportPlansAsJson(outputPath?: string): Promise<void> {
44
99
  const plans = await loadPlans();
45
100
 
@@ -58,6 +113,38 @@ async function exportPlansAsJson(outputPath?: string): Promise<void> {
58
113
  }
59
114
  }
60
115
 
116
+ async function loadPlansFromFile(filepath: string): Promise<PlanMetadata[]> {
117
+ const file = Bun.file(filepath);
118
+ const exists = await file.exists();
119
+
120
+ if (!exists) {
121
+ console.error(`File not found: ${filepath}`);
122
+ process.exit(1);
123
+ }
124
+
125
+ const data = await file.json();
126
+ const plans: PlanMetadata[] = [];
127
+
128
+ for (const plan of data) {
129
+ contentCache.set(plan.filename, plan.content || "");
130
+ plans.push({
131
+ filename: plan.filename,
132
+ filepath: plan.filepath,
133
+ title: plan.title,
134
+ size: plan.size,
135
+ modified: plan.modified,
136
+ created: plan.created,
137
+ lineCount: plan.lineCount,
138
+ wordCount: plan.wordCount,
139
+ project: plan.project,
140
+ sessionId: plan.sessionId,
141
+ });
142
+ }
143
+
144
+ cachedPlans = plans;
145
+ return plans;
146
+ }
147
+
61
148
  // Find an available port starting from the requested port
62
149
  async function findAvailablePort(startPort: number = 3000): Promise<number> {
63
150
  let port = startPort;
@@ -75,7 +162,9 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
75
162
  port++;
76
163
  }
77
164
  }
78
- throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts}`);
165
+ throw new Error(
166
+ `No available port found in range ${startPort}-${startPort + maxAttempts}`,
167
+ );
79
168
  }
80
169
 
81
170
  // Cross-platform open file in default editor
@@ -115,7 +204,9 @@ function extractProjectName(cwd: string): string {
115
204
  // Normalize: handle both / and \ separators
116
205
  const normalized = cwd.replace(/\\/g, "/");
117
206
  // Remove trailing slash
118
- const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
207
+ const trimmed = normalized.endsWith("/")
208
+ ? normalized.slice(0, -1)
209
+ : normalized;
119
210
  // Get last segment
120
211
  const lastSlash = trimmed.lastIndexOf("/");
121
212
  return lastSlash === -1 ? trimmed : trimmed.slice(lastSlash + 1);
@@ -181,48 +272,67 @@ async function buildProjectMapping(): Promise<ProjectMapping> {
181
272
  try {
182
273
  const projectDirs = await readdir(PROJECTS_DIR);
183
274
 
184
- for (const dir of projectDirs) {
185
- const dirPath = join(PROJECTS_DIR, dir);
186
- const dirStats = await stat(dirPath);
187
- if (!dirStats.isDirectory()) continue;
188
-
189
- // Find JSONL files and extract cwd + slugs + sessionIds
190
- const files = await readdir(dirPath);
191
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
192
-
193
- let projectName: string | null = null;
194
- const slugSessionMap = new Map<string, string>();
195
-
196
- for (const file of jsonlFiles) {
275
+ // Process all project directories in parallel
276
+ const results = await Promise.all(
277
+ projectDirs.map(async (dir) => {
278
+ const dirPath = join(PROJECTS_DIR, dir);
197
279
  try {
198
- const content = await Bun.file(join(dirPath, file)).text();
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
+ );
298
+
299
+ let projectName: string | null = null;
300
+ const slugSessionMap = new Map<string, string>();
301
+
302
+ // Process file contents
303
+ for (const content of fileContents) {
304
+ if (!content) continue;
305
+
306
+ // Get project name from cwd (only need to find it once)
307
+ if (!projectName) {
308
+ const cwd = extractCwdFromJsonl(content);
309
+ if (cwd) {
310
+ projectName = extractProjectName(cwd);
311
+ }
312
+ }
199
313
 
200
- // Get project name from cwd (only need to find it once)
201
- if (!projectName) {
202
- const cwd = extractCwdFromJsonl(content);
203
- if (cwd) {
204
- projectName = extractProjectName(cwd);
314
+ // Collect slug -> sessionId mappings
315
+ const fileSlugSessions = extractSlugSessionMap(content);
316
+ for (const [slug, sessionId] of fileSlugSessions) {
317
+ slugSessionMap.set(slug, sessionId);
205
318
  }
206
319
  }
207
320
 
208
- // Collect slug -> sessionId mappings
209
- const fileSlugSessions = extractSlugSessionMap(content);
210
- for (const [slug, sessionId] of fileSlugSessions) {
211
- slugSessionMap.set(slug, sessionId);
212
- }
321
+ return { projectName, slugSessionMap };
213
322
  } catch {
214
- // Skip files that can't be read
215
- }
216
- }
217
-
218
- // Map all slugs to this project with their session IDs
219
- if (projectName) {
220
- for (const [slug, sessionId] of slugSessionMap) {
221
- mapping[slug] = {
222
- project: projectName,
223
- sessionId: sessionId,
224
- };
323
+ return null;
225
324
  }
325
+ })
326
+ );
327
+
328
+ // Merge results into mapping
329
+ for (const result of results) {
330
+ if (!result?.projectName) continue;
331
+ for (const [slug, sessionId] of result.slugSessionMap) {
332
+ mapping[slug] = {
333
+ project: result.projectName,
334
+ sessionId: sessionId,
335
+ };
226
336
  }
227
337
  }
228
338
  } catch {
@@ -254,10 +364,7 @@ async function loadPlans(): Promise<PlanMetadata[]> {
254
364
  const filepath = join(PLANS_DIR, filename);
255
365
  const file = Bun.file(filepath);
256
366
 
257
- const [content, stats] = await Promise.all([
258
- file.text(),
259
- stat(filepath),
260
- ]);
367
+ const [content, stats] = await Promise.all([file.text(), stat(filepath)]);
261
368
 
262
369
  const titleMatch = content.match(/^#\s+(.+)$/m);
263
370
  const title = titleMatch?.[1]
@@ -285,17 +392,34 @@ async function loadPlans(): Promise<PlanMetadata[]> {
285
392
  project: metadata?.project || null,
286
393
  sessionId: metadata?.sessionId || null,
287
394
  };
288
- })
395
+ }),
289
396
  );
290
397
 
291
398
  cachedPlans = plans;
292
399
  return plans;
293
400
  }
294
401
 
295
- function invalidateCache() {
402
+ // Granular cache invalidation
403
+ function invalidatePlansCache() {
296
404
  cachedPlans = null;
405
+ }
406
+
407
+ function invalidateProjectMapping() {
297
408
  cachedProjectMapping = null;
298
- contentCache.clear();
409
+ }
410
+
411
+ function invalidateContentCache(filename?: string) {
412
+ if (filename) {
413
+ contentCache.delete(filename);
414
+ } else {
415
+ contentCache.clear();
416
+ }
417
+ }
418
+
419
+ function invalidateAllCaches() {
420
+ invalidatePlansCache();
421
+ invalidateProjectMapping();
422
+ invalidateContentCache();
299
423
  }
300
424
 
301
425
  // Watch plans directory for changes and invalidate cache
@@ -304,7 +428,10 @@ async function watchPlansDirectory() {
304
428
  const watcher = watch(PLANS_DIR);
305
429
  for await (const event of watcher) {
306
430
  if (event.filename?.endsWith(".md")) {
307
- invalidateCache();
431
+ // Only invalidate plans metadata and the specific file's content
432
+ // Project mapping rarely changes, keep it cached
433
+ invalidatePlansCache();
434
+ invalidateContentCache(event.filename);
308
435
  }
309
436
  }
310
437
  } catch {
@@ -321,6 +448,9 @@ async function startServer() {
321
448
  port,
322
449
  routes: {
323
450
  "/": index,
451
+ "/api": () => Response.redirect("/api/", 301),
452
+ "/api/": apiDocs,
453
+ "/api/openapi.json": () => Response.json(openapi),
324
454
  "/api/projects": async () => {
325
455
  // Lazy load cache on first request
326
456
  if (!cachedPlans) {
@@ -328,7 +458,9 @@ async function startServer() {
328
458
  }
329
459
 
330
460
  const plans = cachedPlans || [];
331
- const projects = [...new Set(plans.map(p => p.project).filter(Boolean))] as string[];
461
+ const projects = [
462
+ ...new Set(plans.map((p) => p.project).filter(Boolean)),
463
+ ] as string[];
332
464
  projects.sort((a, b) => a.localeCompare(b));
333
465
 
334
466
  return Response.json({ projects });
@@ -342,7 +474,7 @@ async function startServer() {
342
474
  const plans = cachedPlans || [];
343
475
 
344
476
  // Strip content from response - will be fetched separately via /api/plans/{id}/content
345
- const plansWithoutContent = plans.map(p => ({
477
+ const plansWithoutContent = plans.map((p) => ({
346
478
  filename: p.filename,
347
479
  filepath: p.filepath,
348
480
  title: p.title,
@@ -376,9 +508,11 @@ async function startServer() {
376
508
  },
377
509
  "/api/refresh": {
378
510
  POST: async () => {
379
- invalidateCache();
511
+ const before = cachedPlans?.length ?? 0;
512
+ invalidateAllCaches();
380
513
  await loadPlans();
381
- return Response.json({ success: true });
514
+ const after = cachedPlans?.length ?? 0;
515
+ return Response.json({ success: true, before, after });
382
516
  },
383
517
  },
384
518
  "/api/open": {
@@ -396,10 +530,13 @@ async function startServer() {
396
530
  },
397
531
  },
398
532
  },
399
- development: process.env.NODE_ENV !== "production" ? {
400
- hmr: true,
401
- console: true,
402
- } : undefined,
533
+ development:
534
+ process.env.NODE_ENV !== "production"
535
+ ? {
536
+ hmr: true,
537
+ console: true,
538
+ }
539
+ : undefined,
403
540
  });
404
541
 
405
542
  return server;
@@ -420,24 +557,60 @@ const c = {
420
557
  (async () => {
421
558
  const args = parseCliArgs();
422
559
 
560
+ if (args.version) {
561
+ console.log(`claude-plan-viewer v${pkg.version}`);
562
+ process.exit(0);
563
+ }
564
+
565
+ if (args.help) {
566
+ printHelp();
567
+ process.exit(0);
568
+ }
569
+
570
+ // Initialize directory paths based on --claude-dir flag or CLAUDE_DIR env var
571
+ const claudeDir = resolveClaudeDir(args.claudeDir);
572
+ initializeDirectories(claudeDir);
573
+
423
574
  if (args.json) {
424
575
  await exportPlansAsJson(args.output);
425
576
  process.exit(0);
426
577
  }
427
578
 
428
- const server = await startServer();
429
- const planCount = (await readdir(PLANS_DIR)).filter(f => f.endsWith('.md')).length;
579
+ // Pre-load plans from file if --from-file is provided
580
+ let planCount: number;
581
+ let sourceDisplay: string;
430
582
 
431
- // Start watching for file changes (runs in background)
432
- watchPlansDirectory();
583
+ if (args.fromFile) {
584
+ const plans = await loadPlansFromFile(args.fromFile);
585
+ planCount = plans.length;
586
+ sourceDisplay = args.fromFile;
587
+ } else {
588
+ planCount = (await readdir(PLANS_DIR)).filter((f) =>
589
+ f.endsWith(".md"),
590
+ ).length;
591
+ sourceDisplay = PLANS_DIR;
592
+ // Only watch for file changes when not using --from-file
593
+ watchPlansDirectory();
594
+ }
595
+
596
+ const server = await startServer();
433
597
 
434
598
  console.log();
435
- console.log(`${c.bold}${c.magenta} 📋 Plans Viewer${c.reset}`);
599
+ console.log(`${c.bold}${c.magenta} 📋 Claude Plan Viewer${c.reset}`);
436
600
  console.log(`${c.dim} ─────────────────────────────${c.reset}`);
437
601
  console.log(`${c.green} ✓${c.reset} Server running`);
438
- console.log(`${c.green} ✓${c.reset} Watching for file changes`);
602
+ if (!args.fromFile) {
603
+ console.log(`${c.green} ✓${c.reset} Watching for file changes`);
604
+ }
439
605
  console.log();
440
- console.log(`${c.dim} Local:${c.reset} ${c.cyan}${c.bold}http://localhost:${server.port}${c.reset}`);
441
- console.log(`${c.dim} Plans:${c.reset} ${c.yellow}${planCount} plans${c.reset} in ${c.dim}${PLANS_DIR}${c.reset}`);
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
+ );
442
615
  console.log();
443
616
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plan-viewer",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A web-based viewer for Claude Code plan files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,23 +8,27 @@
8
8
  },
9
9
  "files": [
10
10
  "index.ts",
11
- "index.html",
12
- "src",
13
- "styles.css",
14
- "prism.bundle.js"
11
+ "src"
15
12
  ],
16
13
  "scripts": {
17
14
  "start": "bun index.ts",
18
15
  "dev": "bun --hot index.ts",
19
- "test": "bun test index.test.ts frontend.test.ts",
16
+ "test": "bun test test/",
17
+ "test:api": "bun test test/api.test.ts",
20
18
  "test:e2e": "bunx playwright test",
21
- "build": "bun build --compile --minify --bytecode ./index.ts --outfile ./dist/plans-viewer",
22
- "build:macos-arm64": "bun build --compile --target=bun-darwin-arm64 --minify --bytecode ./index.ts --outfile ./dist/plans-viewer-macos-arm64",
23
- "build:macos-x64": "bun build --compile --target=bun-darwin-x64 --minify --bytecode ./index.ts --outfile ./dist/plans-viewer-macos-x64",
24
- "build:linux-x64": "bun build --compile --target=bun-linux-x64 --minify --bytecode ./index.ts --outfile ./dist/plans-viewer-linux-x64",
25
- "build:linux-arm64": "bun build --compile --target=bun-linux-arm64 --minify --bytecode ./index.ts --outfile ./dist/plans-viewer-linux-arm64",
26
- "build:windows": "bun build --compile --target=bun-windows-x64 --minify --bytecode ./index.ts --outfile ./dist/plans-viewer-windows.exe",
27
- "build:all": "bun run build:macos-arm64 && bun run build:macos-x64 && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:windows"
19
+ "build": "bun build --compile --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer",
20
+ "build:macos-arm64": "bun build --compile --target=bun-darwin-arm64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-macos-arm64",
21
+ "build:macos-x64": "bun build --compile --target=bun-darwin-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-macos-x64",
22
+ "build:linux-x64": "bun build --compile --target=bun-linux-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-linux-x64",
23
+ "build:linux-arm64": "bun build --compile --target=bun-linux-arm64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-linux-arm64",
24
+ "build:windows": "bun build --compile --target=bun-windows-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-windows.exe",
25
+ "build:all": "bun run build:macos-arm64 && bun run build:macos-x64 && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:windows",
26
+ "clean": "rm -rf ./dist",
27
+ "install:link": "bun link",
28
+ "install:local": "bun scripts/install-local.ts",
29
+ "uninstall:link": "bun unlink",
30
+ "uninstall:local": "bun scripts/uninstall-local.ts",
31
+ "format": "bunx prettier --write src"
28
32
  },
29
33
  "keywords": [
30
34
  "claude",
@@ -49,8 +53,10 @@
49
53
  "devDependencies": {
50
54
  "@playwright/test": "^1.57.0",
51
55
  "@types/bun": "latest",
56
+ "@types/prismjs": "^1.26.5",
52
57
  "@types/react": "^19.2.7",
53
- "@types/react-dom": "^19.2.3"
58
+ "@types/react-dom": "^19.2.3",
59
+ "@types/react-syntax-highlighter": "^15.5.13"
54
60
  },
55
61
  "peerDependencies": {
56
62
  "typescript": "^5.9.3"
@@ -58,6 +64,10 @@
58
64
  "dependencies": {
59
65
  "react": "^19.2.3",
60
66
  "react-dom": "^19.2.3",
61
- "react-select": "^5.10.2"
67
+ "react-markdown": "^10.1.0",
68
+ "react-select": "^5.10.2",
69
+ "react-syntax-highlighter": "^16.1.0",
70
+ "remark-gfm": "^4.0.1",
71
+ "swr": "^2.3.8"
62
72
  }
63
73
  }
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Plans Viewer API</title>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
11
+ <script>
12
+ Scalar.createApiReference("#app", {
13
+ url: "/api/openapi.json",
14
+ });
15
+ </script>
16
+ </body>
17
+ </html>