- inter-session-messaging v2.0.0: channel switched from file inbox (.agents/inbox/) to Mappa inbox.send/inbox.monitor/entity_get; letters are entities i:N, delivery is a lease carve-out; address book + project-exists check via admin_status; replies via entity_get(id).meta.from; subject carries [event: ...] instead of frontmatter. - session-inbox-monitor v1.0.0: monitor now polls GET /inbox?project=<cwd> (HTTP, dedup by letter id, no .read/ move); hook inbox-monitor.ps1 rewritten to inject the mappa poll command (sweep sentinel + project dir). - delegate-task v0.5.1: covering letter goes through inbox_send, not file path. Part of #983 (mappa-skills).
104 lines
5.5 KiB
PowerShell
104 lines
5.5 KiB
PowerShell
# SessionStart inbox-monitor injector hook (session-inbox-monitor skill, v1.0.0).
|
|
#
|
|
# Channel is Mappa, NOT files (flip, решение 15): letters are entities `inbox`
|
|
# (`i:N`) in the mappa service, read via HTTP `GET /inbox?project=<name>`.
|
|
# Two jobs, run on every SessionStart (startup / resume / clear / compact):
|
|
# (a) SWEEP - kill orphaned inbox-monitor OS processes of THIS project.
|
|
# A `/clear` does NOT fire SessionEnd, so a Monitor's underlying
|
|
# poll process can outlive the session it belonged to. Without a
|
|
# sweep, re-raising would stack duplicates. Match is by a sentinel
|
|
# string (CLAUDE_INBOX_MONITOR) baked into the poll command PLUS
|
|
# this project's directory - so we never touch unrelated processes.
|
|
# (b) INJECT - additionalContext telling the agent to raise a persistent
|
|
# Monitor (Monitor TOOL, not background Bash) polling the Mappa
|
|
# inbox of this project (HTTP GET /inbox).
|
|
#
|
|
# Opt-in per project: AGENTS.md or CLAUDE.md line `inbox monitor: raise on start`.
|
|
# (The `.agents/inbox/` dir trigger is gone - no file channel anymore.)
|
|
#
|
|
# Headless (`claude -p`): there is NO reliable hook-level signal to detect it
|
|
# (verified 2026-06-17 - `source` and CLAUDE_* env vars don't distinguish it).
|
|
# So the hook injects unconditionally and the SKILL instructs the agent to skip
|
|
# when headless. A Monitor raised in headless is harmless (killed ~5s after the
|
|
# run ends); a false-skip in an interactive session would silently lose the
|
|
# feature - so the default errs toward raising.
|
|
#
|
|
# Machine-local deploy target: ~/.claude/hooks/inbox-monitor.ps1 (registered in
|
|
# ~/.claude/settings.json SessionStart). Versioned here for multi-machine rollout.
|
|
|
|
param(
|
|
[string]$ProjectDir = $env:CLAUDE_PROJECT_DIR,
|
|
[string]$Endpoint = $env:MAPPA_CORE_URL
|
|
)
|
|
|
|
if (-not $ProjectDir) { exit 0 }
|
|
if (-not $Endpoint) { $Endpoint = 'https://mappa.vds.kzntsv.site' }
|
|
|
|
# UTF-8 stdout guard. This hook emits JSON (additionalContext) to a redirected
|
|
# pipe under WinPS 5.1 - the same context that mojibaked stop-dispatcher output.
|
|
# The project name (user-data) is interpolated into stdout, so set UTF-8 as a
|
|
# forward-guard. Idempotent.
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
$projectName = Split-Path $ProjectDir -Leaf
|
|
$agentsMd = Join-Path $ProjectDir 'AGENTS.md'
|
|
$claudeMd = Join-Path $ProjectDir 'CLAUDE.md'
|
|
|
|
# --- opt-in gate (line in AGENTS.md or CLAUDE.md) ---------------------------
|
|
$optedIn = $false
|
|
foreach ($md in @($agentsMd, $claudeMd)) {
|
|
if (Test-Path $md) {
|
|
if (Select-String -Path $md -SimpleMatch 'inbox monitor: raise on start' -Quiet -ErrorAction SilentlyContinue) {
|
|
$optedIn = $true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (-not $optedIn) { exit 0 }
|
|
|
|
# Forward-slash project dir: the Monitor poll command (Git Bash) uses this form,
|
|
# so both the sweep match and the injected command share one literal.
|
|
$dirFwd = ($ProjectDir -replace '\\', '/')
|
|
|
|
# --- (a) sweep orphaned monitors of THIS project ----------------------------
|
|
# Match = sentinel AND this project's dir in the same process command line.
|
|
try {
|
|
Get-CimInstance Win32_Process -ErrorAction Stop |
|
|
Where-Object {
|
|
$_.CommandLine -and
|
|
$_.CommandLine -match 'CLAUDE_INBOX_MONITOR' -and
|
|
$_.CommandLine -like "*$dirFwd*"
|
|
} |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
} catch { }
|
|
|
|
# --- (b) build the canonical Monitor poll command ---------------------------
|
|
# `: CLAUDE_INBOX_MONITOR` is a bash no-op carrying the sweep sentinel in the
|
|
# process command line without polluting the event stream. Polls the Mappa
|
|
# inbox of this project over HTTP, extracts letter ids via node (present on
|
|
# every machine that runs the mappa MCP), de-dups by id so a sitting letter
|
|
# pages once, not every 15s (a noisy monitor is auto-stopped).
|
|
$auth = ''
|
|
if ($env:MAPPA_API_TOKEN) { $auth = "-H 'x-api-token: $($env:MAPPA_API_TOKEN)'" }
|
|
$cmd = @"
|
|
: CLAUDE_INBOX_MONITOR; s=' '; while true; do ids=`$(curl -s -m 10 $auth '__ENDPOINT__/inbox?project=__PROJECT__&limit=50' | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const r=JSON.parse(d).rows||[];for(let i=r.length-1;i>=0;i--)console.log(r[i].id)}catch(e){}})"); for id in `$ids; do case "`$s" in *" `$id "*) continue;; esac; s="`$s`$id "; echo "New inter-session message in Mappa inbox (letter id `$id) - read it via inbox_monitor and handle now"; done; sleep 15; done
|
|
"@
|
|
$cmd = $cmd.Trim().Replace('__ENDPOINT__', $Endpoint.TrimEnd('/')).Replace('__PROJECT__', $projectName)
|
|
|
|
# --- (c) inject the raise-instruction --------------------------------------
|
|
$ctx = @"
|
|
[session-inbox-monitor] This project participates in inter-session messaging. As your FIRST action, raise a persistent inbox watcher so messages from other sessions page you in real time.
|
|
|
|
Use the Monitor tool with persistent: true, description "inbox watcher", and this EXACT command:
|
|
|
|
$cmd
|
|
|
|
Do NOT use a background Bash for this - it leaks across /clear. The Monitor tool is session-bound and tears down on its own at session end. The paired SessionStart hook already swept any orphaned watcher before this, so raise exactly one.
|
|
|
|
If you are running headless (claude -p / non-interactive), SKIP this - the Stop-hook inbox pickup plus Notify cover delivery there. See the session-inbox-monitor skill for the full contract.
|
|
"@
|
|
|
|
@{ hookSpecificOutput = @{ hookEventName = 'SessionStart'; additionalContext = $ctx } } | ConvertTo-Json -Compress -Depth 5
|
|
exit 0
|