mob-coordinator 0.2.1 → 0.3.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
@@ -27,47 +27,33 @@ A local web dashboard for coordinating multiple Claude Code CLI sessions. Launch
27
27
  ## Quick Start
28
28
 
29
29
  ```bash
30
- git clone https://github.com/nickelbob/mob.git
31
- cd mob
32
- npm install
30
+ npm install -g mob-coordinator
31
+ mob
33
32
  ```
34
33
 
35
- If you see errors about missing native modules, run:
34
+ Open `http://localhost:4040` in your browser.
36
35
 
37
- ```bash
38
- npm run setup
39
- ```
40
-
41
- This detects your platform and installs the correct native binaries for node-pty and rollup. It also runs automatically before `npm run dev` and `npm run build`.
42
-
43
- ### Install Claude Code Hooks
44
-
45
- To enable status reporting (state, branch, auto-naming) from Claude instances launched by mob:
36
+ Claude Code hooks are installed automatically on first launch. These enable status reporting (state, branch, auto-naming) from Claude instances. To manually manage hooks:
46
37
 
47
38
  ```bash
48
- npm run install-hooks
39
+ mob install-hooks # Re-install hooks
40
+ mob uninstall-hooks # Remove hooks
49
41
  ```
50
42
 
51
- This adds hook entries to `~/.claude/settings.json` that report instance status back to the dashboard.
43
+ ### Development
52
44
 
53
- ### Run
54
-
55
- **Development** (hot-reload):
45
+ To contribute or run from source:
56
46
 
57
47
  ```bash
48
+ git clone https://github.com/nickelbob/mob.git
49
+ cd mob
50
+ npm install
58
51
  npm run dev
59
52
  ```
60
53
 
61
54
  Opens the backend on `http://localhost:4040` and the Vite dev server on `http://localhost:4041`. Use port 4041 during development.
62
55
 
63
- **Production**:
64
-
65
- ```bash
66
- npm run build
67
- npm start
68
- ```
69
-
70
- Everything is served from `http://localhost:4040`.
56
+ If you see errors about missing native modules, run `npm run setup` to auto-detect and install the correct platform-specific binaries.
71
57
 
72
58
  ## Usage
73
59
 
@@ -146,7 +132,7 @@ See `CLAUDE.md` for detailed architecture documentation.
146
132
  ## Uninstalling Hooks
147
133
 
148
134
  ```bash
149
- npm run uninstall-hooks
135
+ mob uninstall-hooks
150
136
  ```
151
137
 
152
138
  ## Troubleshooting
@@ -172,7 +158,7 @@ MOB_PORT=4050 npm run dev
172
158
 
173
159
  ### Hooks not reporting status
174
160
 
175
- 1. Make sure hooks are installed: `npm run install-hooks`
161
+ 1. Hooks are auto-installed on startup. To force re-install: `mob install-hooks`
176
162
  2. Check `~/.claude/settings.json` has entries for `PreToolUse`, `PostToolUse`, `Stop`, `UserPromptSubmit`, and `Notification`
177
163
  3. On Windows, ensure PowerShell can run the hook script (execution policy)
178
164
 
@@ -182,6 +168,18 @@ If hooks aren't firing (crash, subtask weirdness), the terminal state fallback s
182
168
 
183
169
  ## Changelog
184
170
 
171
+ ### 0.3.0
172
+
173
+ - Auto-install Claude Code hooks on server startup — no manual setup required
174
+ - Add `mob install-hooks` and `mob uninstall-hooks` CLI subcommands
175
+ - Include hook scripts in npm package
176
+ - Add landing page website for GitHub Pages
177
+
178
+ ### 0.2.1
179
+
180
+ - Fix false "Needs Input" showing when Claude finishes a task (completion notifications no longer trigger waiting state)
181
+ - npm package available: `npm install -g mob-coordinator`
182
+
185
183
  ### 0.2.0
186
184
 
187
185
  - Terminal-based state fallback when hooks are silent (detects running/waiting/idle from scrollback)
