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 +26 -28
- 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
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
npm install
|
|
30
|
+
npm install -g mob-coordinator
|
|
31
|
+
mob
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Open `http://localhost:4040` in your browser.
|
|
36
35
|
|
|
37
|
-
|
|
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
|
-
|
|
39
|
+
mob install-hooks # Re-install hooks
|
|
40
|
+
mob uninstall-hooks # Remove hooks
|
|
49
41
|
```
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
### Development
|
|
52
44
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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"
|