claudedash 0.5.4 → 0.6.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 +1 -1
- package/README.md +43 -3
- package/dist/cli.js +497 -7
- package/dist/cli.js.map +1 -1
- package/dist/core/contextHealth.d.ts +8 -2
- package/dist/core/contextHealth.d.ts.map +1 -1
- package/dist/core/contextHealth.js +27 -5
- package/dist/core/contextHealth.js.map +1 -1
- package/dist/public/404/index.html +1 -1
- package/dist/public/404.html +1 -1
- package/dist/public/__next.__PAGE__.txt +2 -2
- package/dist/public/__next._full.txt +4 -4
- package/dist/public/__next._head.txt +1 -1
- package/dist/public/__next._index.txt +2 -2
- package/dist/public/__next._tree.txt +2 -2
- package/dist/public/_next/static/chunks/db0169666466ef04.js +69 -0
- package/dist/public/_next/static/chunks/fa2cff080f66a65c.css +2 -0
- package/dist/public/_not-found/__next._full.txt +3 -3
- package/dist/public/_not-found/__next._head.txt +1 -1
- package/dist/public/_not-found/__next._index.txt +2 -2
- package/dist/public/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/public/_not-found/__next._not-found.txt +1 -1
- package/dist/public/_not-found/__next._tree.txt +2 -2
- package/dist/public/_not-found/index.html +1 -1
- package/dist/public/_not-found/index.txt +3 -3
- package/dist/public/index.html +1 -1
- package/dist/public/index.txt +4 -4
- package/dist/server/routes/live.d.ts.map +1 -1
- package/dist/server/routes/live.js +22 -4
- package/dist/server/routes/live.js.map +1 -1
- package/dist/server/routes/observability.d.ts.map +1 -1
- package/dist/server/routes/observability.js +18 -0
- package/dist/server/routes/observability.js.map +1 -1
- package/dist/server/routes/plan.d.ts +2 -0
- package/dist/server/routes/plan.d.ts.map +1 -1
- package/dist/server/routes/plan.js +66 -3
- package/dist/server/routes/plan.js.map +1 -1
- package/dist/server/server.d.ts +2 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +48 -5
- package/dist/server/server.js.map +1 -1
- package/dist/server/watcher.d.ts +1 -1
- package/dist/server/watcher.js +3 -3
- package/dist/server/watcher.js.map +1 -1
- package/package.json +14 -6
- package/dist/public/_next/static/chunks/02c545b35c5a7a0d.js +0 -2
- package/dist/public/_next/static/chunks/bfa1988f6340e536.css +0 -2
- /package/dist/public/_next/static/{36m05Da62vzqVee_ifGeq → 1zNqkaO9LSa-UBCfTki_G}/_buildManifest.js +0 -0
- /package/dist/public/_next/static/{36m05Da62vzqVee_ifGeq → 1zNqkaO9LSa-UBCfTki_G}/_clientMiddlewareManifest.json +0 -0
- /package/dist/public/_next/static/{36m05Da62vzqVee_ifGeq → 1zNqkaO9LSa-UBCfTki_G}/_ssgManifest.js +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -93,9 +93,46 @@ Then:
|
|
|
93
93
|
claudedash start # Auto-detect modes, open dashboard
|
|
94
94
|
claudedash start --claude-dir /path # Custom Claude directory
|
|
95
95
|
claudedash start -p 3000 # Custom port
|
|
96
|
+
claudedash start --host 0.0.0.0 # Expose to network (shows warning)
|
|
97
|
+
claudedash start --token <secret> # Enable token auth for sharing
|
|
96
98
|
claudedash init # Init plan mode in current dir
|
|
99
|
+
claudedash recover # Summarize last session after /clear
|
|
100
|
+
claudedash spec # Create spec-mode templates
|
|
101
|
+
claudedash worktree create <branch> # Create isolated worktree
|
|
97
102
|
```
|
|
98
103
|
|
|
104
|
+
## Sharing with your team
|
|
105
|
+
|
|
106
|
+
By default, claudedash only listens on `127.0.0.1` (localhost). To share the dashboard with a teammate:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 1. Start with a secret token
|
|
110
|
+
claudedash start --token mysecret123
|
|
111
|
+
|
|
112
|
+
# Or use an environment variable
|
|
113
|
+
CLAUDEDASH_TOKEN=mysecret123 claudedash start
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Your teammate can then open the dashboard with the token in the URL:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
http://your-host:4317?token=mysecret123
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or pass it as a header:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
curl -H "Authorization: Bearer mysecret123" http://your-host:4317/sessions
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **Security:** Never commit your token to git. Use `.env` and add it to `.gitignore`. Consider using a local tunnel like [ngrok](https://ngrok.com) or [cloudflared](https://github.com/cloudflare/cloudflared) rather than exposing `--host 0.0.0.0` directly.
|
|
129
|
+
>
|
|
130
|
+
> ```bash
|
|
131
|
+
> # Share via tunnel without network exposure
|
|
132
|
+
> claudedash start --token $(openssl rand -hex 16)
|
|
133
|
+
> ngrok http 4317
|
|
134
|
+
> ```
|
|
135
|
+
|
|
99
136
|
## Queue format (Plan mode)
|
|
100
137
|
|
|
101
138
|
```markdown
|
|
@@ -174,13 +211,16 @@ When running agents across multiple git worktrees in parallel, the new **Worktre
|
|
|
174
211
|
|
|
175
212
|
| Endpoint | Description |
|
|
176
213
|
|---|---|
|
|
177
|
-
| `GET /health` | Status + available modes |
|
|
214
|
+
| `GET /health` | Status + available modes + `connectedClients` + `lastSessions` |
|
|
178
215
|
| `GET /sessions` | All Claude Code sessions (includes `contextHealth`) |
|
|
179
216
|
| `GET /sessions/:id` | Tasks for a session |
|
|
180
217
|
| `GET /events` | SSE stream |
|
|
181
218
|
| `GET /snapshot` | Plan mode state |
|
|
182
|
-
| `GET /quality-timeline` | Quality check events (filter with `?taskId=`) |
|
|
219
|
+
| `GET /quality-timeline` | Quality check events (filter with `?taskId=` or `?file=`) |
|
|
183
220
|
| `GET /worktrees` | Git worktrees with task associations |
|
|
221
|
+
| `GET /claude-insights` | Claude usage report (sandboxed HTML) |
|
|
222
|
+
| `POST /plan/task` | Add a task to queue.md |
|
|
223
|
+
| `PATCH /plan/task/:id` | Update task status (DONE / BLOCKED / FAILED) |
|
|
184
224
|
|
|
185
225
|
## Development
|
|
186
226
|
|
|
@@ -190,7 +230,7 @@ cd claudedash && npm install
|
|
|
190
230
|
cd dashboard && npm install && cd ..
|
|
191
231
|
|
|
192
232
|
npm run build # Build core + dashboard
|
|
193
|
-
npm test #
|
|
233
|
+
npm test # 165 tests
|
|
194
234
|
npm run dev # Dev server with watch
|
|
195
235
|
```
|
|
196
236
|
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, statSync, watch } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
6
|
import { startServer } from './server/server.js';
|
|
7
7
|
const program = new Command();
|
|
8
8
|
program
|
|
9
9
|
.name('claudedash')
|
|
10
10
|
.description('Live Kanban, quality gates and context health monitoring for Claude Code agents')
|
|
11
|
-
.version('0.
|
|
11
|
+
.version('0.6.0');
|
|
12
12
|
program
|
|
13
13
|
.command('init')
|
|
14
14
|
.description('Initialize claudedash in current directory')
|
|
@@ -172,9 +172,180 @@ Run \`npx -y claudedash@latest start\` to view progress.
|
|
|
172
172
|
`;
|
|
173
173
|
writeFileSync(join(claudeWatchDir, 'CLAUDE.md'), claudeMdContent);
|
|
174
174
|
console.log('✓ Created CLAUDE.md');
|
|
175
|
+
// Create hooks/ directory with lifecycle templates
|
|
176
|
+
mkdirSync(join(claudeWatchDir, 'hooks'), { recursive: true });
|
|
177
|
+
const preCompactHook = `# Pre-Compact Hook
|
|
178
|
+
|
|
179
|
+
Run this before Claude Code compacts the conversation.
|
|
180
|
+
Add to your project's CLAUDE.md or register as a PreCompact hook.
|
|
181
|
+
|
|
182
|
+
## Purpose
|
|
183
|
+
Save the current plan state so it can be restored after compaction.
|
|
184
|
+
|
|
185
|
+
## Instructions for the agent
|
|
186
|
+
|
|
187
|
+
Before context compaction, write a brief state snapshot:
|
|
188
|
+
1. Current task ID from .claudedash/queue.md (the one in_progress)
|
|
189
|
+
2. How many tasks are DONE (count lines in execution.log)
|
|
190
|
+
3. Any important decisions or blockers from the last few messages
|
|
191
|
+
|
|
192
|
+
Write the snapshot to .claudedash/compact-state.md:
|
|
193
|
+
\`\`\`
|
|
194
|
+
# Compact State
|
|
195
|
+
Task: S1-T3
|
|
196
|
+
Completed: 2 (S1-T1, S1-T2)
|
|
197
|
+
Status: In progress — editing src/server/server.ts, adding CORS restriction
|
|
198
|
+
Blocker: none
|
|
199
|
+
\`\`\`
|
|
200
|
+
`;
|
|
201
|
+
const postCompactHook = `# Post-Compact Hook
|
|
202
|
+
|
|
203
|
+
Run this after Claude Code compacts the conversation.
|
|
204
|
+
Add to your project's CLAUDE.md or register as a PostCompact hook.
|
|
205
|
+
|
|
206
|
+
## Purpose
|
|
207
|
+
Re-inject plan context after compaction so the agent resumes correctly.
|
|
208
|
+
|
|
209
|
+
## Instructions for the agent
|
|
210
|
+
|
|
211
|
+
After context compaction, immediately:
|
|
212
|
+
1. Read .claudedash/compact-state.md (if it exists)
|
|
213
|
+
2. Read .claudedash/execution.log to verify completed tasks
|
|
214
|
+
3. Read .claudedash/queue.md to find the current task
|
|
215
|
+
4. Resume from exactly where the snapshot says
|
|
216
|
+
|
|
217
|
+
Then delete .claudedash/compact-state.md to avoid stale state.
|
|
218
|
+
`;
|
|
219
|
+
const stopHook = `# Stop Hook
|
|
220
|
+
|
|
221
|
+
Prevents the agent from stopping mid-task.
|
|
222
|
+
Register as a Stop hook in Claude Code settings.
|
|
223
|
+
|
|
224
|
+
## Purpose
|
|
225
|
+
If there are pending tasks in the queue, remind the agent to continue.
|
|
226
|
+
|
|
227
|
+
## Logic (loop_limit: 3)
|
|
228
|
+
|
|
229
|
+
\`\`\`json
|
|
230
|
+
{
|
|
231
|
+
"hook": "Stop",
|
|
232
|
+
"loop_limit": 3,
|
|
233
|
+
"condition": "pending tasks remain in .claudedash/queue.md not in execution.log",
|
|
234
|
+
"followup_message": "There are still pending tasks in .claudedash/queue.md. Check .claudedash/workflow.md and continue with the next READY task. Do not stop until all tasks are DONE or BLOCKED."
|
|
235
|
+
}
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
## How to Install
|
|
239
|
+
|
|
240
|
+
Add to your Claude Code hooks configuration:
|
|
241
|
+
1. Open Claude Code settings
|
|
242
|
+
2. Navigate to Hooks → Stop
|
|
243
|
+
3. Paste the JSON block above
|
|
244
|
+
4. Set loop_limit to 3 to prevent infinite loops
|
|
245
|
+
`;
|
|
246
|
+
const postToolUseHook = `# PostToolUse Hook — Quality Gate
|
|
247
|
+
|
|
248
|
+
Runs automatically after Bash, Edit, or Write tool calls.
|
|
249
|
+
Register in Claude Code settings under Hooks → PostToolUse.
|
|
250
|
+
|
|
251
|
+
## Purpose
|
|
252
|
+
Run lint and typecheck after every code change and record results
|
|
253
|
+
in execution.log so the Quality Gates dashboard panel can display them.
|
|
254
|
+
|
|
255
|
+
## Hook Configuration
|
|
256
|
+
|
|
257
|
+
\`\`\`json
|
|
258
|
+
{
|
|
259
|
+
"hook": "PostToolUse",
|
|
260
|
+
"matcher": { "tool_name": ["Bash", "Edit", "Write"] },
|
|
261
|
+
"command": "npm run lint --silent 2>/dev/null && npx tsc --noEmit 2>/dev/null",
|
|
262
|
+
"on_success": {
|
|
263
|
+
"append_to": ".claudedash/execution.log",
|
|
264
|
+
"line": {"task_id":"{{task_id}}","status":"QUALITY","timestamp":"{{iso}}","agent":"claude","meta":{"quality":{"lint":true,"typecheck":true}}}
|
|
265
|
+
},
|
|
266
|
+
"on_failure": {
|
|
267
|
+
"append_to": ".claudedash/execution.log",
|
|
268
|
+
"line": {"task_id":"{{task_id}}","status":"QUALITY","timestamp":"{{iso}}","agent":"claude","meta":{"quality":{"lint":false,"typecheck":false}}}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
## Manual Usage
|
|
274
|
+
|
|
275
|
+
After any code change, run:
|
|
276
|
+
\`\`\`bash
|
|
277
|
+
npm run lint && npx tsc --noEmit
|
|
278
|
+
\`\`\`
|
|
279
|
+
Then append to execution.log:
|
|
280
|
+
\`\`\`json
|
|
281
|
+
{"task_id":"S1-T1","status":"QUALITY","timestamp":"2026-01-15T10:30:00Z","agent":"claude","meta":{"quality":{"lint":true,"typecheck":true}}}
|
|
282
|
+
\`\`\`
|
|
283
|
+
`;
|
|
284
|
+
const tddHook = `# TDD Enforcement Hook
|
|
285
|
+
|
|
286
|
+
Warns when a new source file is created without a corresponding test file.
|
|
287
|
+
|
|
288
|
+
## Naming Conventions Checked
|
|
289
|
+
|
|
290
|
+
| Source file | Expected test file |
|
|
291
|
+
|----------------------|-------------------------------------|
|
|
292
|
+
| src/foo.ts | tests/foo.test.ts or foo.spec.ts |
|
|
293
|
+
| src/core/bar.ts | tests/core/bar.test.ts |
|
|
294
|
+
| lib/baz.py | test_baz.py or baz_test.py |
|
|
295
|
+
| pkg/qux.go | qux_test.go |
|
|
296
|
+
|
|
297
|
+
## Skip List (configure in .claudedash/config.json)
|
|
298
|
+
|
|
299
|
+
\`\`\`json
|
|
300
|
+
{
|
|
301
|
+
"tddHook": {
|
|
302
|
+
"skipPatterns": ["src/cli.ts", "src/server/server.ts", "**/*.d.ts"]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
\`\`\`
|
|
306
|
+
|
|
307
|
+
## Hook Configuration
|
|
308
|
+
|
|
309
|
+
\`\`\`json
|
|
310
|
+
{
|
|
311
|
+
"hook": "PostToolUse",
|
|
312
|
+
"matcher": { "tool_name": ["Write"], "file_pattern": "src/**/*.ts" },
|
|
313
|
+
"script": ".claudedash/hooks/tdd-check.sh"
|
|
314
|
+
}
|
|
315
|
+
\`\`\`
|
|
316
|
+
|
|
317
|
+
## Behavior
|
|
318
|
+
|
|
319
|
+
- If a matching test file exists → silent pass
|
|
320
|
+
- If no test file found → prints a warning (does NOT block)
|
|
321
|
+
- Agent should create the test file before marking the task DONE
|
|
322
|
+
`;
|
|
323
|
+
writeFileSync(join(claudeWatchDir, 'hooks', 'pre-compact.md'), preCompactHook);
|
|
324
|
+
writeFileSync(join(claudeWatchDir, 'hooks', 'post-compact.md'), postCompactHook);
|
|
325
|
+
writeFileSync(join(claudeWatchDir, 'hooks', 'stop.md'), stopHook);
|
|
326
|
+
writeFileSync(join(claudeWatchDir, 'hooks', 'post-tool-use.md'), postToolUseHook);
|
|
327
|
+
writeFileSync(join(claudeWatchDir, 'hooks', 'tdd-enforcement.md'), tddHook);
|
|
328
|
+
console.log('✓ Created hooks/ (pre-compact, post-compact, stop, post-tool-use, tdd-enforcement)');
|
|
329
|
+
// Auto-inject TodoWrite directive into project CLAUDE.md
|
|
330
|
+
const projectClaudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
331
|
+
const todoWriteMarker = 'TodoWrite tool to track your work';
|
|
332
|
+
const todoWriteDirective = `\n## Task Tracking (MANDATORY)\n\nYou MUST use the TodoWrite tool to track your work. This is not optional.\nThe user monitors your progress via a live dashboard that reads TodoWrite output.\n\nRules:\n- At the START of any multi-step task, create a todo list with all steps.\n- Mark each task as \`in_progress\` BEFORE you start working on it.\n- Mark each task as \`completed\` IMMEDIATELY after finishing it.\n- Keep exactly ONE task as \`in_progress\` at any time.\n- Update the todo list as you discover new subtasks.\n\nIf you skip TodoWrite, the user cannot see what you are doing.\n`;
|
|
333
|
+
if (existsSync(projectClaudeMdPath)) {
|
|
334
|
+
const existing = readFileSync(projectClaudeMdPath, 'utf-8');
|
|
335
|
+
if (!existing.includes(todoWriteMarker)) {
|
|
336
|
+
writeFileSync(projectClaudeMdPath, existing + todoWriteDirective, 'utf-8');
|
|
337
|
+
console.log('✓ Added TodoWrite directive to CLAUDE.md');
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.log('✓ CLAUDE.md already has TodoWrite directive');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
writeFileSync(projectClaudeMdPath, `# CLAUDE.md${todoWriteDirective}`, 'utf-8');
|
|
345
|
+
console.log('✓ Created CLAUDE.md with TodoWrite directive');
|
|
346
|
+
}
|
|
175
347
|
console.log('\n✓ Ready! Next steps:');
|
|
176
348
|
console.log(' 1. Edit .claudedash/queue.md with your tasks');
|
|
177
|
-
console.log(' 2. Copy .claudedash/CLAUDE.md contents into your project CLAUDE.md');
|
|
178
349
|
console.log(' 3. Tell your agent: "follow .claudedash/workflow.md, start with S1-T1"');
|
|
179
350
|
console.log(' 4. Run: npx -y claudedash@latest start');
|
|
180
351
|
}
|
|
@@ -188,6 +359,9 @@ program
|
|
|
188
359
|
.description('Start the claudedash server and dashboard')
|
|
189
360
|
.option('--claude-dir <path>', 'Path to Claude directory', join(process.env.HOME || '~', '.claude'))
|
|
190
361
|
.option('-p, --port <number>', 'Port number', '4317')
|
|
362
|
+
.option('--host <host>', 'Bind host', '127.0.0.1')
|
|
363
|
+
.option('--no-bell', 'Disable terminal bell on task alerts')
|
|
364
|
+
.option('--token <secret>', 'Require Bearer token for all API requests (reads CLAUDEDASH_TOKEN env var if not provided)')
|
|
191
365
|
.action(async (opts) => {
|
|
192
366
|
const claudeDir = opts.claudeDir;
|
|
193
367
|
const claudeWatchDir = join(process.cwd(), '.claudedash');
|
|
@@ -215,12 +389,22 @@ program
|
|
|
215
389
|
catch { /* use default port */ }
|
|
216
390
|
}
|
|
217
391
|
}
|
|
218
|
-
const
|
|
392
|
+
const host = opts.host;
|
|
393
|
+
const isLocalhost = host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
394
|
+
const url = `http://${isLocalhost ? 'localhost' : host}:${port}`;
|
|
395
|
+
if (!isLocalhost) {
|
|
396
|
+
console.log(`⚠️ Server exposed to network on ${host}:${port}`);
|
|
397
|
+
}
|
|
398
|
+
const token = opts.token ?? process.env.CLAUDEDASH_TOKEN;
|
|
399
|
+
if (token)
|
|
400
|
+
console.log('🔒 Token authentication enabled');
|
|
219
401
|
try {
|
|
220
402
|
await startServer({
|
|
221
403
|
claudeDir,
|
|
222
404
|
port,
|
|
223
|
-
|
|
405
|
+
host,
|
|
406
|
+
agentScopeDir: hasPlan ? claudeWatchDir : undefined,
|
|
407
|
+
token,
|
|
224
408
|
});
|
|
225
409
|
console.log(`✓ Server running on ${url}`);
|
|
226
410
|
if (hasLive)
|
|
@@ -232,16 +416,322 @@ program
|
|
|
232
416
|
const openCommand = platform === 'darwin' ? 'open' :
|
|
233
417
|
platform === 'win32' ? 'start' :
|
|
234
418
|
'xdg-open';
|
|
235
|
-
|
|
419
|
+
execFile(openCommand, [url], (error) => {
|
|
236
420
|
if (error) {
|
|
237
421
|
console.log('Could not auto-open browser. Please visit:', url);
|
|
238
422
|
}
|
|
239
423
|
});
|
|
424
|
+
// Watch execution.log for FAILED/BLOCKED events and alert in terminal
|
|
425
|
+
if (hasPlan) {
|
|
426
|
+
const logPath = join(claudeWatchDir, 'execution.log');
|
|
427
|
+
const useBell = opts.bell !== false;
|
|
428
|
+
let lastSize = existsSync(logPath) ? statSync(logPath).size : 0;
|
|
429
|
+
watch(logPath, () => {
|
|
430
|
+
try {
|
|
431
|
+
if (!existsSync(logPath))
|
|
432
|
+
return;
|
|
433
|
+
const currentSize = statSync(logPath).size;
|
|
434
|
+
if (currentSize <= lastSize)
|
|
435
|
+
return;
|
|
436
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
437
|
+
const newContent = content.slice(lastSize);
|
|
438
|
+
lastSize = currentSize;
|
|
439
|
+
for (const line of newContent.split('\n').filter(Boolean)) {
|
|
440
|
+
try {
|
|
441
|
+
const event = JSON.parse(line);
|
|
442
|
+
if (event.status === 'FAILED') {
|
|
443
|
+
if (useBell)
|
|
444
|
+
process.stdout.write('\u0007');
|
|
445
|
+
console.error(`\x1b[31m⚠ TASK FAILED: ${event.task_id}\x1b[0m`);
|
|
446
|
+
}
|
|
447
|
+
else if (event.status === 'BLOCKED') {
|
|
448
|
+
if (useBell)
|
|
449
|
+
process.stdout.write('\u0007');
|
|
450
|
+
const reason = event.reason ? ` — ${String(event.reason)}` : '';
|
|
451
|
+
console.warn(`\x1b[33m⚠ TASK BLOCKED: ${event.task_id}${reason}\x1b[0m`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch { /* skip malformed lines */ }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch { /* skip watch errors */ }
|
|
458
|
+
});
|
|
459
|
+
}
|
|
240
460
|
}
|
|
241
461
|
catch (error) {
|
|
242
462
|
console.error('❌ Failed to start server:', error);
|
|
243
463
|
process.exit(1);
|
|
244
464
|
}
|
|
245
465
|
});
|
|
466
|
+
program
|
|
467
|
+
.command('spec')
|
|
468
|
+
.description('Initialize spec-mode templates (.claudedash/spec/)')
|
|
469
|
+
.action(() => {
|
|
470
|
+
const specDir = join(process.cwd(), '.claudedash', 'spec');
|
|
471
|
+
if (!existsSync(join(process.cwd(), '.claudedash'))) {
|
|
472
|
+
console.error('❌ Run "claudedash init" first.');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
if (existsSync(specDir)) {
|
|
476
|
+
console.log('⚠️ .claudedash/spec/ already exists');
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
mkdirSync(specDir, { recursive: true });
|
|
481
|
+
writeFileSync(join(specDir, 'plan.md'), `# Spec: Plan Phase
|
|
482
|
+
|
|
483
|
+
## Goal
|
|
484
|
+
[One-sentence feature description]
|
|
485
|
+
|
|
486
|
+
## Exploration Checklist
|
|
487
|
+
- [ ] Read existing code affected by this change
|
|
488
|
+
- [ ] Identify integration points and edge cases
|
|
489
|
+
- [ ] Propose approach (max 3 options, choose one)
|
|
490
|
+
- [ ] Get user approval before moving to implement phase
|
|
491
|
+
|
|
492
|
+
## Decision
|
|
493
|
+
Chosen approach: [write here]
|
|
494
|
+
Approved: yes / no
|
|
495
|
+
`);
|
|
496
|
+
writeFileSync(join(specDir, 'implement.md'), `# Spec: Implement Phase
|
|
497
|
+
|
|
498
|
+
## Acceptance Criteria
|
|
499
|
+
[Copy from queue.md task AC]
|
|
500
|
+
|
|
501
|
+
## TDD Checklist
|
|
502
|
+
- [ ] Write failing test first
|
|
503
|
+
- [ ] Implement minimum code to pass
|
|
504
|
+
- [ ] Refactor if needed (tests still green)
|
|
505
|
+
- [ ] Run npm test — all pass
|
|
506
|
+
|
|
507
|
+
## Files Changed
|
|
508
|
+
[List here]
|
|
509
|
+
`);
|
|
510
|
+
writeFileSync(join(specDir, 'verify.md'), `# Spec: Verify Phase
|
|
511
|
+
|
|
512
|
+
## Review Checklist
|
|
513
|
+
- [ ] All AC items satisfied
|
|
514
|
+
- [ ] No regressions (full test suite passes)
|
|
515
|
+
- [ ] Lint clean
|
|
516
|
+
- [ ] Edge cases handled
|
|
517
|
+
- [ ] Peer/self review complete
|
|
518
|
+
|
|
519
|
+
## Result
|
|
520
|
+
Status: DONE / FAILED
|
|
521
|
+
Notes: [write here]
|
|
522
|
+
`);
|
|
523
|
+
console.log('✓ Created .claudedash/spec/ (plan.md, implement.md, verify.md)');
|
|
524
|
+
console.log('\nUsage:');
|
|
525
|
+
console.log(' 1. Fill in spec/plan.md and get approval');
|
|
526
|
+
console.log(' 2. Follow spec/implement.md (TDD)');
|
|
527
|
+
console.log(' 3. Complete spec/verify.md checklist');
|
|
528
|
+
console.log(' 4. Log result to .claudedash/execution.log');
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error('❌ Failed to create spec templates:', error);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
program
|
|
536
|
+
.command('worktree')
|
|
537
|
+
.description('Manage isolated worktrees for parallel agent runs')
|
|
538
|
+
.argument('<action>', 'Action: create')
|
|
539
|
+
.argument('[branch]', 'Branch name for create action')
|
|
540
|
+
.action((action, branch) => {
|
|
541
|
+
if (action !== 'create') {
|
|
542
|
+
console.error(`❌ Unknown action: ${action}. Supported: create`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
if (!branch) {
|
|
546
|
+
console.error('❌ Branch name required: claudedash worktree create <branch>');
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
const worktreePath = join(process.cwd(), '..', branch.replace(/\//g, '-'));
|
|
550
|
+
execFile('git', ['worktree', 'add', '-b', branch, worktreePath], (err, _stdout, stderr) => {
|
|
551
|
+
if (err) {
|
|
552
|
+
console.error('❌ Failed to create worktree:', stderr || err.message);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
console.log(`✓ Worktree created at: ${worktreePath}`);
|
|
556
|
+
console.log(` Branch: ${branch}`);
|
|
557
|
+
console.log(`\nNext steps:`);
|
|
558
|
+
console.log(` cd ${worktreePath}`);
|
|
559
|
+
console.log(` claudedash init`);
|
|
560
|
+
console.log(` claudedash start`);
|
|
561
|
+
console.log(`\nThe dashboard Worktrees tab will show sessions for this branch.`);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
program
|
|
565
|
+
.command('recover')
|
|
566
|
+
.description('Summarize the last Claude Code session after /clear')
|
|
567
|
+
.option('--claude-dir <path>', 'Path to Claude directory', join(process.env.HOME || '~', '.claude'))
|
|
568
|
+
.action((opts) => {
|
|
569
|
+
const claudeDir = opts.claudeDir;
|
|
570
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
571
|
+
if (!existsSync(projectsDir)) {
|
|
572
|
+
console.error('❌ No projects directory found at', projectsDir);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
// Map cwd to Claude project dir name (slashes → hyphens, no leading -)
|
|
576
|
+
// Walk up from cwd to find a matching project directory
|
|
577
|
+
let projectDir = '';
|
|
578
|
+
let searchPath = process.cwd();
|
|
579
|
+
while (searchPath !== '/') {
|
|
580
|
+
const candidate = join(projectsDir, searchPath.replace(/\//g, '-'));
|
|
581
|
+
if (existsSync(candidate)) {
|
|
582
|
+
projectDir = candidate;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
searchPath = join(searchPath, '..');
|
|
586
|
+
}
|
|
587
|
+
if (!projectDir) {
|
|
588
|
+
console.error(`❌ No session history found for this directory or any parent.`);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
// Find most recently modified JSONL file
|
|
592
|
+
const jsonlFiles = readdirSync(projectDir)
|
|
593
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
594
|
+
.map(f => ({ name: f, mtime: statSync(join(projectDir, f)).mtime.getTime() }))
|
|
595
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
596
|
+
if (jsonlFiles.length === 0) {
|
|
597
|
+
console.error('❌ No session files found.');
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
const latestFile = join(projectDir, jsonlFiles[0].name);
|
|
601
|
+
const lines = readFileSync(latestFile, 'utf-8').trim().split('\n').filter(Boolean);
|
|
602
|
+
// Extract key info from JSONL
|
|
603
|
+
let lastUserMsg = '';
|
|
604
|
+
let lastAssistantText = '';
|
|
605
|
+
let sessionId = '';
|
|
606
|
+
let sessionStart = '';
|
|
607
|
+
for (const line of lines) {
|
|
608
|
+
try {
|
|
609
|
+
const entry = JSON.parse(line);
|
|
610
|
+
if (entry.sessionId && !sessionId) {
|
|
611
|
+
sessionId = String(entry.sessionId);
|
|
612
|
+
}
|
|
613
|
+
if (!sessionStart && entry.type === 'user') {
|
|
614
|
+
const msg = entry.message;
|
|
615
|
+
if (msg?.role === 'user') {
|
|
616
|
+
const ts = entry.timestamp ?? entry.ts;
|
|
617
|
+
if (ts)
|
|
618
|
+
sessionStart = String(ts);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (entry.type === 'user') {
|
|
622
|
+
const msg = entry.message;
|
|
623
|
+
const content = msg?.content;
|
|
624
|
+
if (typeof content === 'string')
|
|
625
|
+
lastUserMsg = content.slice(0, 200);
|
|
626
|
+
}
|
|
627
|
+
if (entry.type === 'assistant') {
|
|
628
|
+
const msg = entry.message;
|
|
629
|
+
const content = msg?.content;
|
|
630
|
+
if (Array.isArray(content)) {
|
|
631
|
+
for (const block of content) {
|
|
632
|
+
const b = block;
|
|
633
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
634
|
+
lastAssistantText = b.text.slice(0, 400);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch { /* skip malformed lines */ }
|
|
641
|
+
}
|
|
642
|
+
console.log('\n📋 Session Recovery Summary');
|
|
643
|
+
console.log('─'.repeat(50));
|
|
644
|
+
console.log(`Session file : ${jsonlFiles[0].name}`);
|
|
645
|
+
console.log(`Total events : ${lines.length}`);
|
|
646
|
+
if (sessionStart)
|
|
647
|
+
console.log(`Started : ${sessionStart}`);
|
|
648
|
+
if (lastUserMsg) {
|
|
649
|
+
console.log('\n💬 Last user message:');
|
|
650
|
+
console.log(` ${lastUserMsg.replace(/\n/g, '\n ')}`);
|
|
651
|
+
}
|
|
652
|
+
if (lastAssistantText) {
|
|
653
|
+
console.log('\n🤖 Last assistant response (excerpt):');
|
|
654
|
+
console.log(` ${lastAssistantText.replace(/\n/g, '\n ')}`);
|
|
655
|
+
}
|
|
656
|
+
// Plan mode state
|
|
657
|
+
const claudeWatchDir = join(process.cwd(), '.claudedash');
|
|
658
|
+
const logPath = join(claudeWatchDir, 'execution.log');
|
|
659
|
+
const queuePath = join(claudeWatchDir, 'queue.md');
|
|
660
|
+
if (existsSync(logPath) && existsSync(queuePath)) {
|
|
661
|
+
console.log('\n📊 Plan Mode State:');
|
|
662
|
+
const logLines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
663
|
+
const doneTasks = [];
|
|
664
|
+
let lastTask = '';
|
|
665
|
+
for (const l of logLines) {
|
|
666
|
+
try {
|
|
667
|
+
const e = JSON.parse(l);
|
|
668
|
+
const tid = String(e.task_id ?? '');
|
|
669
|
+
const status = String(e.status ?? '');
|
|
670
|
+
if (status === 'DONE')
|
|
671
|
+
doneTasks.push(tid);
|
|
672
|
+
lastTask = tid;
|
|
673
|
+
}
|
|
674
|
+
catch { /* skip */ }
|
|
675
|
+
}
|
|
676
|
+
console.log(` Completed tasks : ${doneTasks.length} (${doneTasks.join(', ') || 'none'})`);
|
|
677
|
+
if (lastTask)
|
|
678
|
+
console.log(` Last task : ${lastTask}`);
|
|
679
|
+
// Find next READY task
|
|
680
|
+
const queueContent = readFileSync(queuePath, 'utf-8');
|
|
681
|
+
const taskIds = [...queueContent.matchAll(/^## (S\d+-T\d+)/gm)].map(m => m[1]);
|
|
682
|
+
const doneSet = new Set(doneTasks);
|
|
683
|
+
const nextTask = taskIds.find(id => !doneSet.has(id));
|
|
684
|
+
if (nextTask)
|
|
685
|
+
console.log(` ➡ Next task : ${nextTask}`);
|
|
686
|
+
}
|
|
687
|
+
console.log('\n─'.repeat(50));
|
|
688
|
+
console.log('Run `claudedash start` to view the live dashboard.\n');
|
|
689
|
+
});
|
|
690
|
+
program
|
|
691
|
+
.command('doctor')
|
|
692
|
+
.description('Check claudedash environment and configuration')
|
|
693
|
+
.option('--claude-dir <path>', 'Path to Claude directory', join(process.env.HOME || '~', '.claude'))
|
|
694
|
+
.action(async (opts) => {
|
|
695
|
+
const claudeDir = opts.claudeDir;
|
|
696
|
+
const claudeWatchDir = join(process.cwd(), '.claudedash');
|
|
697
|
+
const checks = [];
|
|
698
|
+
// Node.js version
|
|
699
|
+
const nodeVer = process.version;
|
|
700
|
+
const major = parseInt(nodeVer.slice(1));
|
|
701
|
+
checks.push({ label: `Node.js ${nodeVer}`, ok: major >= 18, note: major < 18 ? 'Requires Node.js 18+' : undefined });
|
|
702
|
+
// git available
|
|
703
|
+
try {
|
|
704
|
+
const { execFileSync } = await import('child_process').then(m => m);
|
|
705
|
+
execFileSync('git', ['--version'], { stdio: 'ignore' });
|
|
706
|
+
checks.push({ label: 'git available', ok: true });
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
checks.push({ label: 'git available', ok: false, note: 'git not found in PATH' });
|
|
710
|
+
}
|
|
711
|
+
// ~/.claude/ directory
|
|
712
|
+
const claudeDirExists = existsSync(claudeDir);
|
|
713
|
+
checks.push({ label: `~/.claude/ directory (${claudeDir})`, ok: claudeDirExists, note: claudeDirExists ? undefined : 'Not found — Claude Code may not be installed' });
|
|
714
|
+
// ~/.claude/tasks/ (live mode)
|
|
715
|
+
const tasksDir = join(claudeDir, 'tasks');
|
|
716
|
+
checks.push({ label: '~/.claude/tasks/ (Live mode data)', ok: existsSync(tasksDir) });
|
|
717
|
+
// .claudedash/ (plan mode)
|
|
718
|
+
checks.push({ label: '.claudedash/ (Plan mode)', ok: existsSync(claudeWatchDir) });
|
|
719
|
+
// CLAUDE.md in cwd
|
|
720
|
+
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
721
|
+
const claudeMdExists = existsSync(claudeMdPath);
|
|
722
|
+
const hasTodoWrite = claudeMdExists && readFileSync(claudeMdPath, 'utf-8').includes('TodoWrite');
|
|
723
|
+
checks.push({ label: 'CLAUDE.md with TodoWrite directive', ok: hasTodoWrite, note: !claudeMdExists ? 'Not found — run claudedash init' : !hasTodoWrite ? 'TodoWrite missing — run claudedash init' : undefined });
|
|
724
|
+
// Port 4317 availability (simple check)
|
|
725
|
+
checks.push({ label: 'Default port 4317', ok: true, note: 'Cannot test without binding' });
|
|
726
|
+
// Print results
|
|
727
|
+
console.log('\n🩺 claudedash doctor\n');
|
|
728
|
+
for (const check of checks) {
|
|
729
|
+
const icon = check.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
730
|
+
const note = check.note ? ` \x1b[33m(${check.note})\x1b[0m` : '';
|
|
731
|
+
console.log(` ${icon} ${check.label}${note}`);
|
|
732
|
+
}
|
|
733
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
734
|
+
console.log(`\n${failed === 0 ? '\x1b[32mAll checks passed.\x1b[0m' : `\x1b[31m${failed} check(s) failed.\x1b[0m`}\n`);
|
|
735
|
+
});
|
|
246
736
|
program.parse();
|
|
247
737
|
//# sourceMappingURL=cli.js.map
|