mob-coordinator 0.2.2 → 0.3.1
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 +21 -19
- package/bin/mob.js +21 -10
- package/dist/client/assets/{index-DkdPImCW.js → index-D6WthCp0.js} +8 -8
- package/dist/client/index.html +1 -1
- package/dist/server/server/hooks.js +119 -0
- package/dist/server/server/index.js +4 -0
- package/dist/server/shared/settings.js +12 -9
- package/hooks/mob-status.ps1 +151 -0
- package/hooks/mob-status.sh +120 -0
- package/package.json +2 -1
package/dist/client/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Mob — Claude Instance Dashboard</title>
|
|
7
7
|
<link rel="icon" href="/favicon.png" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D6WthCp0.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BE5nxcXU.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -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();
|
|
@@ -4,16 +4,19 @@ export const DEFAULT_SETTINGS = {
|
|
|
4
4
|
toggleSidebar: 'Alt+KeyB',
|
|
5
5
|
cycleInstanceDown: 'Alt+ArrowDown',
|
|
6
6
|
cycleInstanceUp: 'Alt+ArrowUp',
|
|
7
|
-
jumpToInstance1: '
|
|
8
|
-
jumpToInstance2: '
|
|
9
|
-
jumpToInstance3: '
|
|
10
|
-
jumpToInstance4: '
|
|
11
|
-
jumpToInstance5: '
|
|
12
|
-
jumpToInstance6: '
|
|
13
|
-
jumpToInstance7: '
|
|
14
|
-
jumpToInstance8: '
|
|
15
|
-
jumpToInstance9: '
|
|
7
|
+
jumpToInstance1: 'Alt+Digit1',
|
|
8
|
+
jumpToInstance2: 'Alt+Digit2',
|
|
9
|
+
jumpToInstance3: 'Alt+Digit3',
|
|
10
|
+
jumpToInstance4: 'Alt+Digit4',
|
|
11
|
+
jumpToInstance5: 'Alt+Digit5',
|
|
12
|
+
jumpToInstance6: 'Alt+Digit6',
|
|
13
|
+
jumpToInstance7: 'Alt+Digit7',
|
|
14
|
+
jumpToInstance8: 'Alt+Digit8',
|
|
15
|
+
jumpToInstance9: 'Alt+Digit9',
|
|
16
16
|
resumeInstance: 'Alt+KeyR',
|
|
17
|
+
killInstance: 'Alt+KeyW',
|
|
18
|
+
dismissInstance: 'Alt+KeyX',
|
|
19
|
+
openSettings: 'Alt+Comma',
|
|
17
20
|
},
|
|
18
21
|
launch: {
|
|
19
22
|
cwd: '',
|
|
@@ -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.1",
|
|
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"
|