@poulles/worktree-dashboard 0.1.0 → 0.2.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/.claude/hooks/log-prompt-start.sh +40 -0
- package/.claude/hooks/log-prompt.sh +66 -0
- package/README.md +25 -1
- package/bin/cli.mjs +107 -4
- package/package.json +22 -6
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# UserPromptSubmit hook — runs the moment a prompt is submitted, before Claude
|
|
3
|
+
# does any work. Records the prompt as the worktree's CURRENT task so the
|
|
4
|
+
# dashboard shows "what it's working on" live during the turn. Writes a
|
|
5
|
+
# {kind:"start"} entry to the same central, per-branch log the Stop hook later
|
|
6
|
+
# appends a {kind:"done"} summary to.
|
|
7
|
+
#
|
|
8
|
+
# Wired up in .claude/settings.json. Receives the payload as JSON on stdin:
|
|
9
|
+
# { prompt, cwd, session_id, transcript_path, hook_event_name }
|
|
10
|
+
|
|
11
|
+
# Don't fire inside the Stop hook's headless summarizer call.
|
|
12
|
+
[ "${WORKTREE_LOG_HOOK:-}" = "1" ] && exit 0
|
|
13
|
+
|
|
14
|
+
input=$(cat)
|
|
15
|
+
# Critical: UserPromptSubmit stdout is injected into Claude's context. Send all
|
|
16
|
+
# of this script's output to the void so we only ever touch the log file.
|
|
17
|
+
exec >/dev/null 2>&1
|
|
18
|
+
|
|
19
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
20
|
+
|
|
21
|
+
prompt=$(printf '%s' "$input" | jq -r '.prompt // empty')
|
|
22
|
+
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty')
|
|
23
|
+
[ -n "$prompt" ] || exit 0
|
|
24
|
+
[ -n "$cwd" ] || cwd=$PWD
|
|
25
|
+
|
|
26
|
+
cd "$cwd" 2>/dev/null || exit 0
|
|
27
|
+
common=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
28
|
+
main=$(dirname "$(cd "$common" && pwd)")
|
|
29
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached)
|
|
30
|
+
safe=$(printf '%s' "$branch" | tr '/' '-')
|
|
31
|
+
logdir="$main/.worktree-logs"
|
|
32
|
+
mkdir -p "$logdir"
|
|
33
|
+
|
|
34
|
+
# Keep the stored prompt compact — the dashboard only needs a glanceable line.
|
|
35
|
+
prompt=$(printf '%s' "$prompt" | tr '\n' ' ' | cut -c1-500)
|
|
36
|
+
|
|
37
|
+
jq -nc --arg b "$branch" --arg p "$prompt" \
|
|
38
|
+
'{ts: (now * 1000 | floor), branch: $b, kind: "start", prompt: $p}' >> "$logdir/$safe.jsonl"
|
|
39
|
+
|
|
40
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook — runs every time Claude finishes a turn.
|
|
3
|
+
# Appends a one-line summary of the latest turn to a central, per-branch log in
|
|
4
|
+
# the MAIN checkout, so every worktree's history lives in one shared, gitignored
|
|
5
|
+
# place that the dashboard (which runs from main) can read.
|
|
6
|
+
#
|
|
7
|
+
# Wired up in .claude/settings.json. Receives the hook payload as JSON on stdin:
|
|
8
|
+
# { transcript_path, cwd, session_id, hook_event_name, stop_hook_active }
|
|
9
|
+
|
|
10
|
+
# Guard: the summary itself is produced by a headless `claude` call, whose own
|
|
11
|
+
# Stop hook would otherwise fire this script again — a fork bomb. Bail early when
|
|
12
|
+
# we're already inside the summarizer.
|
|
13
|
+
[ "${WORKTREE_LOG_HOOK:-}" = "1" ] && exit 0
|
|
14
|
+
|
|
15
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
16
|
+
command -v claude >/dev/null 2>&1 || exit 0
|
|
17
|
+
|
|
18
|
+
input=$(cat)
|
|
19
|
+
transcript=$(printf '%s' "$input" | jq -r '.transcript_path // empty')
|
|
20
|
+
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty')
|
|
21
|
+
[ -n "$transcript" ] && [ -f "$transcript" ] || exit 0
|
|
22
|
+
[ -n "$cwd" ] || cwd=$PWD
|
|
23
|
+
|
|
24
|
+
cd "$cwd" 2>/dev/null || exit 0
|
|
25
|
+
common=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
26
|
+
main=$(dirname "$(cd "$common" && pwd)")
|
|
27
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo detached)
|
|
28
|
+
safe=$(printf '%s' "$branch" | tr '/' '-')
|
|
29
|
+
logdir="$main/.worktree-logs"
|
|
30
|
+
mkdir -p "$logdir"
|
|
31
|
+
|
|
32
|
+
# Summarize + write asynchronously so we never add latency to the turn.
|
|
33
|
+
(
|
|
34
|
+
# Files changed in the latest turn: tool_uses that come after the last *human*
|
|
35
|
+
# prompt (a user event whose content is text, not a tool_result).
|
|
36
|
+
files=$(jq -rs '
|
|
37
|
+
. as $all
|
|
38
|
+
| ([ range(0; ($all | length)) as $i
|
|
39
|
+
| select($all[$i].type == "user"
|
|
40
|
+
and (($all[$i].message.content | type == "string")
|
|
41
|
+
or (any($all[$i].message.content[]?; .type == "text")))) | $i ] | last) as $u
|
|
42
|
+
| [ $all[(($u // -1) + 1):][]
|
|
43
|
+
| select(.type == "assistant") | .message.content[]?
|
|
44
|
+
| select(.type == "tool_use" and (.name == "Write" or .name == "Edit" or .name == "NotebookEdit"))
|
|
45
|
+
| .input.file_path ] | unique
|
|
46
|
+
' "$transcript" 2>/dev/null)
|
|
47
|
+
case "$files" in "" | null) files='[]' ;; esac
|
|
48
|
+
|
|
49
|
+
instr='Below is the tail of a Claude Code session transcript in JSONL. In ONE concise sentence (max ~20 words), summarize what the user most recently asked for and what was done in response. Reply with ONLY that sentence — no preamble, no quotes.'
|
|
50
|
+
body=$(tail -c 16000 "$transcript")
|
|
51
|
+
|
|
52
|
+
# Run the summarizer from a throwaway dir so its own session transcript lands
|
|
53
|
+
# in a separate ~/.claude/projects entry and never shadows the worktree's real
|
|
54
|
+
# session in the dashboard.
|
|
55
|
+
sdir="${TMPDIR:-/tmp}/worktree-log-summarizer"
|
|
56
|
+
mkdir -p "$sdir"
|
|
57
|
+
summary=$(cd "$sdir" && printf '%s\n\n%s\n' "$instr" "$body" \
|
|
58
|
+
| WORKTREE_LOG_HOOK=1 claude -p --model haiku 2>/dev/null \
|
|
59
|
+
| tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
60
|
+
[ -n "$summary" ] || summary='(summary unavailable)'
|
|
61
|
+
|
|
62
|
+
jq -nc --arg b "$branch" --arg s "$summary" --argjson f "$files" \
|
|
63
|
+
'{ts: (now * 1000 | floor), branch: $b, kind: "done", summary: $s, files: $f}' >> "$logdir/$safe.jsonl"
|
|
64
|
+
) >/dev/null 2>&1 &
|
|
65
|
+
|
|
66
|
+
exit 0
|
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ npx @poulles/worktree-dashboard
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
worktree-dashboard [options]
|
|
20
|
+
worktree-dashboard [options] # start the dashboard
|
|
21
|
+
worktree-dashboard init # install prompt-logging hooks (see below)
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
| Flag | Default | Description |
|
|
@@ -28,6 +29,28 @@ worktree-dashboard [options]
|
|
|
28
29
|
| `--worktrees <path>` | `.claude/worktrees` | Path to the worktrees folder |
|
|
29
30
|
| `--config <path>` | `.worktree-dashboard.json` | Path to a config file |
|
|
30
31
|
|
|
32
|
+
## Prompt logging
|
|
33
|
+
|
|
34
|
+
By default the dashboard shows each agent's live status. To also show **what each
|
|
35
|
+
worktree was last asked to do** (a one-line summary per turn), install the logging hooks
|
|
36
|
+
into your project:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @poulles/worktree-dashboard init
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This copies two Claude Code hooks into `.claude/hooks/`, wires them into
|
|
43
|
+
`.claude/settings.json` (existing settings are preserved), and adds `.worktree-logs/` to
|
|
44
|
+
`.gitignore`. The hooks write a per-branch log on every prompt and turn; the dashboard
|
|
45
|
+
reads those logs and shows the latest prompt/summary on each worktree card. Without this
|
|
46
|
+
step, nothing creates `.worktree-logs/` and the prompt column stays empty.
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
|
|
50
|
+
- Requires `jq` and the `claude` CLI on your PATH — summaries are generated by Claude.
|
|
51
|
+
- Re-running `init` is safe; it won't duplicate hooks already present.
|
|
52
|
+
- Restart any running Claude Code session afterward so it picks up the new hooks.
|
|
53
|
+
|
|
31
54
|
## Config file
|
|
32
55
|
|
|
33
56
|
Create `.worktree-dashboard.json` in your project root. CLI flags override these values.
|
|
@@ -112,6 +135,7 @@ Each worktree card links to VS Code so you can jump straight to the relevant fil
|
|
|
112
135
|
- Node.js 18+
|
|
113
136
|
- git
|
|
114
137
|
- VS Code with `code` on PATH
|
|
138
|
+
- `jq` and the `claude` CLI — only for prompt logging (`worktree-dashboard init`)
|
|
115
139
|
|
|
116
140
|
## WSL
|
|
117
141
|
|
package/bin/cli.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, existsSync } from 'fs';
|
|
3
|
-
import { resolve, join } from 'path';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync } from 'fs';
|
|
3
|
+
import { resolve, join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import { spawn } from 'child_process';
|
|
5
6
|
import { createServer } from '../src/server.mjs';
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
|
+
const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
8
10
|
|
|
9
11
|
function parseArgs(argv) {
|
|
10
12
|
const args = {};
|
|
@@ -50,6 +52,84 @@ function loadLogo(logoPath) {
|
|
|
50
52
|
return `data:${mime};base64,${data}`;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// ── `init` command ──────────────────────────────────────────────────────────
|
|
56
|
+
// Install the prompt-logging hooks into the current project so the dashboard can
|
|
57
|
+
// show "what each worktree is working on". The dashboard only READS the logs in
|
|
58
|
+
// `.worktree-logs/`; these Claude Code hooks are what create and write them.
|
|
59
|
+
const HOOK_FILES = ['log-prompt-start.sh', 'log-prompt.sh'];
|
|
60
|
+
const HOOK_EVENTS = {
|
|
61
|
+
UserPromptSubmit: '$CLAUDE_PROJECT_DIR/.claude/hooks/log-prompt-start.sh',
|
|
62
|
+
Stop: '$CLAUDE_PROJECT_DIR/.claude/hooks/log-prompt.sh',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Ensure settings.hooks[event] contains a command entry, without clobbering any
|
|
66
|
+
// existing hooks (including a previous run of this command).
|
|
67
|
+
function ensureHook(settings, event, command) {
|
|
68
|
+
settings.hooks ??= {};
|
|
69
|
+
const groups = (settings.hooks[event] ??= []);
|
|
70
|
+
const already = groups.some(g => (g.hooks ?? []).some(h => h.command === command));
|
|
71
|
+
if (already) return false;
|
|
72
|
+
groups.push({ hooks: [{ type: 'command', command }] });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureGitignore(target) {
|
|
77
|
+
const file = join(target, '.gitignore');
|
|
78
|
+
const entry = '.worktree-logs/';
|
|
79
|
+
const existing = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
80
|
+
if (existing.split('\n').some(l => l.trim() === entry)) return false;
|
|
81
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
82
|
+
writeFileSync(file, `${existing}${prefix}${entry}\n`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function init() {
|
|
87
|
+
const target = process.cwd();
|
|
88
|
+
const srcHooks = join(PKG_ROOT, '.claude/hooks');
|
|
89
|
+
const destHooks = join(target, '.claude/hooks');
|
|
90
|
+
|
|
91
|
+
if (!existsSync(join(srcHooks, HOOK_FILES[0]))) {
|
|
92
|
+
console.error(` ✗ Could not find bundled hooks at ${srcHooks}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mkdirSync(destHooks, { recursive: true });
|
|
97
|
+
for (const f of HOOK_FILES) {
|
|
98
|
+
copyFileSync(join(srcHooks, f), join(destHooks, f));
|
|
99
|
+
chmodSync(join(destHooks, f), 0o755);
|
|
100
|
+
}
|
|
101
|
+
console.log(` ✓ Installed hooks → ${join('.claude', 'hooks')}/`);
|
|
102
|
+
|
|
103
|
+
const settingsPath = join(target, '.claude/settings.json');
|
|
104
|
+
let settings = {};
|
|
105
|
+
if (existsSync(settingsPath)) {
|
|
106
|
+
try {
|
|
107
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(` ✗ Could not parse ${settingsPath}: ${e.message}`);
|
|
110
|
+
console.error(' Fix or remove it, then re-run `worktree-dashboard init`.');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let added = false;
|
|
115
|
+
for (const [event, command] of Object.entries(HOOK_EVENTS)) {
|
|
116
|
+
added = ensureHook(settings, event, command) || added;
|
|
117
|
+
}
|
|
118
|
+
if (added) {
|
|
119
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
120
|
+
console.log(` ✓ Wired hooks into ${join('.claude', 'settings.json')}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(` • Hooks already wired in ${join('.claude', 'settings.json')}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (ensureGitignore(target)) console.log(' ✓ Added .worktree-logs/ to .gitignore');
|
|
126
|
+
|
|
127
|
+
console.log('\n Prompt logging is set up. Notes:');
|
|
128
|
+
console.log(' • Requires `jq` and the `claude` CLI on your PATH (summaries use Claude).');
|
|
129
|
+
console.log(' • Restart any running Claude Code session to pick up the new hooks.');
|
|
130
|
+
console.log(' • Logs land in .worktree-logs/ in your main checkout.\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
53
133
|
function openBrowser(url) {
|
|
54
134
|
try {
|
|
55
135
|
const procVersion = readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
@@ -66,8 +146,31 @@ function openBrowser(url) {
|
|
|
66
146
|
}
|
|
67
147
|
}
|
|
68
148
|
|
|
149
|
+
function printHelp() {
|
|
150
|
+
console.log(`
|
|
151
|
+
Worktree Dashboard (v${VERSION})
|
|
152
|
+
|
|
153
|
+
Usage:
|
|
154
|
+
worktree-dashboard [options] Start the dashboard (default)
|
|
155
|
+
worktree-dashboard init Install prompt-logging hooks into this project
|
|
156
|
+
worktree-dashboard --help Show this help
|
|
157
|
+
|
|
158
|
+
Options:
|
|
159
|
+
--port <n> Port to listen on (default 3333)
|
|
160
|
+
--title <text> Dashboard title
|
|
161
|
+
--worktrees <path> Worktrees directory (default .claude/worktrees)
|
|
162
|
+
--logo <path> Path to a logo image
|
|
163
|
+
--config <path> Path to a .worktree-dashboard.json config file
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
|
|
69
167
|
function main() {
|
|
70
|
-
const
|
|
168
|
+
const argv = process.argv.slice(2);
|
|
169
|
+
const command = argv[0];
|
|
170
|
+
if (command === 'init') return init();
|
|
171
|
+
if (command === '--help' || command === '-h' || command === 'help') return printHelp();
|
|
172
|
+
|
|
173
|
+
const cliArgs = parseArgs(argv);
|
|
71
174
|
const fileConfig = loadFileConfig(cliArgs.configPath);
|
|
72
175
|
|
|
73
176
|
const config = {
|
package/package.json
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poulles/worktree-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A local dashboard for monitoring and managing git worktrees with Claude Code agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"worktree-dashboard": "./bin/cli.mjs"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
".claude/hooks"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"worktree",
|
|
20
|
+
"git",
|
|
21
|
+
"dashboard",
|
|
22
|
+
"developer-tools"
|
|
23
|
+
],
|
|
12
24
|
"license": "MIT",
|
|
13
25
|
"repository": {
|
|
14
26
|
"type": "git",
|
|
15
27
|
"url": "git+https://github.com/gpoulles/worktree-dashboard.git"
|
|
16
28
|
},
|
|
17
|
-
"bugs": {
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/gpoulles/worktree-dashboard/issues"
|
|
31
|
+
},
|
|
18
32
|
"homepage": "https://github.com/gpoulles/worktree-dashboard#readme",
|
|
19
|
-
"publishConfig": {
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
20
36
|
}
|