mob-coordinator 0.2.2 → 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 +12 -13
- package/bin/mob.js +21 -10
- package/dist/server/server/hooks.js +119 -0
- package/dist/server/server/index.js +4 -0
- package/hooks/mob-status.ps1 +151 -0
- package/hooks/mob-status.sh +120 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -33,19 +33,13 @@ mob
|
|
|
33
33
|
|
|
34
34
|
Open `http://localhost:4040` in your browser.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
To enable status reporting (state, branch, auto-naming) from Claude instances, clone the repo and run the hook installer:
|
|
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:
|
|
39
37
|
|
|
40
38
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
npm install
|
|
44
|
-
npm run install-hooks
|
|
39
|
+
mob install-hooks # Re-install hooks
|
|
40
|
+
mob uninstall-hooks # Remove hooks
|
|
45
41
|
```
|
|
46
42
|
|
|
47
|
-
This adds hook entries to `~/.claude/settings.json` that point to the hook scripts in the cloned repo. You only need to do this once — the hooks work regardless of whether you run mob via npm or from source.
|
|
48
|
-
|
|
49
43
|
### Development
|
|
50
44
|
|
|
51
45
|
To contribute or run from source:
|
|
@@ -137,10 +131,8 @@ See `CLAUDE.md` for detailed architecture documentation.
|
|
|
137
131
|
|
|
138
132
|
## Uninstalling Hooks
|
|
139
133
|
|
|
140
|
-
From the cloned repo:
|
|
141
|
-
|
|
142
134
|
```bash
|
|
143
|
-
|
|
135
|
+
mob uninstall-hooks
|
|
144
136
|
```
|
|
145
137
|
|
|
146
138
|
## Troubleshooting
|
|
@@ -166,7 +158,7 @@ MOB_PORT=4050 npm run dev
|
|
|
166
158
|
|
|
167
159
|
### Hooks not reporting status
|
|
168
160
|
|
|
169
|
-
1.
|
|
161
|
+
1. Hooks are auto-installed on startup. To force re-install: `mob install-hooks`
|
|
170
162
|
2. Check `~/.claude/settings.json` has entries for `PreToolUse`, `PostToolUse`, `Stop`, `UserPromptSubmit`, and `Notification`
|
|
171
163
|
3. On Windows, ensure PowerShell can run the hook script (execution policy)
|
|
172
164
|
|
|
@@ -176,6 +168,13 @@ If hooks aren't firing (crash, subtask weirdness), the terminal state fallback s
|
|
|
176
168
|
|
|
177
169
|
## Changelog
|
|
178
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
|
+
|
|
179
178
|
### 0.2.1
|
|
180
179
|
|
|
181
180
|
- Fix false "Needs Input" showing when Claude finishes a task (completion notifications no longer trigger waiting state)
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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"
|