Files
skills/skills/session-inbox-monitor/hooks/inbox-monitor.ps1
vitya 146fbdb107 feat(skills): mail on Mappa — inter-session-messaging v2.0.0 + session-inbox-monitor v1.0.0
- 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).
2026-08-24 15:19:27 +03:00

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