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 +84 -4
- package/index.ts +238 -65
- package/package.json +25 -15
- package/src/api-docs.html +17 -0
- package/src/client/App.tsx +54 -9
- package/src/client/components/DetailOverlay.tsx +66 -9
- package/src/client/components/DetailPanel.tsx +63 -11
- package/src/client/components/Header.tsx +24 -5
- package/src/client/components/HelpModal.tsx +30 -7
- package/src/client/components/Markdown.tsx +37 -7
- package/src/client/components/PlanRow.tsx +15 -4
- package/src/client/components/ProjectFilter.tsx +6 -11
- package/src/client/components/SearchInput.tsx +7 -1
- package/src/client/components/index.ts +0 -1
- package/src/client/hooks/useFilters.ts +7 -7
- package/src/client/hooks/useFocusTrap.ts +70 -0
- package/src/client/hooks/useKeyboard.ts +2 -2
- package/src/client/hooks/usePlans.ts +64 -73
- package/src/client/hooks/useProjects.ts +24 -11
- package/src/client/index.tsx +1 -1
- package/src/client/types.ts +7 -1
- package/src/client/utils/api.ts +13 -4
- package/src/client/utils/formatters.ts +38 -9
- package/src/client/utils/index.ts +0 -12
- package/src/index.html +13 -0
- package/{styles.css → src/styles/styles.css} +154 -72
- package/index.html +0 -14
- package/prism.bundle.js +0 -35
- package/src/client/utils/markdown.ts +0 -123
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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(
|
|
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("/")
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
+
// Granular cache invalidation
|
|
403
|
+
function invalidatePlansCache() {
|
|
296
404
|
cachedPlans = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function invalidateProjectMapping() {
|
|
297
408
|
cachedProjectMapping = null;
|
|
298
|
-
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
511
|
+
const before = cachedPlans?.length ?? 0;
|
|
512
|
+
invalidateAllCaches();
|
|
380
513
|
await loadPlans();
|
|
381
|
-
|
|
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:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
429
|
-
|
|
579
|
+
// Pre-load plans from file if --from-file is provided
|
|
580
|
+
let planCount: number;
|
|
581
|
+
let sourceDisplay: string;
|
|
430
582
|
|
|
431
|
-
|
|
432
|
-
|
|
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} 📋
|
|
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
|
-
|
|
602
|
+
if (!args.fromFile) {
|
|
603
|
+
console.log(`${c.green} ✓${c.reset} Watching for file changes`);
|
|
604
|
+
}
|
|
439
605
|
console.log();
|
|
440
|
-
console.log(
|
|
441
|
-
|
|
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
|
+
"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
|
-
"
|
|
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
|
|
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/
|
|
22
|
-
"build:macos-arm64": "bun build --compile --target=bun-darwin-arm64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
23
|
-
"build:macos-x64": "bun build --compile --target=bun-darwin-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
24
|
-
"build:linux-x64": "bun build --compile --target=bun-linux-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
25
|
-
"build:linux-arm64": "bun build --compile --target=bun-linux-arm64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
26
|
-
"build:windows": "bun build --compile --target=bun-windows-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
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-
|
|
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>
|