Per spec/agent-watcher.md §4. TypeScript/Node implementation living in
mcp-watcher/ subdirectory, parallel to Layer 1 Collector at repo root.
What lands:
- Core MCP server (src/server.ts) with experimental['claude/channel']:{}
+ tools:{} capability declarations, stdio transport, channel-event
notifier wired through the inbox watcher.
- Identity resolution mirroring agent-ping's layered model
(PING_AGENT_IDENTITY env, $CLAUDE_HOME/ping-agent, ~/.ping-agent).
- Inbox reader with HWM tracking, sentinel deferral (warn-after-3),
atomic HWM writes via tmp+rename.
- chokidar-backed file watcher with coalesced drain, urgent-first
ordering, recentEvents map for tool sender lookup.
- Three reply tools (ack / respond / mark_handled) with cross-host
write discipline (writes to local inbox files; Syncthing replicates).
- Sentinel file (.<agent>.watcher-active) for hook coexistence per
spec §4.3 — agent-ping hook stands down when the watcher is in
charge of delivery on this host. Sentinel + hwm in .stignore.
- 35 unit tests passing (vitest): inbox parsing, HWM round-trip,
sentinel deferral semantics, identity layers, tool I/O, watcher
drain + ordering + restart-from-hwm.
- install.sh (Angus-executed, rule-2 compliant) installs deps,
builds, symlinks ~/.local/bin/agent-watcher-mcp, prints mcp.json
registration snippet for paste.
- README documents launch flag, sandbox CLAUDE_HOME pattern,
hook coexistence, observability, v2 limitations.
Not yet:
- Integration test against a real Claude Code session — gated on
Angus spinning up a sandbox CC session on the VPS with
CLAUDE_HOME=~/.claude-sandbox.
- agent-ping hook update to read the sentinel and stand down.
Separate small PR against agent-ping.
Interface contract with Layer 1: the inbox JSONL line shape from
inbox.ts::PingEvent matches inbox.Event in the Collector — bit-
identical reads regardless of source.
57 lines
1.7 KiB
TypeScript
57 lines
1.7 KiB
TypeScript
// Identity resolution for the MCP Watcher.
|
|
//
|
|
// Mirrors the layered resolution shipped in agent-ping (PR #1). The CLI
|
|
// has four layers (--as flag, env, $CLAUDE_HOME/ping-agent, ~/.ping-agent);
|
|
// the watcher subprocess has three (no argv override useful in MCP context).
|
|
//
|
|
// Spec: agent-ping spec §9; agent-watcher spec §4.4.
|
|
|
|
import { homedir } from "node:os";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
export interface ResolvedIdentity {
|
|
agent: string;
|
|
source: string;
|
|
}
|
|
|
|
/**
|
|
* Resolve this process's agent identity.
|
|
* Layers, first match wins:
|
|
* 1. PING_AGENT_IDENTITY env var
|
|
* 2. $CLAUDE_HOME/ping-agent (if CLAUDE_HOME is set and file exists)
|
|
* 3. ~/.ping-agent
|
|
*
|
|
* Throws if no layer resolves.
|
|
*/
|
|
export function resolveIdentity(env: NodeJS.ProcessEnv = process.env): ResolvedIdentity {
|
|
const envId = (env.PING_AGENT_IDENTITY ?? "").trim();
|
|
if (envId) {
|
|
return { agent: envId, source: "PING_AGENT_IDENTITY env var" };
|
|
}
|
|
|
|
const claudeHome = (env.CLAUDE_HOME ?? "").trim();
|
|
if (claudeHome) {
|
|
const f = join(claudeHome, "ping-agent");
|
|
if (existsSync(f)) {
|
|
const name = readFileSync(f, "utf8").trim();
|
|
if (name) {
|
|
return { agent: name, source: f };
|
|
}
|
|
}
|
|
}
|
|
|
|
const home = env.HOME ?? homedir();
|
|
const defaultFile = join(home, ".ping-agent");
|
|
if (existsSync(defaultFile)) {
|
|
const name = readFileSync(defaultFile, "utf8").trim();
|
|
if (name) {
|
|
return { agent: name, source: defaultFile };
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`agent-watcher-mcp: no identity resolved. Set PING_AGENT_IDENTITY, ` +
|
|
`$CLAUDE_HOME/ping-agent, or ${defaultFile}.`,
|
|
);
|
|
}
|