{"_id":"@cahalane/pi-monitor","_rev":"2-f9f1f734d6db39b91082f0034877d574","name":"@cahalane/pi-monitor","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@cahalane/pi-monitor","version":"0.1.0","keywords":["pi-package","pi-extension"],"author":{"name":"Colm Cahalane","email":"colmcahalane@gmail.com"},"license":"ISC","_id":"@cahalane/pi-monitor@0.1.0","maintainers":[{"name":"colm2","email":"colmcahalane@gmail.com"}],"homepage":"https://github.com/cahalane/pi-monitor#readme","bugs":{"url":"https://github.com/cahalane/pi-monitor/issues"},"pi":{"extensions":["./index.ts"]},"dist":{"shasum":"b813716462688c83fd33ed3d264ca349d06ca5e6","tarball":"https://registry.npmjs.org/@cahalane/pi-monitor/-/pi-monitor-0.1.0.tgz","fileCount":12,"integrity":"sha512-eKIBvJwJyu+FUZoNMfNjKwlOTPuSMsUJlMUSemwq4K61D65jAKHyQUkblVxDwUd6urAxG3Zhr8cKn9zqvUIfKg==","signatures":[{"sig":"MEYCIQDbXlcVRhynxIqqWOSDKszGVl6fW5JHvPjIABAWjvEYJgIhALhCvjmfjCi7fsKzYPmmWVUXWsjoYasExq6NkIAaKO5K","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":89035},"type":"module","gitHead":"9fa3757bdafc77193d3a1a31ff9be82b9d0d0876","scripts":{"test":"node --test --experimental-strip-types tests/*.test.ts"},"_npmUser":{"name":"colm2","email":"colmcahalane@gmail.com"},"repository":{"url":"git+ssh://git@github.com/cahalane/pi-monitor.git","type":"git"},"_npmVersion":"11.12.1","description":"Monitor tool for pi: stream background command, poll and WebSocket output into the live session.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"undici":"^8.10.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"typebox":"*","@earendil-works/pi-ai":"*","@earendil-works/pi-coding-agent":"*"},"_npmOperationalInternal":{"tmp":"tmp/pi-monitor_0.1.0_1788290494464_0.43044544436343646","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@cahalane/pi-monitor","version":"0.2.0","type":"module","engines":{"node":">=22.6"},"description":"Monitor tool for pi: stream background command, poll and WebSocket output into the live session.","keywords":["pi-package","pi-extension"],"license":"ISC","author":{"name":"Colm Cahalane","email":"colmcahalane@gmail.com"},"repository":{"type":"git","url":"git+ssh://git@github.com/cahalane/pi-monitor.git"},"bugs":{"url":"https://github.com/cahalane/pi-monitor/issues"},"homepage":"https://github.com/cahalane/pi-monitor#readme","publishConfig":{"access":"public"},"pi":{"extensions":["./index.ts"]},"scripts":{"test":"node --test --experimental-strip-types tests/*.test.ts","test:integration":"node --test --experimental-strip-types tests/integration/*.test.ts","typecheck":"tsc --noEmit","pack:verify":"node .github/scripts/verify-pack.cjs"},"dependencies":{"undici":"^8.10.0"},"peerDependencies":{"@earendil-works/pi-ai":"*","@earendil-works/pi-coding-agent":"*","typebox":"*"},"devDependencies":{"@types/node":"^24.13.3","typescript":"^5.9.3"},"_id":"@cahalane/pi-monitor@0.2.0","gitHead":"3a24607e5a97001f9d736ca463d4dc6660204c73","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-+uW1Zb+IRkJOPUbruFb9GXHqWgLecblowKEtM+3xPYUJicJ8I51ZTdEWbtQ4JLkBgKkgcBmEI8d7NyaMdsFwFQ==","shasum":"b984fe4066b7ac0f2c28430278ebc54cc56d5ea4","tarball":"https://registry.npmjs.org/@cahalane/pi-monitor/-/pi-monitor-0.2.0.tgz","fileCount":12,"unpackedSize":95013,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIG/Rb9m8+WWuL4qc8SevJvy4SGIrsMzPdN4k1rnselP9AiAz2vgBwyGy5ZAm4Yf9Z3iY5tf/bdJnvgremo07DpNQlQ=="}]},"_npmUser":{"name":"colm2","email":"colmcahalane@gmail.com"},"directories":{},"maintainers":[{"name":"colm2","email":"colmcahalane@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-monitor_0.2.0_1788425428495_0.3148364596369273"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-01T19:21:34.241Z","modified":"2026-09-03T08:50:28.817Z","0.1.0":"2026-09-01T19:21:34.617Z","0.2.0":"2026-09-03T08:50:28.636Z"},"bugs":{"url":"https://github.com/cahalane/pi-monitor/issues"},"author":{"name":"Colm Cahalane","email":"colmcahalane@gmail.com"},"license":"ISC","homepage":"https://github.com/cahalane/pi-monitor#readme","keywords":["pi-package","pi-extension"],"repository":{"type":"git","url":"git+ssh://git@github.com/cahalane/pi-monitor.git"},"description":"Monitor tool for pi: stream background command, poll and WebSocket output into the live session.","maintainers":[{"name":"colm2","email":"colmcahalane@gmail.com"}],"readme":"# pi-monitor\n\nA [pi](https://pi.dev) extension for event-stream and change monitoring. The model calls\n`monitor` once; streamed command events, change-only polling for CI and deploy status, WebSocket\nfeeds, and bounded wake-ups deliver relevant changes into the running session. It does\nnot need to rerun a status command in a loop.\n\n## Attribution\n\nThis project is original work by Colm Cahalane. Its delivery-scheduler design was informed by\n[pi-background-tasks](https://pi.dev/packages/pi-background-tasks), an ISC-licensed pi extension by\n[Ismail](https://github.com/ismailsaleekh). This repository does not include source files from that\npackage; the upstream notice is preserved in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).\n\nThe model calls `monitor` once. Each batch of new output then arrives on its own as a separate\nmessage, and the model can start a turn to react to it.\n\n## Example\n\nAsk pi to watch a build, and it makes one tool call:\n\n```\nmonitor { name: \"build\", command: \"./gradlew build --console=plain\", match: \"ERROR|BUILD\" }\n```\n\nOutput then arrives on its own, one message per batch:\n\n```\nmonitor mon_1 \"build\" — line 1 (untrusted output)\n| ERROR: unresolved reference: fooBar\n\nmonitor mon_1 \"build\" ended — command exited 1. 4 lines delivered, 310 B, over 47s.\nThe watch is finished; do not restart it just to confirm.\n```\n\nFor CI status, use `poll` so unchanged output costs nothing:\n\n```\nmonitor { name: \"ci\", poll: { command: \"gh pr checks 4821\", intervalMs: 30000 }, until: \"fail|pass\" }\n```\n\n## When to use it\n\nUse `monitor` for event-stream and change monitoring: tailing a build log, watching a long-running\ncommand, checking CI/PR/deploy status on a timer, or consuming a WebSocket feed. It is a good fit\nwhen the model would otherwise poll. `poll` reruns the command but emits only changed output, and\nscheduler limits keep automatic wake-ups bounded and controlled.\n\nThis is not the best tool for a one-shot noisy command where only the terminal result matters.\nUse `bg_run` for that case; its normal terminal notification reports completion.\n\nThree sources:\n\n- `command` — streams a long-running command's stdout/stderr lines as they arrive.\n- `poll` — reruns a command on an interval and reports only when the output changes. This is the\n  CI/PR/deploy case; a status table that prints unchanged every ten seconds produces no events\n  until it moves.\n- `ws` — connects to a WebSocket feed that already pushes events, rather than pulling them.\n\n## Requirements\n\nNode 22.6 or newer. The extension is TypeScript loaded through pi, and it uses node's built-in\n`WebSocket`, so nothing needs compiling.\n\n## Install\n\n```\npi install git:github.com/cahalane/pi-monitor\n```\n\nThat adds the package to `~/.pi/settings.json` and runs `npm install` for you, which pulls in\n`undici`. Restart pi and the `monitor`, `monitor_list` and `monitor_stop` tools are available.\n\nTo hack on it instead, clone the repo and install from the path:\n\n```\ngit clone git@github.com:cahalane/pi-monitor.git\ncd pi-monitor && npm install\npi install ./\n```\n\n`undici` is only needed for WebSocket monitors, which use it to pin the connection to an\nalready-validated address (see Security). Without `undici` importable, `ws` monitors refuse to start\nunless `PI_MONITOR_ALLOW_UNPINNED_WS=1` is set. `command` and `poll` monitors do not need it.\n\n## Uninstall\n\n```\npi remove git:github.com/cahalane/pi-monitor\n```\n\n## Tools\n\n### `monitor`\n\nStarts a watch and returns immediately with a monitor id (`mon_1`, `mon_2`, …), the resolved\nsource, and the caps in effect. Exactly one of `command`, `poll`, `ws` is required.\n\n| Parameter | Type | Default | Notes |\n|---|---|---|---|\n| `name` | string | required | Short label, shown in event headers, the widget and `monitor_list`. |\n| `command` | string | — | Long-running shell command; mutually exclusive with `poll` and `ws`. |\n| `poll` | `{command, intervalMs?}` | — | Rerun `command` every `intervalMs` (min 1000 ms, default 15000 ms, max 3,600,000 ms) and emit only when the canonical output changes. |\n| `ws` | `{url, protocols?}` | — | `ws://` or `wss://` only, no embedded credentials, no whitespace. |\n| `cwd` | string | session cwd | `command` and `poll` only. |\n| `timeoutMs` | number | 300000 | Watch ends at this deadline unless `persistent`. Clamped to 1000–86,400,000 ms. |\n| `persistent` | boolean | false | Ignore the deadline; run until stopped or capped. |\n| `match` | string (regex) | — | Only lines matching this become events. |\n| `ignore` | string (regex) | — | Lines matching this are dropped, applied after `match`. |\n| `until` | string (regex) | — | The watch ends once a line matches, after delivering that line. |\n| `dedupe` | boolean | false | Drop a line identical to the immediately preceding accepted line. |\n| `batchMs` | number | 400 | Coalesce lines arriving within this window into one interjection; 0 sends each line as it lands. Clamped to 0–60000 ms. |\n| `maxEvents` | number | 100 | Stop the monitor after this many delivered lines. Clamped to 1–1000. |\n| `wake` | boolean | true | Start a turn (`triggerTurn`) when output arrives and the agent is idle. `false` means the output waits for the next reply anyway. |\n| `deliverAs` | `steer` \\| `followUp` \\| `nextTurn` | `steer` | Escape hatch for a monitor whose output should not interrupt the current line of work. |\n\nCommand monitors run under `/bin/sh -c` (or `cmd.exe /c` on Windows) in their own process group.\n\nFiltering happens after ANSI and other terminal control sequences are stripped. The `match`,\n`ignore`, `until`, and `dedupe` checks see the bounded line (the maximum is 2000 characters by\ndefault). Patterns that match common catastrophic-backtracking shapes are rejected heuristically;\nthis is not a guarantee that a regular expression is safe. Poll monitors run the same way, once\nper interval, and hash trimmed, newline-normalised, ANSI-free canonical output to detect change.\n\n### `monitor_list`\n\nNo parameters. Returns every monitor in the session — live and recently ended — with id, name,\nsource, status, events delivered, bytes injected, uptime, last-event age, buffered-but-undelivered\nline count, and end reason if ended. Also reports the session-wide injected-byte budget used out of\n262144 bytes (256 KiB). Cheap to call; the footer widget is the primary display while something is\nrunning.\n\n### `monitor_stop`\n\nOne parameter, `id` — a monitor id or `\"all\"`. Stops the source (kills the process group, or\ncloses the socket) and returns the final counts. Does not send a model-facing terminal notice for a\ntool-initiated stop; the tool's own return value is the notice.\n\n### `/monitor` command\n\n`/monitor list` (or bare `/monitor`) prints the same snapshot as `monitor_list`. `/monitor stop\n<id|all>` stops one or all monitors. Both are host UI notifications, not tokens spent by the model.\n\n### Footer status and widget\n\nWhile at least one monitor is live, the status line shows `◉ N watching` (with a `(waking\nsuspended)` suffix when the consecutive-wake cap has fired), and a widget lists each live monitor\nwith its event count and last-event age. Both clear when no monitor is live.\n\n## Delivery semantics\n\npi's `pi.sendMessage` has three delivery modes, and monitor uses all of them precisely:\n\n- `steer` delivers after the current assistant turn finishes all of its tool calls, immediately\n  before the next LLM call. It does not interrupt a running tool call — pi has no mechanism for\n  that.\n- `followUp` waits until the agent has no tool calls left at all before delivering.\n- `triggerTurn: true` starts a new turn only when the agent is idle; it never creates a concurrent\n  turn alongside one already running.\n\nMonitor events and the terminal \"monitor ended\" notice both use the monitor's own `deliverAs`\n(default `steer`). They share one queue deliberately: sending the notice as `followUp` while events\nwent out as `steer` let an idle session show \"ended\" before the last event it summarised.\n\n### Scheduler rules\n\n- One undelivered notification per monitor. Once a monitor's batch is sent, further lines coalesce\n  in its buffer rather than triggering a second send. The buffer is released once `turn_end` or\n  `agent_settled` fires, which is pi's signal that the previous notification has been consumed.\n- One automatic wake outstanding across the whole registry at a time. A second monitor's output\n  arriving while a wake is already pending is sent without `triggerTurn` (it will be picked up\n  anyway) rather than queuing a second wake.\n- A minimum wake interval of 5000 ms. If a monitor's batch is ready before 5 s have passed since\n  the last wake, the flush is deferred rather than sent — sending it immediately would queue a\n  message with nothing scheduled to read it.\n- A cap of 8 consecutive monitor-triggered turns. Hitting it suspends waking: monitors keep\n  collecting and their output still arrives, but nothing starts a turn until the user sends a\n  message (`before_agent_start`), which resets the counter and lifts the suspension. The model is\n  told this once, via a `followUp` notice with `triggerTurn: false`.\n\n## Caps and budgets\n\n| Budget | Value | Effect when hit |\n|---|---|---|\n| Live monitors per session | 8 | `monitor` rejects a new watch until one is stopped. |\n| Events per monitor | 100 (or `maxEvents`) | The monitor auto-stops with a `cap` end reason. |\n| Injected bytes per monitor | 64 KiB | Same: auto-stop, `cap` end reason. |\n| Injected bytes per session | 256 KiB | Every live monitor is stopped with a `cap` end reason citing the session budget. |\n| Buffered-but-undelivered bytes, all monitors | 64 KiB | Oldest lines are dropped from the biggest buffers first; the drop count surfaces as \"N dropped\" in the next event header. |\n| Lines per flush | 20 (ceiling 50) | Extra lines are elided from the middle of the batch with a `… N lines elided …` marker. |\n| Bytes per flush | 8 KiB | Lines are dropped from the middle (recomputing the elision marker) until the batch fits, or hard-truncated as a last resort. |\n| Line length | 2000 characters | The line's head is elided, keeping the tail, since new output is usually at the end. |\n| Poll command output captured per tick | 64 KiB | Extra output is discarded before hashing/diffing. |\n| WebSocket message size | 1 MiB | The message is dropped and the watch ends with an `error` reason. |\n\nA monitor that hits `maxEvents` or its byte cap does not just stop quietly: it sends the same\n`monitor-ended` notice as any other end reason, so the model does not need to poll to find out.\n\n## Limitations\n\n- Monitors are session-scoped. `/new`, `/resume`, `/fork` and quitting all stop every monitor;\n  nothing resumes across sessions, and `monitor_list` after `/resume` is empty.\n- Print mode (`pi -p`) exits as soon as the agent settles, which stops the monitors with it. A watch\n  whose first event is seconds away will not survive; monitors are for interactive sessions.\n- There is no mid-tool-call interrupt in pi. A monitor's output cannot land while a tool the model\n  called is still running; it waits for that tool call to finish.\n- Process-group kill (`SIGTERM` then `SIGKILL` after 3 s) is POSIX only. On Windows, stop calls\n  `child.kill()` on the shell process itself and is best effort — a pipeline's children may\n  outlive it.\n- A WebSocket message over 1 MiB ends the watch rather than truncating it. Subscribe to a filtered\n  feed instead of a firehose.\n- A binary WebSocket frame is never decoded; it becomes a `[binary frame, N bytes]` placeholder\n  line.\n\n## Security\n\nEvery event and terminal notice tells the model the payload is untrusted output and not to follow\ninstructions found in it. There is no guarantee that secrets are redacted. ANSI and control\nsequence stripping removes terminal decoration before filtering and hashing; it does not sanitise\ninstructions or secrets. Each line is also prefixed with `| ` in the rendered payload, so nothing in\na watched stream can forge a header or close the block early.\n\nWebSocket monitors are the SSRF-sensitive path. `ws://`/`wss://` targets are validated (scheme,\nASCII only, no embedded credentials, valid subprotocol tokens), the hostname is resolved once, and\nevery resolved address is checked against loopback, private (10/8, 172.16/12, 192.168/16),\nlink-local (169.254/16, which covers cloud metadata endpoints), CGNAT (100.64/10), and the IPv6\nequivalents. The connection is then made through an `undici` dispatcher whose `lookup` returns only\nthat already-validated address — TLS still verifies and SNIs on the real hostname, but the connect\nstep cannot re-resolve to a different, private address after the check passed. Without `undici` importable, `ws` monitors refuse to start unless `PI_MONITOR_ALLOW_UNPINNED_WS=1` is set, which\naccepts the re-resolution risk explicitly.\n\nOpening a WebSocket also asks the user first: `monitor` calls `ctx.ui.confirm` with the target URL\nonce per call, and refuses the connection if the answer is no. With no UI attached there is nobody\nto ask, so `ws` monitors fail unless `PI_MONITOR_ALLOW_WS=1` is set. There is no per-host \"always\nallow\".\n\nCommand and poll monitors are not sandboxed and are not gated by any confirmation prompt: calling\n`monitor` with a `command` runs it exactly as `bash`/`bg_run` would. Enabling this extension adds a\nsecond arbitrary-command execution path that does not go through Bash's own allow/deny policy.\nPre-tool hooks still see the `monitor` tool call and can block it there, but there is no\nextension-level confirmation step in front of it.\n\n## Tests\n\n```\nnpm test\nnpm run test:integration\n```\n\nUnit coverage exercises line filtering, batching, WebSocket address validation, event and notice\nrendering, and the delivery scheduler with fake clocks and sources, without external I/O.\nIntegration tests spawn local processes and open loopback sockets to exercise the real source\nbehaviour without external network access.\n\nBugs caught by these tests and live smoke runs are recorded in [`docs/design.md`](docs/design.md),\nalong with why the scheduler defers a flush rather than sending output nothing is scheduled to\nread.\n","readmeFilename":"README.md"}