package/bin/mob.js CHANGED
@@ -6,15 +6,26 @@ import { dirname, join } from 'path';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const root = join(__dirname, '..');
9
- const serverEntry = join(root, 'dist', 'server', 'server', 'index.js');
10
9
 
11
- // Run the server, forwarding stdio and signals
12
- const child = spawn(process.execPath, [serverEntry], {
13
- cwd: root,
14
- stdio: 'inherit',
15
- env: { ...process.env },
16
- });
10
+ const subcommand = process.argv[2];
17
11
 
18
- child.on('exit', (code) => process.exit(code ?? 0));
19
- process.on('SIGINT', () => child.kill('SIGINT'));
20
- process.on('SIGTERM', () => child.kill('SIGTERM'));
12
+ if (subcommand === 'install-hooks') {
13
+ const { installHooks } = await import(join(root, 'dist', 'server', 'server', 'hooks.js'));
14
+ installHooks(root);
15
+ } else if (subcommand === 'uninstall-hooks') {
16
+ const { uninstallHooks } = await import(join(root, 'dist', 'server', 'server', 'hooks.js'));
17
+ uninstallHooks();
18
+ } else {
19
+ const serverEntry = join(root, 'dist', 'server', 'server', 'index.js');
20
+
21
+ // Run the server, forwarding stdio and signals
22
+ const child = spawn(process.execPath, [serverEntry], {
23
+ cwd: root,
24
+ stdio: 'inherit',
25
+ env: { ...process.env },
26
+ });
27
+
28
+ child.on('exit', (code) => process.exit(code ?? 0));
29
+ process.on('SIGINT', () => child.kill('SIGINT'));
30
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
31
+ }
@@ -0,0 +1,119 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
5
+ const HOOK_EVENTS = [
6
+ 'PreToolUse',
7
+ 'PostToolUse',
8
+ 'Stop',
9
+ 'UserPromptSubmit',
10
+ 'Notification',
11
+ ];
12
+ function getHookCommand(packageRoot) {
13
+ if (process.platform === 'win32') {
14
+ const hookScript = path.resolve(packageRoot, 'hooks', 'mob-status.ps1');
15
+ const psPath = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
16
+ return `"${psPath}" -NoProfile -ExecutionPolicy Bypass -File "${hookScript}"`;
17
+ }
18
+ const hookScript = path.resolve(packageRoot, 'hooks', 'mob-status.sh');
19
+ return `bash "${hookScript}"`;
20
+ }
21
+ function readSettings() {
22
+ const settingsDir = path.dirname(CLAUDE_SETTINGS_PATH);
23
+ if (!fs.existsSync(settingsDir)) {
24
+ fs.mkdirSync(settingsDir, { recursive: true });
25
+ }
26
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
27
+ try {
28
+ return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8'));
29
+ }
30
+ catch {
31
+ // Corrupted settings — start fresh
32
+ }
33
+ }
34
+ return {};
35
+ }
36
+ function writeSettings(settings) {
37
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
38
+ }
39
+ function isMobHook(entry) {
40
+ // New format: { matcher, hooks: [{ type, command }] }
41
+ if (entry.hooks && Array.isArray(entry.hooks)) {
42
+ return entry.hooks.some((h) => h.type === 'command' && typeof h.command === 'string' && h.command.includes('mob-status'));
43
+ }
44
+ // Old format: { type, command }
45
+ if (entry.type === 'command' && typeof entry.command === 'string' && entry.command.includes('mob-status')) {
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ export function installHooks(packageRoot, quiet = false) {
51
+ const hookScript = process.platform === 'win32'
52
+ ? path.resolve(packageRoot, 'hooks', 'mob-status.ps1')
53
+ : path.resolve(packageRoot, 'hooks', 'mob-status.sh');
54
+ if (!fs.existsSync(hookScript)) {
55
+ if (!quiet)
56
+ console.error(`Hook script not found: ${hookScript}`);
57
+ return;
58
+ }
59
+ const settings = readSettings();
60
+ if (!settings.hooks)
61
+ settings.hooks = {};
62
+ const hookCommand = getHookCommand(packageRoot);
63
+ for (const event of HOOK_EVENTS) {
64
+ if (!settings.hooks[event]) {
65
+ settings.hooks[event] = [];
66
+ }
67
+ // Remove any existing mob hooks (old or new format)
68
+ settings.hooks[event] = settings.hooks[event].filter((entry) => !isMobHook(entry));
69
+ // Add hook in current format
70
+ settings.hooks[event].push({
71
+ matcher: '',
72
+ hooks: [{ type: 'command', command: hookCommand }],
73
+ });
74
+ }
75
+ writeSettings(settings);
76
+ if (!quiet) {
77
+ console.log('Mob hooks installed successfully!');
78
+ console.log(`Hook script: ${hookScript}`);
79
+ console.log(`Events: ${HOOK_EVENTS.join(', ')}`);
80
+ }
81
+ }
82
+ export function uninstallHooks(quiet = false) {
83
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
84
+ if (!quiet)
85
+ console.log('No Claude settings found, nothing to uninstall.');
86
+ return;
87
+ }
88
+ let settings;
89
+ try {
90
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8'));
91
+ }
92
+ catch {
93
+ if (!quiet)
94
+ console.error('Could not parse settings.json');
95
+ return;
96
+ }
97
+ if (!settings.hooks) {
98
+ if (!quiet)
99
+ console.log('No hooks found in settings.');
100
+ return;
101
+ }
102
+ let removed = 0;
103
+ for (const event of Object.keys(settings.hooks)) {
104
+ if (Array.isArray(settings.hooks[event])) {
105
+ const before = settings.hooks[event].length;
106
+ settings.hooks[event] = settings.hooks[event].filter((entry) => !isMobHook(entry));
107
+ removed += before - settings.hooks[event].length;
108
+ if (settings.hooks[event].length === 0) {
109
+ delete settings.hooks[event];
110
+ }
111
+ }
112
+ }
113
+ if (Object.keys(settings.hooks).length === 0) {
114
+ delete settings.hooks;
115
+ }
116
+ writeSettings(settings);
117
+ if (!quiet)
118
+ console.log(`Removed ${removed} mob hook(s) from Claude settings.`);
119
+ }
@@ -9,6 +9,7 @@ import { ScrollbackBuffer } from './scrollback-buffer.js';
9
9
  import { SettingsManager } from './settings-manager.js';
10
10
  import { ensureDir, getMobDir, getInstancesDir, getSessionsDir, getScrollbackDir } from './util/platform.js';
11
11
  import { DEFAULT_PORT } from '../shared/constants.js';
12
+ import { installHooks } from './hooks.js';
12
13
  const port = parseInt(process.env.MOB_PORT || '', 10) || DEFAULT_PORT;
13
14
  const host = process.env.MOB_HOST || '127.0.0.1';
14
15
  // Ensure directories exist
@@ -16,6 +17,9 @@ ensureDir(getMobDir());
16
17
  ensureDir(getInstancesDir());
17
18
  ensureDir(getSessionsDir());
18
19
  ensureDir(getScrollbackDir());
20
+ // Auto-install Claude Code hooks so external instances are discovered
21
+ const packageRoot = new URL('../../..', import.meta.url).pathname;
22
+ installHooks(packageRoot, true);
19
23
  const settingsManager = new SettingsManager();
20
24
  const ptyManager = new PtyManager();
21
25
  const discovery = new DiscoveryService();
@@ -0,0 +1,151 @@
1
+ # Mob status hook for Claude Code (Windows PowerShell)
2
+ # Reads hook event from stdin, writes instance status to ~/.mob/instances/{id}.json
3
+
4
+ $MobDir = Join-Path $env:USERPROFILE ".mob"
5
+ $InstancesDir = Join-Path $MobDir "instances"
6
+ $LogFile = Join-Path $MobDir "hook-debug.log"
7
+ $MobPort = if ($env:MOB_PORT) { $env:MOB_PORT } else { "4040" }
8
+
9
+ New-Item -ItemType Directory -Force -Path $InstancesDir | Out-Null
10
+
11
+ function Log($msg) {
12
+ $ts = Get-Date -Format "HH:mm:ss.fff"
13
+ "$ts $msg" | Out-File -Append -FilePath $LogFile -Encoding UTF8
14
+ }
15
+
16
+ Log "=== Hook started (PID=$PID) ==="
17
+
18
+ # Only report for mob-launched instances
19
+ if (-not $env:MOB_INSTANCE_ID) {
20
+ Log "No MOB_INSTANCE_ID, skipping"
21
+ exit 0
22
+ }
23
+
24
+ # Read JSON from stdin
25
+ try {
26
+ $RawInput = [Console]::In.ReadToEnd()
27
+ } catch {
28
+ Log "stdin read FAILED: $_"
29
+ exit 0
30
+ }
31
+
32
+ if (-not $RawInput -or $RawInput.Trim().Length -eq 0) {
33
+ Log "Empty stdin, exiting"
34
+ exit 0
35
+ }
36
+
37
+ try {
38
+ $Data = $RawInput | ConvertFrom-Json
39
+ } catch {
40
+ Log "JSON parse FAILED: $_"
41
+ exit 0
42
+ }
43
+
44
+ # Claude Code uses hook_event_name, not event
45
+ $HookEvent = if ($Data.hook_event_name) { "$($Data.hook_event_name)" }
46
+ elseif ($Data.event) { "$($Data.event)" }
47
+ else { "" }
48
+ Log "Event=$HookEvent"
49
+
50
+ # Determine instance ID
51
+ $InstanceId = if ($env:MOB_INSTANCE_ID) { $env:MOB_INSTANCE_ID }
52
+ elseif ($Data.session_id) { "$($Data.session_id)" }
53
+ else { "ext-$PID" }
54
+ Log "InstanceId=$InstanceId"
55
+
56
+ $Cwd = if ($Data.cwd) { "$($Data.cwd)" } else { (Get-Location).Path }
57
+
58
+ # Git branch
59
+ $GitBranch = ""
60
+ try { $GitBranch = git -C "$Cwd" branch --show-current 2>$null } catch {}
61
+
62
+ # State from event
63
+ $State = switch ($HookEvent) {
64
+ "SessionStart" { "running" }
65
+ "SessionEnd" { "stopped" }
66
+ "Stop" { "idle" }
67
+ "PreToolUse" { "running" }
68
+ "PostToolUse" { "running" }
69
+ "Notification" { "waiting" }
70
+ "UserPromptSubmit" { "idle" }
71
+ default { "running" }
72
+ }
73
+ Log "State=$State"
74
+
75
+ $ToolName = if ($Data.tool_name) { "$($Data.tool_name)" } else { "" }
76
+
77
+ # Extract user prompt from UserPromptSubmit for auto-naming
78
+ # Claude Code sends the prompt in the "prompt" field
79
+ $Topic = ""
80
+ if ($HookEvent -eq "UserPromptSubmit" -and $Data.prompt) {
81
+ $Topic = ("$($Data.prompt)" -split "`n")[0]
82
+ if ($Topic.Length -gt 80) { $Topic = $Topic.Substring(0, 80) }
83
+ Log "Topic=$Topic"
84
+ }
85
+
86
+ # Task metadata
87
+ $Ticket = ""
88
+ $Subtask = ""
89
+ $Progress = $null
90
+ $TicketStatus = ""
91
+ $TaskFile = Join-Path $Cwd ".mob-task.json"
92
+ if (Test-Path $TaskFile) {
93
+ $Task = Get-Content $TaskFile | ConvertFrom-Json
94
+ $Ticket = if ($Task.ticket) { "$($Task.ticket)" } else { "" }
95
+ $Subtask = if ($Task.subtask) { "$($Task.subtask)" } else { "" }
96
+ $Progress = $Task.progress
97
+ $TicketStatus = if ($Task.ticketStatus) { "$($Task.ticketStatus)" } else { "" }
98
+ }
99
+
100
+ $Timestamp = [long](Get-Date -UFormat %s) * 1000
101
+
102
+ $Status = @{
103
+ id = $InstanceId
104
+ cwd = "$Cwd"
105
+ gitBranch = "$GitBranch"
106
+ state = "$State"
107
+ hookEvent = "$HookEvent"
108
+ ticket = "$Ticket"
109
+ ticketStatus = "$TicketStatus"
110
+ subtask = "$Subtask"
111
+ currentTool = "$ToolName"
112
+ lastUpdated = $Timestamp
113
+ sessionId = "$InstanceId"
114
+ topic = "$Topic"
115
+ }
116
+ if ($null -ne $Progress) { $Status.progress = $Progress }
117
+
118
+ $Json = $Status | ConvertTo-Json -Compress
119
+ Log "JSON=$Json"
120
+
121
+ # Atomic write
122
+ try {
123
+ $TmpFile = Join-Path $InstancesDir ".tmp.$InstanceId.json"
124
+ $FinalFile = Join-Path $InstancesDir "$InstanceId.json"
125
+ $Json | Out-File -FilePath $TmpFile -Encoding UTF8 -NoNewline
126
+ Move-Item -Force $TmpFile $FinalFile
127
+ Log "File written OK"
128
+ } catch {
129
+ Log "File write FAILED: $_"
130
+ }
131
+
132
+ # Best-effort POST (bypass system proxy, use 127.0.0.1 to avoid IPv6)
133
+ try {
134
+ $wc = New-Object System.Net.WebClient
135
+ $wc.Proxy = $null
136
+ $wc.Headers.Add("Content-Type", "application/json")
137
+ $wc.UploadString("http://127.0.0.1:${MobPort}/api/hook", $Json) | Out-Null
138
+ Log "POST OK"
139
+ } catch {
140
+ Log "POST failed: $_"
141
+ }
142
+
143
+ # Clean up on stop
144
+ if ($State -eq "stopped") {
145
+ Start-Job -ScriptBlock {
146
+ Start-Sleep -Seconds 5
147
+ Remove-Item -Force $using:FinalFile
148
+ } | Out-Null
149
+ }
150
+
151
+ Log "=== Hook finished ==="
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # Mob status hook for Claude Code
3
+ # Reads hook event from stdin, writes instance status to ~/.mob/instances/{id}.json
4
+ # and POSTs to the dashboard for instant updates.
5
+
6
+ set -euo pipefail
7
+
8
+ MOB_DIR="$HOME/.mob"
9
+ INSTANCES_DIR="$MOB_DIR/instances"
10
+ MOB_PORT="${MOB_PORT:-4040}"
11
+
12
+ mkdir -p "$INSTANCES_DIR"
13
+
14
+ # Only report for mob-launched instances
15
+ if [ -z "${MOB_INSTANCE_ID:-}" ]; then
16
+ exit 0
17
+ fi
18
+
19
+ # Read JSON from stdin
20
+ INPUT=$(cat)
21
+
22
+ # Determine instance ID
23
+ if [ -n "${MOB_INSTANCE_ID:-}" ]; then
24
+ INSTANCE_ID="$MOB_INSTANCE_ID"
25
+ else
26
+ INSTANCE_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "")
27
+ if [ -z "$INSTANCE_ID" ]; then
28
+ INSTANCE_ID="ext-$$"
29
+ fi
30
+ fi
31
+
32
+ # Extract fields from hook input
33
+ CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || pwd)
34
+ [ -z "$CWD" ] && CWD=$(pwd)
35
+
36
+ # Try to get git branch
37
+ GIT_BRANCH=""
38
+ if command -v git &>/dev/null && [ -d "$CWD/.git" ] || git -C "$CWD" rev-parse --git-dir &>/dev/null 2>&1; then
39
+ GIT_BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null || echo "")
40
+ fi
41
+
42
+ # Determine state from hook event type
43
+ # Claude Code uses hook_event_name, fall back to event for compatibility
44
+ HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // .event // empty' 2>/dev/null || echo "")
45
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
46
+
47
+ case "$HOOK_EVENT" in
48
+ "SessionStart") STATE="running" ;;
49
+ "SessionEnd") STATE="stopped" ;;
50
+ "Stop") STATE="idle" ;;
51
+ "PreToolUse") STATE="running" ;;
52
+ "PostToolUse") STATE="running" ;;
53
+ "Notification") STATE="waiting" ;;
54
+ "UserPromptSubmit") STATE="idle" ;;
55
+ *) STATE="running" ;;
56
+ esac
57
+
58
+ # Extract user prompt from UserPromptSubmit for auto-naming
59
+ TOPIC=""
60
+ if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
61
+ RAW_MSG=$(echo "$INPUT" | jq -r '.prompt // .message // empty' 2>/dev/null || echo "")
62
+ if [ -n "$RAW_MSG" ]; then
63
+ # Truncate to first 80 chars, first line only
64
+ TOPIC=$(echo "$RAW_MSG" | head -1 | cut -c1-80)
65
+ fi
66
+ fi
67
+
68
+ # Extract session_id from hook input
69
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "")
70
+
71
+ # Read optional task metadata from .mob-task.json in working directory
72
+ TICKET=""
73
+ SUBTASK=""
74
+ PROGRESS=""
75
+ TICKET_STATUS=""
76
+ if [ -f "$CWD/.mob-task.json" ]; then
77
+ TICKET=$(jq -r '.ticket // empty' "$CWD/.mob-task.json" 2>/dev/null || echo "")
78
+ SUBTASK=$(jq -r '.subtask // empty' "$CWD/.mob-task.json" 2>/dev/null || echo "")
79
+ PROGRESS=$(jq -r '.progress // empty' "$CWD/.mob-task.json" 2>/dev/null || echo "")
80
+ TICKET_STATUS=$(jq -r '.ticketStatus // empty' "$CWD/.mob-task.json" 2>/dev/null || echo "")
81
+ fi
82
+
83
+ TIMESTAMP=$(date +%s)000
84
+
85
+ # Build JSON status
86
+ STATUS_JSON=$(cat <<ENDJSON
87
+ {
88
+ "id": "$INSTANCE_ID",
89
+ "cwd": "$CWD",
90
+ "gitBranch": "$GIT_BRANCH",
91
+ "state": "$STATE",
92
+ "hookEvent": "$HOOK_EVENT",
93
+ "ticket": "$TICKET",
94
+ "ticketStatus": "$TICKET_STATUS",
95
+ "subtask": "$SUBTASK",
96
+ "topic": "$TOPIC",
97
+ $([ -n "$PROGRESS" ] && echo "\"progress\": $PROGRESS," || echo "")
98
+ "currentTool": "$TOOL_NAME",
99
+ "lastUpdated": $TIMESTAMP,
100
+ "sessionId": "${SESSION_ID:-$INSTANCE_ID}"
101
+ }
102
+ ENDJSON
103
+ )
104
+
105
+ # Atomic write via tmp+rename
106
+ TMP_FILE="$INSTANCES_DIR/.tmp.${INSTANCE_ID}.json"
107
+ echo "$STATUS_JSON" > "$TMP_FILE"
108
+ mv "$TMP_FILE" "$INSTANCES_DIR/${INSTANCE_ID}.json"
109
+
110
+ # Best-effort POST to dashboard (non-blocking)
111
+ if command -v curl &>/dev/null; then
112
+ curl -s -X POST "http://localhost:${MOB_PORT}/api/hook" \
113
+ -H "Content-Type: application/json" \
114
+ -d "$STATUS_JSON" &>/dev/null &
115
+ fi
116
+
117
+ # If session ended, clean up the status file after a delay
118
+ if [ "$STATE" = "stopped" ]; then
119
+ (sleep 5 && rm -f "$INSTANCES_DIR/${INSTANCE_ID}.json") &
120
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mob-coordinator",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Local web dashboard for coordinating multiple Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "files": [
36
36
  "bin/",
37
37
  "dist/",
38
+ "hooks/",
38
39
  "scripts/postinstall.cjs",
39
40
  "package.json",
40
41
  "README.md"