@nocville/npc 0.2.1 → 0.2.3
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/LICENSE +21 -0
- package/README.md +58 -120
- package/dist/cjs/cli.js +55 -34
- package/dist/cjs/core/base-npc.js +8 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/session-filter.js +3 -1
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/process-watcher/index.js +61 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +45 -19
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +56 -35
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +8 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/session-filter.d.ts +2 -2
- package/dist/core/session-filter.js +3 -1
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +62 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
- package/dist/npcs/process-watcher/process-scanner.js +61 -71
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +12 -2
- package/dist/npcs/receptionist/index.js +46 -20
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeff Marshall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ a hand when it needs you, dozes when idle, and shows what it's doing on its spee
|
|
|
7
7
|
desk sign: `Editing src/GameScene.ts`, `Running pnpm test`, `Searching for 'reconnect'`,
|
|
8
8
|
`Needs your approval`, `Thinking…`. It leaves the office when the session ends.
|
|
9
9
|
|
|
10
|
-
Requires Node.js 20+.
|
|
10
|
+
Requires Node.js 20+. Get an agent key in your Nocville office under **Connect AI coworkers**.
|
|
11
11
|
|
|
12
12
|
## Quick start: AI coworkers
|
|
13
13
|
|
|
@@ -16,23 +16,23 @@ npx @nocville/npc setup
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Paste your key at the prompt (input is hidden, so the key never lands in your shell history).
|
|
19
|
-
For scripts, pass `--api-key=<your
|
|
20
|
-
|
|
19
|
+
For scripts, pass `--api-key=<your key>` or set `NOCVILLE_API_KEY` instead. That's it. The
|
|
20
|
+
command saves your key to `~/.config/nocville/config.json` (readable only by you), then hooks
|
|
21
|
+
up whichever agents you have installed:
|
|
21
22
|
|
|
22
23
|
- **Claude Code**: adds hooks to `~/.claude/settings.json`. New sessions show up right away;
|
|
23
24
|
restart sessions that are already running.
|
|
24
25
|
- **Codex**: adds hooks to `~/.codex/hooks.json`. Codex won't run a new hook until you've
|
|
25
26
|
approved it, so start Codex once and run **`/hooks`** to trust the Nocville hooks.
|
|
27
|
+
- **Gemini CLI**: adds hooks to `~/.gemini/settings.json`, keeping your settings and existing
|
|
28
|
+
hooks. Start a new session after setup.
|
|
29
|
+
- **Cursor**: adds hooks to `~/.cursor/hooks.json`. Start a new chat after setup.
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
Your existing hooks are left alone, and Nocville's hooks never answer permission prompts or
|
|
32
|
+
change what your agent does; they only report activity.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
one coworker and deduplicate matching events. No permission decisions or model output are sent.
|
|
33
|
-
|
|
34
|
-
Self-hosting or developing locally? Add `--api-url=http://localhost:3601`. To send sessions to
|
|
35
|
-
a map other than your team's default, add `--map="Trading Floor"`.
|
|
34
|
+
Running your own Nocville server? Add `--api-url=https://your-server`. To send sessions to a
|
|
35
|
+
map other than your team's default, add `--map="Trading Floor"`.
|
|
36
36
|
|
|
37
37
|
Prefer a global install? Run `npm i -g @nocville/npc`, then `nocville-npc setup --api-key=…`.
|
|
38
38
|
When setup runs through `npx`, it copies itself to `~/.config/nocville/runtime/` so the hooks
|
|
@@ -71,26 +71,8 @@ checks the current folder (`--project=<dir>`, `--name=<session name>` to test ot
|
|
|
71
71
|
| `nocville-npc claude-code uninstall` | `nocville-npc codex uninstall` |
|
|
72
72
|
| | `nocville-npc codex watch` (no-hooks fallback) |
|
|
73
73
|
|
|
74
|
-
`nocville-npc gemini setup` and `nocville-npc
|
|
75
|
-
Gemini
|
|
76
|
-
Its Node process is identified by the Gemini executable path, not an unrelated Node parent.
|
|
77
|
-
|
|
78
|
-
`nocville-npc cursor setup` and `nocville-npc cursor uninstall` manage native Cursor hooks.
|
|
79
|
-
Native commands use 10-second timeouts. Setup installs only sessionStart/sessionEnd,
|
|
80
|
-
beforeSubmitPrompt, postToolUse/postToolUseFailure, afterFileEdit, stop, subagentStop and
|
|
81
|
-
preCompact; it removes older Nocville permission hooks while preserving other hooks.
|
|
82
|
-
Activity comes from completed tools. Native and imported Cursor invocations return exactly
|
|
83
|
-
`{}` with no decision fields. Payload fields identify imported Cursor; `CURSOR_VERSION`
|
|
84
|
-
alone never reclassifies Claude launched inside its terminal. The launcher suppresses child
|
|
85
|
-
stdout for every agent, including debug logs, and emits only the appropriate neutral response.
|
|
86
|
-
Cursor does not watch the editor process or start a watchdog: an abandoned conversation uses
|
|
87
|
-
the one-hour fallback heartbeat interval (unknown after two hours, despawn ten minutes later).
|
|
88
|
-
Uninstall intentionally leaves `{"version":1}` when no other settings remain.
|
|
89
|
-
|
|
90
|
-
Cursor's [documented tool names](https://cursor.com/docs/hooks#configuration) include Grep,
|
|
91
|
-
Delete, Task and MCP tools. Their summaries use the shared redaction/path handling; MCP names
|
|
92
|
-
are normalized to the shared MCP format. Real Cursor Shell/Read/Write verification with both
|
|
93
|
-
Claude-imported-only and native hooks is still required before a public support claim.
|
|
74
|
+
`nocville-npc gemini setup|uninstall` and `nocville-npc cursor setup|uninstall` manage
|
|
75
|
+
Gemini CLI and Cursor on their own. For Cursor, start a new chat after setup.
|
|
94
76
|
|
|
95
77
|
`nocville-npc uninstall` removes every Nocville hook. `nocville-npc init` only saves
|
|
96
78
|
credentials. Every setup command takes the key from `--api-key`, then `NOCVILLE_API_KEY`, then
|
|
@@ -111,58 +93,22 @@ are shortened to their first line, and anything that looks like a secret is reda
|
|
|
111
93
|
env assignments, `--token=`/`--password` flags, bearer headers, URL credentials, and long
|
|
112
94
|
key-like strings. Paths are shown relative to the project.
|
|
113
95
|
|
|
114
|
-
**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Re-running setup
|
|
129
|
-
|
|
130
|
-
`nocville-hook.cmd` launcher, which resolves the saved Node path and runs the same bootstrap.
|
|
131
|
-
Windows setup writes plain commands, never `-EncodedCommand` or an execution-policy override.
|
|
132
|
-
Re-running setup replaces the former encoded commands; uninstall still recognizes them.
|
|
133
|
-
|
|
134
|
-
| Agent | Windows command host and verification |
|
|
135
|
-
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
-
| Claude Code | Explicit per-hook `shell: "powershell"`; invokes `& '<shim>.cmd' claude-code hook`. Claude selects PowerShell 7 or falls back to 5.1. CI exercises both. [Claude hook shell reference](https://code.claude.com/docs/en/hooks#windows-powershell-tool). |
|
|
137
|
-
| Gemini | Its hook runner selects PowerShell 7 or 5.1. The saved `cmd.exe /d /s /c --% ...` preserves cmd quoting, with a terminating newline before Gemini's appended exit check. CI exercises both hosts with that check. [Hook reference](https://geminicli.com/docs/hooks/reference/), [runner](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/hooks/hookRunner.ts), [shell selection](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/shell-utils.ts). |
|
|
138
|
-
| Cursor | Explicit `cmd.exe /d /s /c` launcher; CI executes the saved string under cmd. The [published hook contract](https://cursor.com/docs/hooks) does not specify the Windows outer shell; real Windows Cursor verification remains required. |
|
|
139
|
-
| Codex | Explicit `cmd.exe /d /s /c` launcher; CI executes the saved string under cmd. The [published hook contract](https://learn.chatgpt.com/docs/hooks) does not identify the Windows outer shell; real Windows Codex verification remains required. |
|
|
140
|
-
|
|
141
|
-
PowerShell's [stop-parsing token](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#the-stop-parsing-token)
|
|
142
|
-
ends at a newline, not a semicolon. Keep Gemini's saved newline: its runner appends PowerShell
|
|
143
|
-
code to the command. No event input is interpolated into shell commands. Windows support remains
|
|
144
|
-
unverified until the Windows CI job and real-agent checks pass; macOS tests cannot establish it.
|
|
145
|
-
**Windows Cursor users:** install native hooks with `nocville-npc cursor setup`; do not rely on
|
|
146
|
-
Cursor importing Claude Code hooks. Claude's saved hook uses PowerShell syntax and an explicit
|
|
147
|
-
`shell` field. If Cursor's import path ignores that field and uses cmd, the imported command can
|
|
148
|
-
fail before the shim runs and produce hook errors. Nocville cannot control Cursor's import path.
|
|
149
|
-
|
|
150
|
-
Windows real-agent owner checklist (required before support claims):
|
|
151
|
-
|
|
152
|
-
- Run the native Windows CI job, including PowerShell 5.1/7 and a spaced `O’Neil` profile path.
|
|
153
|
-
- Verify native Cursor Shell/Read/Write events after `nocville-npc cursor setup`.
|
|
154
|
-
- With Claude hooks also installed, check whether Cursor imports them and honors their `shell`
|
|
155
|
-
field. Record agent versions and any duplicate/error events; verify native Cursor hooks work
|
|
156
|
-
independently. Do not count an imported Claude hook as proof of native Cursor support.
|
|
157
|
-
- Verify the actual Claude, Gemini and Codex hosts using their installed saved commands.
|
|
158
|
-
|
|
159
|
-
Claude's single-quoted path doubles straight and curly apostrophes according to
|
|
160
|
-
[PowerShell quoting rules](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#including-quote-characters-in-a-string).
|
|
161
|
-
|
|
162
|
-
Both launchers discard child stdout and preserve stderr; all SDK logger levels use stderr.
|
|
163
|
-
If Node is unavailable, the shim drains stdin and returns neutral `{}` for Cursor/Gemini
|
|
164
|
-
and Claude hooks (which may have been imported by Cursor); Codex stays silent. Session records are local, including terminal
|
|
165
|
-
records that prevent late events from reviving an ended NPC; `cleanup` removes old records.
|
|
96
|
+
**Overhead:** hooks return immediately and send updates from a background process, so your
|
|
97
|
+
agent never waits on the network. Rapid bursts of activity are combined into one update.
|
|
98
|
+
|
|
99
|
+
If an agent is killed, its coworker leaves the office within about 15 seconds. If your machine
|
|
100
|
+
sleeps or goes offline, the coworker greys out after a few minutes and leaves after about
|
|
101
|
+
thirteen. Claude subagent activity appears below the parent's current activity.
|
|
102
|
+
|
|
103
|
+
### Windows
|
|
104
|
+
|
|
105
|
+
Setup works the same way on Windows; it writes a small `nocville-hook.cmd` launcher to
|
|
106
|
+
`~/.config/nocville/bin/` and points each agent's hooks at it. **Cursor users on Windows:**
|
|
107
|
+
install Cursor's own hooks with `nocville-npc cursor setup` rather than relying on Cursor
|
|
108
|
+
importing your Claude Code hooks.
|
|
109
|
+
|
|
110
|
+
Re-running `setup` (for example after upgrading with `npx @nocville/npc@latest setup`) keeps
|
|
111
|
+
your hooks in place and switches them to the new version; running sessions are not interrupted.
|
|
166
112
|
|
|
167
113
|
### How the Codex integration works
|
|
168
114
|
|
|
@@ -175,9 +121,8 @@ hooks, makes a one-time `hooks.json.nocville-backup`, and is safe to run again.
|
|
|
175
121
|
reads the rollout logs Codex writes to `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. It
|
|
176
122
|
shows one NPC per interactive session (subagent and reviewer threads are skipped; add
|
|
177
123
|
`--include-exec` to include `codex exec` runs). A session leaves the office after 30 minutes
|
|
178
|
-
without log activity (`--idle-timeout=<minutes>`) or when you press Ctrl-C.
|
|
179
|
-
|
|
180
|
-
leaves that session to them. `doctor` warns when both modes are running.
|
|
124
|
+
without log activity (`--idle-timeout=<minutes>`) or when you press Ctrl-C. Sessions that
|
|
125
|
+
hooks already report are left to the hooks.
|
|
181
126
|
|
|
182
127
|
## Monitoring NPCs
|
|
183
128
|
|
|
@@ -205,16 +150,34 @@ on your map automatically and despawns when you press Ctrl-C. Once you've run
|
|
|
205
150
|
| `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
|
|
206
151
|
| `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
|
|
207
152
|
|
|
208
|
-
Run `nocville-npc --help` to see every flag.
|
|
209
|
-
|
|
153
|
+
Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
|
|
154
|
+
seconds. A few notes:
|
|
155
|
+
|
|
156
|
+
- **system-monitor**: `--disk-path=/data` picks the volume; thresholds are percentages
|
|
157
|
+
(`--cpu-warn`/`--cpu-crit`, `--mem-…`, `--disk-…`). On macOS, memory counts the file cache as
|
|
158
|
+
available, like Activity Monitor.
|
|
159
|
+
- **internet-connectivity**: `--dns-hosts`, `--http-urls` (http/https only) and
|
|
160
|
+
`--latency-hosts` take comma-separated lists; latency targets accept `host:port` or
|
|
161
|
+
`[ipv6]:port`. `--timeout=<ms>` bounds each check (default 5000).
|
|
162
|
+
- **process-watcher**: matches executable names, so `--processes=python` also finds `python3.11`
|
|
163
|
+
but `sh` does not match `bash`. Use a full path to match one binary exactly. macOS and Linux
|
|
164
|
+
only.
|
|
210
165
|
- **log-watcher**: `--error-pattern="ERROR|FATAL" --window=300 --error-warn=5 --error-crit=20`.
|
|
166
|
+
Files are followed across log rotation and may be created after the watcher starts. Lines
|
|
167
|
+
shown in the office have anything that looks like a secret (tokens, passwords, keys, URL
|
|
168
|
+
credentials) redacted, but treat the screen as visible to your whole team.
|
|
211
169
|
- **receptionist**: weather comes from the keyless open-meteo API. Set the place with
|
|
212
|
-
`--lat=52.5 --lon=13.4` or `--location
|
|
170
|
+
`--lat=52.5 --lon=13.4` or `--location=…`. Without either, it estimates your location from
|
|
171
|
+
your public IP via ipapi.co; pass `--no-ip-geo` to turn that off and show only the time. Add
|
|
213
172
|
`--fahrenheit` for °F.
|
|
214
|
-
- **github-watcher**: uses ETag polling.
|
|
173
|
+
- **github-watcher**: uses ETag polling. Set `GITHUB_TOKEN` for private repos and a higher rate
|
|
174
|
+
limit (`--token` also works, but flags are visible to other users in the process list).
|
|
175
|
+
Without a token it polls every 90 seconds to stay within GitHub's 60 requests per hour.
|
|
215
176
|
- **web-page-watcher**: the pixel-art TV needs `npm i puppeteer-core` and a local
|
|
216
177
|
Chrome/Chromium (`--chrome-path` or `CHROME_PATH`). Without them it falls back to a
|
|
217
|
-
title/status ticker. Other options: `--mode=auto|screenshot|basic` and `--screen=160x120
|
|
178
|
+
title/status ticker. Other options: `--mode=auto|screenshot|basic` and `--screen=160x120`
|
|
179
|
+
(up to 320x240). Only `http(s)` URLs are accepted. Chrome runs with its sandbox unless you're
|
|
180
|
+
root or pass `--no-chrome-sandbox`; only disable it for pages you trust.
|
|
218
181
|
|
|
219
182
|
## Programmatic use
|
|
220
183
|
|
|
@@ -224,7 +187,7 @@ import type { CollectedMetrics } from '@nocville/npc';
|
|
|
224
187
|
|
|
225
188
|
await new SystemMonitorNPC({
|
|
226
189
|
apiUrl: 'https://nocville.com',
|
|
227
|
-
apiKey: '
|
|
190
|
+
apiKey: 'nvk_…',
|
|
228
191
|
serviceId: 'web-1',
|
|
229
192
|
}).start();
|
|
230
193
|
|
|
@@ -238,32 +201,7 @@ class MyNPC extends PollingNPC {
|
|
|
238
201
|
}
|
|
239
202
|
```
|
|
240
203
|
|
|
241
|
-
##
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
pnpm install
|
|
245
|
-
pnpm --filter @nocville/npc build
|
|
246
|
-
pnpm --filter @nocville/npc test
|
|
247
|
-
node libs/nocville-npc-library/dist/cli.js --help
|
|
248
|
-
SMOKE_STUB=1 pnpm --filter @nocville/npc smoke # every payload shape, no server needed
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
**Watch an AI-coworker NPC without running an agent.** `simulate` plays a scripted session
|
|
252
|
-
(start → prompt → Read → Edit → `pnpm test` → approval wait → idle → end) through the real
|
|
253
|
-
built hook handlers. It works in a throwaway HOME, so your `~/.claude` and `~/.codex` are
|
|
254
|
-
untouched, and the NPC appears as "Claude · demo" (or "Codex · demo"):
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
pnpm --filter @nocville/npc simulate claude --api-key=<key> --api-url=http://localhost:3601
|
|
258
|
-
pnpm --filter @nocville/npc simulate codex --api-key=<key> --project=gridbreak --fast
|
|
259
|
-
# --hold stay idle after Stop until Ctrl-C, then end the session
|
|
260
|
-
# --fast 5x shorter pauses (CI)
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**Real hooks against your local checkout.** This writes real hooks into `~/.claude` and
|
|
264
|
-
`~/.codex` that run your working copy's `dist/cli.js`:
|
|
204
|
+
## Help
|
|
265
205
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
node libs/nocville-npc-library/dist/cli.js uninstall # remove them again
|
|
269
|
-
```
|
|
206
|
+
`nocville-npc --help` lists every command and flag, and `nocville-npc doctor` checks that your
|
|
207
|
+
key and connection work. More at [nocville.com](https://nocville.com).
|
package/dist/cjs/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const node_fs_1 = require("node:fs");
|
|
|
38
38
|
const node_os_1 = require("node:os");
|
|
39
39
|
const node_path_1 = require("node:path");
|
|
40
40
|
const config_js_1 = require("./core/config.js");
|
|
41
|
+
const errors_js_1 = require("./core/errors.js");
|
|
41
42
|
const logger_js_1 = require("./core/logger.js");
|
|
42
43
|
const session_filter_js_1 = require("./core/session-filter.js");
|
|
43
44
|
// NPC modules are imported lazily per command: `claude-code hook` / `codex hook`
|
|
@@ -112,17 +113,23 @@ Internet Connectivity Options:
|
|
|
112
113
|
--poll-interval=<ms> Poll interval in ms (default: 30000)
|
|
113
114
|
--dns-hosts=<list> Comma-separated DNS hosts (default: google.com,cloudflare.com)
|
|
114
115
|
--http-urls=<list> Comma-separated HTTP URLs to check
|
|
115
|
-
--latency-hosts=<list> Comma-separated
|
|
116
|
+
--latency-hosts=<list> Comma-separated TCP latency targets: host, host:port or
|
|
117
|
+
[ipv6]:port (default: 8.8.8.8:443,1.1.1.1:443;
|
|
118
|
+
port 80 when omitted)
|
|
116
119
|
--slow-threshold=<ms> Slow response threshold in ms (default: 1000)
|
|
120
|
+
--timeout=<ms> Per-check timeout in ms (default: 5000)
|
|
117
121
|
|
|
118
122
|
Process Watcher Options:
|
|
119
123
|
--poll-interval=<ms> Poll interval in ms (default: 15000)
|
|
120
|
-
--processes=<list> Comma-separated
|
|
124
|
+
--processes=<list> Comma-separated executable names to watch (python
|
|
125
|
+
also matches python3.11; a /path matches exactly)
|
|
121
126
|
--cpu-warn=<n> CPU warning threshold % (default: 80)
|
|
122
127
|
--mem-warn=<n> Memory warning threshold MB (default: 500)
|
|
123
128
|
|
|
124
129
|
Log Watcher Options:
|
|
125
|
-
--files=<list> Comma-separated log file paths
|
|
130
|
+
--files=<list> Comma-separated log file paths (followed across
|
|
131
|
+
rotation; files may appear later). Lines shown on
|
|
132
|
+
the screen have secrets redacted.
|
|
126
133
|
--error-pattern=<regex> Error pattern (default: ERROR|FATAL)
|
|
127
134
|
--warning-pattern=<regex> Warning pattern (default: WARN)
|
|
128
135
|
--window=<seconds> Sliding window in seconds (default: 300)
|
|
@@ -133,23 +140,30 @@ Log Watcher Options:
|
|
|
133
140
|
Receptionist Options:
|
|
134
141
|
--poll-interval=<ms> Clock tick in ms (default: 60000)
|
|
135
142
|
--lat=<n> --lon=<n> Explicit coordinates for weather
|
|
136
|
-
--location=<name> Place name to geocode (
|
|
143
|
+
--location=<name> Place name to geocode (open-meteo)
|
|
144
|
+
--no-ip-geo Without --lat/--lon or --location, don't look up
|
|
145
|
+
this machine's IP location (ipapi.co); show time only
|
|
137
146
|
--fahrenheit Show temperature in °F (default: °C)
|
|
138
147
|
|
|
139
148
|
GitHub Watcher Options:
|
|
140
149
|
--repo=<owner/name> Repository to watch (required)
|
|
141
150
|
--branch=<name> Branch to watch (default: repo default branch)
|
|
142
|
-
--token=<token> GitHub token (
|
|
143
|
-
|
|
151
|
+
--token=<token> GitHub token (prefer the GITHUB_TOKEN env: flags are
|
|
152
|
+
visible in the process list)
|
|
153
|
+
--poll-interval=<ms> Poll interval in ms (default: 60000 with a token,
|
|
154
|
+
90000 without, to stay under GitHub's 60/hour)
|
|
144
155
|
|
|
145
156
|
Web Page Watcher Options:
|
|
146
|
-
--url=<url>
|
|
157
|
+
--url=<url> http(s) page to watch (required)
|
|
147
158
|
--mode=<auto|screenshot|basic> Render mode (default: auto)
|
|
148
159
|
--interval=<ms> Poll interval in ms (default: 60000, floor 15000)
|
|
149
|
-
--viewport=<WxH> Browser viewport (default: screen x 8)
|
|
150
|
-
--screen=<WxH> TV pixel resolution (default: 160x120
|
|
151
|
-
--wait-until=<event>
|
|
160
|
+
--viewport=<WxH> Browser viewport (default: screen x 8, max 4096x4096)
|
|
161
|
+
--screen=<WxH> TV pixel resolution (default: 160x120, max 320x240)
|
|
162
|
+
--wait-until=<event> load, domcontentloaded, networkidle0 or networkidle2
|
|
163
|
+
(default: networkidle2)
|
|
152
164
|
--chrome-path=<path> Chrome executable (or CHROME_PATH env)
|
|
165
|
+
--no-chrome-sandbox Run Chrome without its sandbox (only for trusted
|
|
166
|
+
pages; automatic when running as root)
|
|
153
167
|
(needs puppeteer-core + Chrome for the pixel-art TV; falls back to basic)
|
|
154
168
|
`.trim();
|
|
155
169
|
async function main() {
|
|
@@ -310,18 +324,10 @@ async function main() {
|
|
|
310
324
|
if (!config.serviceId) {
|
|
311
325
|
config.serviceId = 'internet-connectivity';
|
|
312
326
|
}
|
|
313
|
-
const dnsHosts = (0, config_js_1.stringFlag)(parsed.flags, 'dns-hosts')?.split(',');
|
|
314
|
-
const httpUrls = (0, config_js_1.stringFlag)(parsed.flags, 'http-urls')?.split(',');
|
|
315
|
-
const latencyHostsRaw = (0, config_js_1.stringFlag)(parsed.flags, 'latency-hosts')?.split(',');
|
|
316
|
-
const latencyHosts = latencyHostsRaw?.map((h) => ({ host: h.trim(), port: 80 }));
|
|
317
327
|
const { InternetConnectivityNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/internet-connectivity/index.js')));
|
|
318
328
|
const npc = new InternetConnectivityNPC({
|
|
319
329
|
...config,
|
|
320
|
-
|
|
321
|
-
dnsHosts,
|
|
322
|
-
httpUrls,
|
|
323
|
-
latencyHosts,
|
|
324
|
-
slowThreshold: (0, config_js_1.numberFlag)(parsed.flags, 'slow-threshold'),
|
|
330
|
+
...InternetConnectivityNPC.configFromFlags(parsed.flags),
|
|
325
331
|
});
|
|
326
332
|
logger.info('Starting internet-connectivity NPC');
|
|
327
333
|
await npc.start();
|
|
@@ -331,8 +337,8 @@ async function main() {
|
|
|
331
337
|
if (!config.serviceId) {
|
|
332
338
|
config.serviceId = 'process-watcher';
|
|
333
339
|
}
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
340
|
+
const processes = (0, config_js_1.listFlag)(parsed.flags, 'processes');
|
|
341
|
+
if (!processes) {
|
|
336
342
|
console.error('Error: --processes is required (comma-separated list)');
|
|
337
343
|
process.exit(1);
|
|
338
344
|
}
|
|
@@ -340,7 +346,7 @@ async function main() {
|
|
|
340
346
|
const npc = new ProcessWatcherNPC({
|
|
341
347
|
...config,
|
|
342
348
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
343
|
-
processes
|
|
349
|
+
processes,
|
|
344
350
|
cpuWarn: (0, config_js_1.numberFlag)(parsed.flags, 'cpu-warn'),
|
|
345
351
|
memWarn: (0, config_js_1.numberFlag)(parsed.flags, 'mem-warn'),
|
|
346
352
|
});
|
|
@@ -349,8 +355,8 @@ async function main() {
|
|
|
349
355
|
break;
|
|
350
356
|
}
|
|
351
357
|
case 'log-watcher': {
|
|
352
|
-
const
|
|
353
|
-
if (!
|
|
358
|
+
const files = (0, config_js_1.listFlag)(parsed.flags, 'files');
|
|
359
|
+
if (!files) {
|
|
354
360
|
console.error('Error: --files is required (comma-separated list)');
|
|
355
361
|
process.exit(1);
|
|
356
362
|
}
|
|
@@ -360,7 +366,7 @@ async function main() {
|
|
|
360
366
|
const { LogWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/log-watcher/index.js')));
|
|
361
367
|
const npc = new LogWatcherNPC({
|
|
362
368
|
...config,
|
|
363
|
-
files
|
|
369
|
+
files,
|
|
364
370
|
errorPattern: (0, config_js_1.stringFlag)(parsed.flags, 'error-pattern'),
|
|
365
371
|
warningPattern: (0, config_js_1.stringFlag)(parsed.flags, 'warning-pattern'),
|
|
366
372
|
windowSeconds: (0, config_js_1.numberFlag)(parsed.flags, 'window'),
|
|
@@ -384,6 +390,7 @@ async function main() {
|
|
|
384
390
|
lon: (0, config_js_1.numberFlag)(parsed.flags, 'lon'),
|
|
385
391
|
location: (0, config_js_1.stringFlag)(parsed.flags, 'location'),
|
|
386
392
|
fahrenheit: (0, config_js_1.boolFlag)(parsed.flags, 'fahrenheit') ?? false,
|
|
393
|
+
ipGeo: (0, config_js_1.boolFlag)(parsed.flags, 'no-ip-geo') !== true,
|
|
387
394
|
});
|
|
388
395
|
logger.info('Starting receptionist NPC');
|
|
389
396
|
await npc.start();
|
|
@@ -400,11 +407,15 @@ async function main() {
|
|
|
400
407
|
config.serviceId = `github-${shortName.replace(/[^a-zA-Z0-9_-]/g, '-')}`.slice(0, 100);
|
|
401
408
|
}
|
|
402
409
|
const { GitHubWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/github-watcher/index.js')));
|
|
410
|
+
const tokenFlag = (0, config_js_1.stringFlag)(parsed.flags, 'token');
|
|
411
|
+
if (tokenFlag) {
|
|
412
|
+
logger.warn('--token is visible to other local users in the process list; prefer GITHUB_TOKEN');
|
|
413
|
+
}
|
|
403
414
|
const npc = new GitHubWatcherNPC({
|
|
404
415
|
...config,
|
|
405
416
|
repo,
|
|
406
417
|
branch: (0, config_js_1.stringFlag)(parsed.flags, 'branch'),
|
|
407
|
-
token:
|
|
418
|
+
token: tokenFlag ?? process.env['GITHUB_TOKEN'],
|
|
408
419
|
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
409
420
|
});
|
|
410
421
|
logger.info('Starting github-watcher NPC');
|
|
@@ -420,22 +431,28 @@ async function main() {
|
|
|
420
431
|
const { hostnameOf } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/basic-mode.js')));
|
|
421
432
|
const { parseDimensions } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/defaults.js')));
|
|
422
433
|
const { WebPageWatcherNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/web-page-watcher/index.js')));
|
|
423
|
-
const parseWxH = (
|
|
434
|
+
const parseWxH = (flag) => {
|
|
435
|
+
const v = (0, config_js_1.stringFlag)(parsed.flags, flag);
|
|
436
|
+
if (v === undefined)
|
|
437
|
+
return undefined;
|
|
438
|
+
const dims = parseDimensions(v);
|
|
439
|
+
if (!dims)
|
|
440
|
+
throw new errors_js_1.NpcConfigError(`--${flag} must look like 160x120`);
|
|
441
|
+
return dims;
|
|
442
|
+
};
|
|
424
443
|
if (!config.serviceId) {
|
|
425
444
|
config.serviceId = `webpage-${hostnameOf(url)}`
|
|
426
445
|
.replace(/[^a-zA-Z0-9_.-]/g, '-')
|
|
427
446
|
.slice(0, 100);
|
|
428
447
|
}
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
? modeFlag
|
|
432
|
-
: undefined;
|
|
433
|
-
const viewport = parseWxH((0, config_js_1.stringFlag)(parsed.flags, 'viewport'));
|
|
434
|
-
const screen = parseWxH((0, config_js_1.stringFlag)(parsed.flags, 'screen'));
|
|
448
|
+
const viewport = parseWxH('viewport');
|
|
449
|
+
const screen = parseWxH('screen');
|
|
435
450
|
const npc = new WebPageWatcherNPC({
|
|
436
451
|
...config,
|
|
437
452
|
url,
|
|
438
|
-
|
|
453
|
+
// Validated (with a clear error) by the constructor.
|
|
454
|
+
mode: (0, config_js_1.stringFlag)(parsed.flags, 'mode'),
|
|
455
|
+
chromeSandbox: (0, config_js_1.boolFlag)(parsed.flags, 'no-chrome-sandbox') !== true,
|
|
439
456
|
chromePath: (0, config_js_1.stringFlag)(parsed.flags, 'chrome-path'),
|
|
440
457
|
waitUntil: (0, config_js_1.stringFlag)(parsed.flags, 'wait-until'),
|
|
441
458
|
viewportWidth: viewport?.width,
|
|
@@ -578,6 +595,10 @@ main().catch((err) => {
|
|
|
578
595
|
process.stdout.write('{}\n');
|
|
579
596
|
return;
|
|
580
597
|
}
|
|
598
|
+
if (err instanceof errors_js_1.NpcConfigError) {
|
|
599
|
+
console.error(`Error: ${err.message}`);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
581
602
|
console.error('Fatal error:', err);
|
|
582
603
|
process.exit(1);
|
|
583
604
|
});
|
|
@@ -42,6 +42,13 @@ class BaseNPC {
|
|
|
42
42
|
const name = this.displayName;
|
|
43
43
|
return name ? (0, text_js_1.truncate)(name, 32) : undefined;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* NPC type sent as the push `kind` (groups NPCs for per-type rules such as "all process
|
|
47
|
+
* watchers work in the Server Room"). Defaults to `name` when it is a valid slug.
|
|
48
|
+
*/
|
|
49
|
+
get kind() {
|
|
50
|
+
return /^[a-z0-9][a-z0-9-]{0,39}$/.test(this.name) ? this.name : undefined;
|
|
51
|
+
}
|
|
45
52
|
/** NPC archetype — steers movement speed and default behavior. Override per NPC. */
|
|
46
53
|
get archetype() {
|
|
47
54
|
return 'service';
|
|
@@ -116,6 +123,7 @@ class BaseNPC {
|
|
|
116
123
|
message: metrics.message ? (0, text_js_1.truncate)(metrics.message, 500) : undefined,
|
|
117
124
|
metrics: metrics.metrics,
|
|
118
125
|
archetype: this.archetype,
|
|
126
|
+
kind: this.kind,
|
|
119
127
|
subStatus: metrics.subStatus,
|
|
120
128
|
screenData: metrics.screenData,
|
|
121
129
|
behavior: Object.keys(behavior).length > 0 ? behavior : undefined,
|
package/dist/cjs/core/config.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.parseArgs = parseArgs;
|
|
|
4
4
|
exports.resolveConfig = resolveConfig;
|
|
5
5
|
exports.stringFlag = stringFlag;
|
|
6
6
|
exports.boolFlag = boolFlag;
|
|
7
|
+
exports.listFlag = listFlag;
|
|
7
8
|
exports.numberFlag = numberFlag;
|
|
8
9
|
const user_config_js_1 = require("./user-config.js");
|
|
9
10
|
/**
|
|
@@ -96,11 +97,27 @@ function boolFlag(flags, key) {
|
|
|
96
97
|
return false;
|
|
97
98
|
return undefined;
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* A comma-separated list flag (`--processes=nginx,redis`): entries trimmed,
|
|
102
|
+
* empties dropped, duplicates removed. Undefined when the flag is absent or empty.
|
|
103
|
+
*/
|
|
104
|
+
function listFlag(flags, key) {
|
|
105
|
+
const raw = stringFlag(flags, key);
|
|
106
|
+
if (raw === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const items = [
|
|
109
|
+
...new Set(raw
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((s) => s.trim())
|
|
112
|
+
.filter(Boolean)),
|
|
113
|
+
];
|
|
114
|
+
return items.length > 0 ? items : undefined;
|
|
115
|
+
}
|
|
99
116
|
function numberFlag(flags, key) {
|
|
100
117
|
const val = flags[key];
|
|
101
|
-
if (typeof val === 'string') {
|
|
118
|
+
if (typeof val === 'string' && val.trim() !== '') {
|
|
102
119
|
const n = Number(val);
|
|
103
|
-
if (
|
|
120
|
+
if (Number.isFinite(n))
|
|
104
121
|
return n;
|
|
105
122
|
}
|
|
106
123
|
return undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NpcConfigError = void 0;
|
|
4
|
+
exports.assertPosix = assertPosix;
|
|
5
|
+
/**
|
|
6
|
+
* A user-facing configuration problem (bad flag value, unsupported platform).
|
|
7
|
+
* The CLI prints only its message and exits 1 — no stack trace — so throw it
|
|
8
|
+
* for anything the user can fix by changing how they run the NPC.
|
|
9
|
+
*/
|
|
10
|
+
class NpcConfigError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'NpcConfigError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.NpcConfigError = NpcConfigError;
|
|
17
|
+
/** Throw an NpcConfigError on Windows, for NPCs that rely on POSIX tools or paths. */
|
|
18
|
+
function assertPosix(npcName, hint) {
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
throw new NpcConfigError(`${npcName} is not supported on Windows. ${hint}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=errors.js.map
|