@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.
@@ -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 VERSION = '0.1.0';
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 cliArgs = parseArgs(process.argv.slice(2));
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.1.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": ["bin", "src"],
10
- "engines": { "node": ">=18" },
11
- "keywords": ["claude-code", "worktree", "git", "dashboard", "developer-tools"],
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": { "url": "https://github.com/gpoulles/worktree-dashboard/issues" },
29
+ "bugs": {
30
+ "url": "https://github.com/gpoulles/worktree-dashboard/issues"
31
+ },
18
32
  "homepage": "https://github.com/gpoulles/worktree-dashboard#readme",
19
- "publishConfig": { "access": "public" }
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
20
36
  }