@nocville/npc 0.2.3 → 0.3.1
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/README.md +36 -15
- package/dist/cjs/cli.js +44 -8
- package/dist/cjs/core/base-npc.js +7 -0
- package/dist/cjs/core/polling-npc.js +8 -0
- package/dist/cjs/index.js +13 -1
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- package/dist/cjs/npcs/github-watcher/github.js +109 -0
- package/dist/cjs/npcs/office-characters/index.js +102 -0
- package/dist/cjs/npcs/process-watcher/index.js +0 -1
- package/dist/cjs/npcs/process-watcher/process-scanner.js +42 -4
- package/dist/cjs/npcs/receptionist/index.js +6 -119
- package/dist/cjs/npcs/security/index.js +26 -0
- package/dist/cjs/npcs/web-page-watcher/basic.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +26 -0
- package/dist/cjs/npcs/web-page-watcher/chrome.js +62 -0
- package/dist/cli.js +44 -8
- package/dist/core/base-npc.js +7 -0
- package/dist/core/polling-npc.d.ts +3 -0
- package/dist/core/polling-npc.js +8 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- package/dist/npcs/github-watcher/github.d.ts +39 -0
- package/dist/npcs/github-watcher/github.js +103 -0
- package/dist/npcs/office-characters/index.d.ts +55 -0
- package/dist/npcs/office-characters/index.js +91 -0
- package/dist/npcs/process-watcher/index.js +1 -2
- package/dist/npcs/process-watcher/process-scanner.d.ts +3 -1
- package/dist/npcs/process-watcher/process-scanner.js +41 -4
- package/dist/npcs/receptionist/index.d.ts +2 -21
- package/dist/npcs/receptionist/index.js +6 -119
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- package/dist/npcs/web-page-watcher/basic.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic.js +52 -0
- package/dist/npcs/web-page-watcher/capture.js +26 -0
- package/dist/npcs/web-page-watcher/chrome.d.ts +46 -0
- package/dist/npcs/web-page-watcher/chrome.js +57 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -140,15 +140,24 @@ on your map automatically and despawns when you press Ctrl-C. Once you've run
|
|
|
140
140
|
# --debug Verbose logging
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
| NPC
|
|
144
|
-
|
|
|
145
|
-
| `system-monitor`
|
|
146
|
-
| `internet-connectivity`
|
|
147
|
-
| `process-watcher`
|
|
148
|
-
| `log-watcher`
|
|
149
|
-
| `receptionist`
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
143
|
+
| NPC | What it does | Example |
|
|
144
|
+
| --------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ |
|
|
145
|
+
| `system-monitor` | CPU / memory / disk / load with sparklines | `nocville-npc system-monitor --cpu-warn=75` |
|
|
146
|
+
| `internet-connectivity` | DNS, HTTP and TCP latency checks | `nocville-npc internet-connectivity` |
|
|
147
|
+
| `process-watcher` | Watches named processes, calls out deaths | `nocville-npc process-watcher --processes=nginx,redis` |
|
|
148
|
+
| `log-watcher` | Error bursts in log files, last lines on a monitor | `nocville-npc log-watcher --files=/var/log/app.log` |
|
|
149
|
+
| `receptionist` | Lobby greetings, check-ins and front desk jokes | `nocville-npc receptionist` |
|
|
150
|
+
| `security` | Indoor/outdoor patrols and exterior door watch | `nocville-npc security` |
|
|
151
|
+
| `janitor` | Mops suspicious spots and complains about bugs everywhere. | `nocville-npc janitor` |
|
|
152
|
+
| `barista` | Your coffee order is getting a little out of hand. | `nocville-npc barista` |
|
|
153
|
+
| `office-cat` | Your desk belongs to the cat now. | `nocville-npc office-cat` |
|
|
154
|
+
| `office-dog` | Chief morale officer. Easily distracted. | `nocville-npc office-dog` |
|
|
155
|
+
| `plant-caretaker` | Every fern deserves a performance review. | `nocville-npc plant-caretaker` |
|
|
156
|
+
| `it-technician` | Have you tried turning it off and on again? | `nocville-npc it-technician` |
|
|
157
|
+
| `out-of-touch-executive` | Big ideas. Long coffee breaks. Your overtime. | `nocville-npc out-of-touch-executive` |
|
|
158
|
+
| `slightly-inappropriate-hr` | People skills. Questionable discretion. | `nocville-npc slightly-inappropriate-hr` |
|
|
159
|
+
| `github-watcher` | New commits on a commit board | `nocville-npc github-watcher --repo=owner/name` |
|
|
160
|
+
| `web-page-watcher` | A web page rendered as a pixel-art TV | `nocville-npc web-page-watcher --url=https://…` |
|
|
152
161
|
|
|
153
162
|
Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
|
|
154
163
|
seconds. A few notes:
|
|
@@ -160,16 +169,28 @@ seconds. A few notes:
|
|
|
160
169
|
`--latency-hosts` take comma-separated lists; latency targets accept `host:port` or
|
|
161
170
|
`[ipv6]:port`. `--timeout=<ms>` bounds each check (default 5000).
|
|
162
171
|
- **process-watcher**: matches executable names, so `--processes=python` also finds `python3.11`
|
|
163
|
-
but `sh` does not match `bash`.
|
|
164
|
-
|
|
172
|
+
but `sh` does not match `bash`. Works on macOS and Linux via `ps`, and Windows via
|
|
173
|
+
PowerShell/CIM performance counters. Windows accepts names with or without `.exe` and
|
|
174
|
+
reports CPU as a percentage of total machine capacity. Full-path matching is available
|
|
175
|
+
where the OS reports paths (macOS); use executable names on Windows and Linux.
|
|
165
176
|
- **log-watcher**: `--error-pattern="ERROR|FATAL" --window=300 --error-warn=5 --error-crit=20`.
|
|
166
177
|
Files are followed across log rotation and may be created after the watcher starts. Lines
|
|
167
178
|
shown in the office have anything that looks like a secret (tokens, passwords, keys, URL
|
|
168
179
|
credentials) redacted, but treat the screen as visible to your whole team.
|
|
169
|
-
- **receptionist**:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
- **receptionist**: claims a free desk in a room named Lobby, Reception, Foyer or Entrance
|
|
181
|
+
(or its assigned work room). Without a desk, uses an open spot. Greets each arrival,
|
|
182
|
+
checks on a teammate every 2–4 minutes and makes a desk-side joke every 1–2 minutes.
|
|
183
|
+
No screen, weather requests or geolocation; old weather options are accepted but ignored.
|
|
184
|
+
- **security**: continuously visits rooms and reachable outdoor areas, pausing beside
|
|
185
|
+
exterior doors for 15–30 seconds. Does not claim a desk. An explicit work-room rule
|
|
186
|
+
still confines its route, so leave it unassigned for office-wide rounds.
|
|
187
|
+
- **office characters**: `janitor`, `barista`, `office-cat`, `office-dog`,
|
|
188
|
+
`plant-caretaker`, `it-technician`, `out-of-touch-executive`, and `slightly-inappropriate-hr` run without screens or
|
|
189
|
+
external service credentials. They share the usual API key, name, service ID and map
|
|
190
|
+
options. Run each as its own process. Their routes and conversation are synchronized by
|
|
191
|
+
the server, and work-room rules apply. Missing furniture or coworkers cause quiet walks
|
|
192
|
+
or floor rests instead. The technician's reboots and executive's requests are pretend.
|
|
193
|
+
Animals have custom walk, sleep and tail animations; staff carry their tools or coffee.
|
|
173
194
|
- **github-watcher**: uses ETag polling. Set `GITHUB_TOKEN` for private repos and a higher rate
|
|
174
195
|
limit (`--token` also works, but flags are visible to other users in the process list).
|
|
175
196
|
Without a token it polls every 90 seconds to stay within GitHub's 60 requests per hour.
|
package/dist/cjs/cli.js
CHANGED
|
@@ -82,7 +82,16 @@ Monitoring NPC types:
|
|
|
82
82
|
internet-connectivity Monitor network connectivity
|
|
83
83
|
process-watcher Watch for running processes
|
|
84
84
|
log-watcher Watch log files for errors
|
|
85
|
-
receptionist
|
|
85
|
+
receptionist Lobby greeter with office check-ins and jokes
|
|
86
|
+
security Indoor/outdoor patrols and exterior door watch
|
|
87
|
+
janitor Mops suspicious spots and hunts office bugs
|
|
88
|
+
barista Takes increasingly absurd coffee orders
|
|
89
|
+
office-cat Naps on desks and supervises keyboards
|
|
90
|
+
office-dog Naps, barks at coworkers and runs around
|
|
91
|
+
plant-caretaker Waters plants and reviews their performance
|
|
92
|
+
it-technician Inspects tech gear and recommends reboots
|
|
93
|
+
out-of-touch-executive Interrupts coworkers between long lounge breaks
|
|
94
|
+
slightly-inappropriate-hr Desk-based HR with questionable people skills
|
|
86
95
|
github-watcher Call out new commits on a repo
|
|
87
96
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
88
97
|
|
|
@@ -137,13 +146,8 @@ Log Watcher Options:
|
|
|
137
146
|
--error-crit=<n> Error count for critical status (default: 20)
|
|
138
147
|
--report-interval=<ms> Report interval in ms (default: 15000)
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
--poll-interval=<ms>
|
|
142
|
-
--lat=<n> --lon=<n> Explicit coordinates for weather
|
|
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
|
|
146
|
-
--fahrenheit Show temperature in °F (default: °C)
|
|
149
|
+
Office Character Options:
|
|
150
|
+
--poll-interval=<ms> Heartbeat interval in ms (default: 60000)
|
|
147
151
|
|
|
148
152
|
GitHub Watcher Options:
|
|
149
153
|
--repo=<owner/name> Repository to watch (required)
|
|
@@ -396,6 +400,38 @@ async function main() {
|
|
|
396
400
|
await npc.start();
|
|
397
401
|
break;
|
|
398
402
|
}
|
|
403
|
+
case 'security': {
|
|
404
|
+
if (!config.serviceId)
|
|
405
|
+
config.serviceId = 'security';
|
|
406
|
+
const { SecurityNPC } = await Promise.resolve().then(() => __importStar(require('./npcs/security/index.js')));
|
|
407
|
+
const npc = new SecurityNPC({
|
|
408
|
+
...config,
|
|
409
|
+
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
410
|
+
});
|
|
411
|
+
logger.info('Starting security NPC');
|
|
412
|
+
await npc.start();
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case 'janitor':
|
|
416
|
+
case 'barista':
|
|
417
|
+
case 'office-cat':
|
|
418
|
+
case 'office-dog':
|
|
419
|
+
case 'plant-caretaker':
|
|
420
|
+
case 'it-technician':
|
|
421
|
+
case 'out-of-touch-executive':
|
|
422
|
+
case 'slightly-inappropriate-hr': {
|
|
423
|
+
if (!config.serviceId)
|
|
424
|
+
config.serviceId = parsed.command;
|
|
425
|
+
const { OFFICE_CHARACTER_NPCS } = await Promise.resolve().then(() => __importStar(require('./npcs/office-characters/index.js')));
|
|
426
|
+
const NPCClass = OFFICE_CHARACTER_NPCS[parsed.command];
|
|
427
|
+
const npc = new NPCClass({
|
|
428
|
+
...config,
|
|
429
|
+
pollInterval: (0, config_js_1.numberFlag)(parsed.flags, 'poll-interval'),
|
|
430
|
+
});
|
|
431
|
+
logger.info(`Starting ${parsed.command} NPC`);
|
|
432
|
+
await npc.start();
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
399
435
|
case 'github-watcher': {
|
|
400
436
|
const repo = (0, config_js_1.stringFlag)(parsed.flags, 'repo');
|
|
401
437
|
if (!repo) {
|
|
@@ -131,6 +131,13 @@ class BaseNPC {
|
|
|
131
131
|
alertConfig: { heartbeatInterval: this.heartbeatSeconds },
|
|
132
132
|
map: this.config.map,
|
|
133
133
|
});
|
|
134
|
+
// An observer cannot interrupt monitoring or retries.
|
|
135
|
+
try {
|
|
136
|
+
this.config.onPushResult?.({ success: result.success, error: result.error });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* observer only */
|
|
140
|
+
}
|
|
134
141
|
if (!result.success) {
|
|
135
142
|
this.reportPushFailure(result.error);
|
|
136
143
|
}
|
|
@@ -23,8 +23,10 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
|
|
|
23
23
|
lastPushed = null;
|
|
24
24
|
lastPushAt = 0;
|
|
25
25
|
pollInterval;
|
|
26
|
+
onCollection;
|
|
26
27
|
constructor(config) {
|
|
27
28
|
super(config);
|
|
29
|
+
this.onCollection = config.onCollection;
|
|
28
30
|
const requested = config.pollInterval ?? 15000;
|
|
29
31
|
this.pollInterval = Number.isFinite(requested)
|
|
30
32
|
? Math.max(exports.MIN_POLL_INTERVAL_MS, requested)
|
|
@@ -117,6 +119,12 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
|
|
|
117
119
|
// Stopped mid-collection: never push after (or race) the offline push.
|
|
118
120
|
if (this.state !== 'running')
|
|
119
121
|
return;
|
|
122
|
+
try {
|
|
123
|
+
this.onCollection?.(metrics);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* Diagnostics cannot stop publishing. */
|
|
127
|
+
}
|
|
120
128
|
await this.pushMetrics(metrics);
|
|
121
129
|
this.lastPushed = metrics;
|
|
122
130
|
this.lastPushAt = Date.now();
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReceptionistNPC = exports.LogWatcherNPC = exports.ProcessWatcherNPC = exports.InternetConnectivityNPC = exports.ensureSessionFilterTemplate = exports.sessionPublishDecision = exports.sessionFilterPath = exports.loadSessionFilter = exports.pixelDevice = exports.pixelArtFromPng = exports.encodePixelArt = exports.userConfigPath = exports.saveUserConfig = exports.loadUserConfig = exports.redactSecrets = exports.shortPath = exports.shortCommand = exports.describeToolUse = exports.CodexWatcher = exports.parseRolloutLine = exports.uninstallCodexHooks = exports.setupCodexHooks = exports.codexTransition = exports.handleCodexHookEvent = exports.handleCodexCommand = exports.claudeTransition = exports.processHookEvent = exports.uninstallHooks = exports.setupHooks = exports.handleHookEvent = exports.handleClaudeCodeCommand = exports.ClaudeCodeNPC = exports.SystemMonitorNPC = exports.pushHistory = exports.statusColor = exports.NpcConfigError = exports.sanitizeText = exports.truncate = exports.moodForStatus = exports.worstStatus = exports.evaluateThreshold = exports.numberFlag = exports.boolFlag = exports.stringFlag = exports.resolveConfig = exports.parseArgs = exports.Logger = exports.EventNPC = exports.PollingNPC = exports.BaseNPC = void 0;
|
|
4
|
-
exports.normalizeCursorEvent = exports.isCursorInvocation = exports.CURSOR_HOOK_EVENTS = exports.installCursorHooksInto = exports.uninstallCursorHooks = exports.setupCursorHooks = exports.handleCursorHookEvent = exports.cursorServiceId = exports.CURSOR_ADAPTER = exports.CURSOR_DEFAULTS = exports.GEMINI_HOOK_EVENTS = exports.installGeminiHooksInto = exports.uninstallGeminiHooks = exports.setupGeminiHooks = exports.handleGeminiHookEvent = exports.geminiServiceId = exports.geminiTransition = exports.GEMINI_ADAPTER = exports.GEMINI_DEFAULTS = exports.WebPageWatcherNPC = exports.GitHubWatcherNPC = void 0;
|
|
4
|
+
exports.OFFICE_CHARACTER_NPCS = exports.SlightlyInappropriateHRNPC = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = exports.SecurityNPC = exports.normalizeCursorEvent = exports.isCursorInvocation = exports.CURSOR_HOOK_EVENTS = exports.installCursorHooksInto = exports.uninstallCursorHooks = exports.setupCursorHooks = exports.handleCursorHookEvent = exports.cursorServiceId = exports.CURSOR_ADAPTER = exports.CURSOR_DEFAULTS = exports.GEMINI_HOOK_EVENTS = exports.installGeminiHooksInto = exports.uninstallGeminiHooks = exports.setupGeminiHooks = exports.handleGeminiHookEvent = exports.geminiServiceId = exports.geminiTransition = exports.GEMINI_ADAPTER = exports.GEMINI_DEFAULTS = exports.WebPageWatcherNPC = exports.GitHubWatcherNPC = void 0;
|
|
5
5
|
// Core
|
|
6
6
|
var base_npc_js_1 = require("./core/base-npc.js");
|
|
7
7
|
Object.defineProperty(exports, "BaseNPC", { enumerable: true, get: function () { return base_npc_js_1.BaseNPC; } });
|
|
@@ -100,4 +100,16 @@ Object.defineProperty(exports, "CURSOR_HOOK_EVENTS", { enumerable: true, get: fu
|
|
|
100
100
|
var normalize_js_1 = require("./npcs/cursor/normalize.js");
|
|
101
101
|
Object.defineProperty(exports, "isCursorInvocation", { enumerable: true, get: function () { return normalize_js_1.isCursorInvocation; } });
|
|
102
102
|
Object.defineProperty(exports, "normalizeCursorEvent", { enumerable: true, get: function () { return normalize_js_1.normalizeCursorEvent; } });
|
|
103
|
+
var index_js_12 = require("./npcs/security/index.js");
|
|
104
|
+
Object.defineProperty(exports, "SecurityNPC", { enumerable: true, get: function () { return index_js_12.SecurityNPC; } });
|
|
105
|
+
var index_js_13 = require("./npcs/office-characters/index.js");
|
|
106
|
+
Object.defineProperty(exports, "JanitorNPC", { enumerable: true, get: function () { return index_js_13.JanitorNPC; } });
|
|
107
|
+
Object.defineProperty(exports, "BaristaNPC", { enumerable: true, get: function () { return index_js_13.BaristaNPC; } });
|
|
108
|
+
Object.defineProperty(exports, "OfficeCatNPC", { enumerable: true, get: function () { return index_js_13.OfficeCatNPC; } });
|
|
109
|
+
Object.defineProperty(exports, "OfficeDogNPC", { enumerable: true, get: function () { return index_js_13.OfficeDogNPC; } });
|
|
110
|
+
Object.defineProperty(exports, "PlantCaretakerNPC", { enumerable: true, get: function () { return index_js_13.PlantCaretakerNPC; } });
|
|
111
|
+
Object.defineProperty(exports, "ITTechnicianNPC", { enumerable: true, get: function () { return index_js_13.ITTechnicianNPC; } });
|
|
112
|
+
Object.defineProperty(exports, "OutOfTouchExecutiveNPC", { enumerable: true, get: function () { return index_js_13.OutOfTouchExecutiveNPC; } });
|
|
113
|
+
Object.defineProperty(exports, "SlightlyInappropriateHRNPC", { enumerable: true, get: function () { return index_js_13.SlightlyInappropriateHRNPC; } });
|
|
114
|
+
Object.defineProperty(exports, "OFFICE_CHARACTER_NPCS", { enumerable: true, get: function () { return index_js_13.OFFICE_CHARACTER_NPCS; } });
|
|
103
115
|
//# sourceMappingURL=index.js.map
|
|
@@ -83,13 +83,19 @@ try {
|
|
|
83
83
|
if (neutral) process.stdout.write('{}\\n');
|
|
84
84
|
`;
|
|
85
85
|
function installHookShim(nodePath, cliPath) {
|
|
86
|
+
// Desktop ships this same CLI with Electron's Node runtime. Keep the flag in
|
|
87
|
+
// the launcher (never globally), inherited by flush/watchdog child processes.
|
|
88
|
+
const electron = !!process.versions['electron'];
|
|
86
89
|
writeChanged((0, exports.RUNTIME_POINTER)(), JSON.stringify({ node: nodePath, cli: cliPath }, null, 2) + '\n', 0o600);
|
|
87
90
|
writeChanged((0, node_path_1.join)((0, node_path_1.dirname)((0, exports.RUNTIME_POINTER)()), 'node-path'), nodePath + '\n', 0o600);
|
|
88
91
|
writeChanged((0, node_path_1.join)((0, node_path_1.dirname)((0, exports.SHIM_PATH)()), 'launch.cjs'), LAUNCHER, 0o600);
|
|
89
|
-
writeChanged((0, exports.SHIM_PATH)(), SHIM, 0o755);
|
|
92
|
+
writeChanged((0, exports.SHIM_PATH)(), electron ? SHIM.replace('exec "$NODE"', 'export ELECTRON_RUN_AS_NODE=1\nexec "$NODE"') : SHIM, 0o755);
|
|
90
93
|
(0, node_fs_1.chmodSync)((0, exports.SHIM_PATH)(), 0o755);
|
|
91
94
|
// Both launchers use the same bootstrap, including imported-Cursor detection.
|
|
92
|
-
|
|
95
|
+
const windowsShim = electron
|
|
96
|
+
? WINDOWS_SHIM.replace(':launch\n', ':launch\nset "ELECTRON_RUN_AS_NODE=1"\n')
|
|
97
|
+
: WINDOWS_SHIM;
|
|
98
|
+
writeChanged((0, exports.SHIM_PATH)() + '.cmd', windowsShim.replace(/\n/g, '\r\n'), 0o755);
|
|
93
99
|
(0, runtime_js_1.pruneOldRuntimes)((0, node_path_1.dirname)((0, exports.RUNTIME_POINTER)()));
|
|
94
100
|
return (0, exports.SHIM_PATH)();
|
|
95
101
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchCommits = fetchCommits;
|
|
4
|
+
exports.commitsWithin = commitsWithin;
|
|
5
|
+
exports.relativeTime = relativeTime;
|
|
6
|
+
exports.newCommitsSince = newCommitsSince;
|
|
7
|
+
function toNum(v) {
|
|
8
|
+
if (v === null)
|
|
9
|
+
return null;
|
|
10
|
+
const n = Number(v);
|
|
11
|
+
return Number.isNaN(n) ? null : n;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetch recent commits from the GitHub REST API using conditional requests.
|
|
15
|
+
* Passing the previous ETag yields a 304 (which does not count against the
|
|
16
|
+
* unauthenticated 60/hour limit) when nothing has changed. Never throws —
|
|
17
|
+
* network errors surface as `status: 0`.
|
|
18
|
+
*/
|
|
19
|
+
async function fetchCommits(repo, opts = {}) {
|
|
20
|
+
const perPage = opts.perPage ?? 10;
|
|
21
|
+
const url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
|
|
22
|
+
const headers = {
|
|
23
|
+
Accept: 'application/vnd.github+json',
|
|
24
|
+
'User-Agent': 'nocville-npc',
|
|
25
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
26
|
+
};
|
|
27
|
+
if (opts.token)
|
|
28
|
+
headers['Authorization'] = `Bearer ${opts.token}`;
|
|
29
|
+
if (opts.etag)
|
|
30
|
+
headers['If-None-Match'] = opts.etag;
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url, { headers, signal: controller.signal });
|
|
35
|
+
const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
|
|
36
|
+
const etag = res.headers.get('etag');
|
|
37
|
+
if (res.status === 304) {
|
|
38
|
+
return { notModified: true, commits: [], etag: opts.etag ?? null, rateRemaining, status: 304 };
|
|
39
|
+
}
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
// Drain the body so the socket can be reused.
|
|
42
|
+
await res.text().catch(() => undefined);
|
|
43
|
+
return { notModified: false, commits: [], etag, rateRemaining, status: res.status };
|
|
44
|
+
}
|
|
45
|
+
const body = (await res.json());
|
|
46
|
+
const commits = body.map((c) => parseCommit(c));
|
|
47
|
+
return { notModified: false, commits, etag, rateRemaining, status: res.status };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { notModified: false, commits: [], etag: null, rateRemaining: null, status: 0 };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseCommit(c) {
|
|
57
|
+
const commit = c['commit'] ?? {};
|
|
58
|
+
const commitAuthor = commit['author'] ?? {};
|
|
59
|
+
const ghAuthor = c['author'] ?? {};
|
|
60
|
+
const message = String(commit['message'] ?? '').split('\n')[0] ?? '';
|
|
61
|
+
return {
|
|
62
|
+
sha: String(c['sha'] ?? ''),
|
|
63
|
+
author: String(commitAuthor['name'] ?? ghAuthor['login'] ?? 'unknown'),
|
|
64
|
+
message,
|
|
65
|
+
date: String(commitAuthor['date'] ?? ''),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Count how many commits landed within the last `hours`. */
|
|
69
|
+
function commitsWithin(commits, hours, now = Date.now()) {
|
|
70
|
+
const cutoff = now - hours * 3600_000;
|
|
71
|
+
return commits.filter((c) => {
|
|
72
|
+
const t = Date.parse(c.date);
|
|
73
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
74
|
+
}).length;
|
|
75
|
+
}
|
|
76
|
+
/** Short relative-time string like "3m ago", "2h ago", "1d ago". */
|
|
77
|
+
function relativeTime(iso, now = Date.now()) {
|
|
78
|
+
const t = Date.parse(iso);
|
|
79
|
+
if (!Number.isFinite(t))
|
|
80
|
+
return 'unknown';
|
|
81
|
+
const secs = Math.max(0, Math.round((now - t) / 1000));
|
|
82
|
+
if (secs < 60)
|
|
83
|
+
return `${secs}s ago`;
|
|
84
|
+
const mins = Math.round(secs / 60);
|
|
85
|
+
if (mins < 60)
|
|
86
|
+
return `${mins}m ago`;
|
|
87
|
+
const hrs = Math.round(mins / 60);
|
|
88
|
+
if (hrs < 24)
|
|
89
|
+
return `${hrs}h ago`;
|
|
90
|
+
const days = Math.round(hrs / 24);
|
|
91
|
+
return `${days}d ago`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
|
|
95
|
+
* present in the window (more than a page of new commits, or rewritten history)
|
|
96
|
+
* every fetched commit is treated as new. An undefined sha means "not seeded
|
|
97
|
+
* yet" — returns empty so the caller can seed without a spurious announcement.
|
|
98
|
+
*/
|
|
99
|
+
function newCommitsSince(commits, lastSeenSha) {
|
|
100
|
+
if (!lastSeenSha)
|
|
101
|
+
return [];
|
|
102
|
+
const idx = commits.findIndex((c) => c.sha === lastSeenSha);
|
|
103
|
+
if (idx === 0)
|
|
104
|
+
return []; // newest is already seen
|
|
105
|
+
if (idx === -1)
|
|
106
|
+
return commits; // fell out of the window — treat all as new
|
|
107
|
+
return commits.slice(0, idx);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OFFICE_CHARACTER_NPCS = exports.SlightlyInappropriateHRNPC = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
|
|
4
|
+
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
|
+
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
6
|
+
class OfficeCharacterNPC extends polling_npc_js_1.PollingNPC {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super({ ...config, pollInterval: config.pollInterval ?? 60_000 });
|
|
9
|
+
}
|
|
10
|
+
get archetype() {
|
|
11
|
+
return 'custom';
|
|
12
|
+
}
|
|
13
|
+
get behavior() {
|
|
14
|
+
return { role: this.name, activity: 'idle', idlePose: 'stand' };
|
|
15
|
+
}
|
|
16
|
+
async collect() {
|
|
17
|
+
return { status: 'healthy', mood: 'happy', message: '', metrics: {} };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class JanitorNPC extends OfficeCharacterNPC {
|
|
21
|
+
get name() {
|
|
22
|
+
return 'janitor';
|
|
23
|
+
}
|
|
24
|
+
get defaultDisplayName() {
|
|
25
|
+
return 'Janitor';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.JanitorNPC = JanitorNPC;
|
|
29
|
+
class BaristaNPC extends OfficeCharacterNPC {
|
|
30
|
+
get name() {
|
|
31
|
+
return 'barista';
|
|
32
|
+
}
|
|
33
|
+
get defaultDisplayName() {
|
|
34
|
+
return 'Barista';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.BaristaNPC = BaristaNPC;
|
|
38
|
+
class OfficeCatNPC extends OfficeCharacterNPC {
|
|
39
|
+
get name() {
|
|
40
|
+
return 'office-cat';
|
|
41
|
+
}
|
|
42
|
+
get defaultDisplayName() {
|
|
43
|
+
return 'Office Cat';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.OfficeCatNPC = OfficeCatNPC;
|
|
47
|
+
class OfficeDogNPC extends OfficeCharacterNPC {
|
|
48
|
+
get name() {
|
|
49
|
+
return 'office-dog';
|
|
50
|
+
}
|
|
51
|
+
get defaultDisplayName() {
|
|
52
|
+
return 'Office Dog';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.OfficeDogNPC = OfficeDogNPC;
|
|
56
|
+
class PlantCaretakerNPC extends OfficeCharacterNPC {
|
|
57
|
+
get name() {
|
|
58
|
+
return 'plant-caretaker';
|
|
59
|
+
}
|
|
60
|
+
get defaultDisplayName() {
|
|
61
|
+
return 'Plant Caretaker';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.PlantCaretakerNPC = PlantCaretakerNPC;
|
|
65
|
+
class ITTechnicianNPC extends OfficeCharacterNPC {
|
|
66
|
+
get name() {
|
|
67
|
+
return 'it-technician';
|
|
68
|
+
}
|
|
69
|
+
get defaultDisplayName() {
|
|
70
|
+
return 'IT Technician';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.ITTechnicianNPC = ITTechnicianNPC;
|
|
74
|
+
class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
75
|
+
get name() {
|
|
76
|
+
return 'out-of-touch-executive';
|
|
77
|
+
}
|
|
78
|
+
get defaultDisplayName() {
|
|
79
|
+
return 'Out-of-Touch Executive';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.OutOfTouchExecutiveNPC = OutOfTouchExecutiveNPC;
|
|
83
|
+
class SlightlyInappropriateHRNPC extends OfficeCharacterNPC {
|
|
84
|
+
get name() {
|
|
85
|
+
return 'slightly-inappropriate-hr';
|
|
86
|
+
}
|
|
87
|
+
get defaultDisplayName() {
|
|
88
|
+
return 'Slightly Inappropriate HR';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.SlightlyInappropriateHRNPC = SlightlyInappropriateHRNPC;
|
|
92
|
+
exports.OFFICE_CHARACTER_NPCS = {
|
|
93
|
+
janitor: JanitorNPC,
|
|
94
|
+
barista: BaristaNPC,
|
|
95
|
+
'office-cat': OfficeCatNPC,
|
|
96
|
+
'office-dog': OfficeDogNPC,
|
|
97
|
+
'plant-caretaker': PlantCaretakerNPC,
|
|
98
|
+
'it-technician': ITTechnicianNPC,
|
|
99
|
+
'out-of-touch-executive': OutOfTouchExecutiveNPC,
|
|
100
|
+
'slightly-inappropriate-hr': SlightlyInappropriateHRNPC,
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -16,7 +16,6 @@ class ProcessWatcherNPC extends polling_npc_js_1.PollingNPC {
|
|
|
16
16
|
cpuHistories = new Map();
|
|
17
17
|
previousRunning = null;
|
|
18
18
|
constructor(config) {
|
|
19
|
-
(0, errors_js_1.assertPosix)('process-watcher', 'It relies on the POSIX `ps` command; run it under WSL/Linux, or use system-monitor instead.');
|
|
20
19
|
super(config);
|
|
21
20
|
this.processes = [...new Set(config.processes.map((p) => p.trim()).filter(Boolean))];
|
|
22
21
|
if (this.processes.length === 0) {
|
|
@@ -4,10 +4,13 @@ exports.detectTransitions = detectTransitions;
|
|
|
4
4
|
exports.parsePsOutput = parsePsOutput;
|
|
5
5
|
exports.matchesName = matchesName;
|
|
6
6
|
exports.listProcesses = listProcesses;
|
|
7
|
+
exports.parseWindowsProcesses = parseWindowsProcesses;
|
|
7
8
|
exports.matchProcesses = matchProcesses;
|
|
8
9
|
exports.scanProcesses = scanProcesses;
|
|
9
10
|
const node_child_process_1 = require("node:child_process");
|
|
10
11
|
const node_util_1 = require("node:util");
|
|
12
|
+
const node_os_1 = require("node:os");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
11
14
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
12
15
|
/** Bounds on the `ps` call: a wedged or enormous process table can't stall or flood us. */
|
|
13
16
|
const PS_TIMEOUT_MS = 5000;
|
|
@@ -60,10 +63,10 @@ function parsePsOutput(stdout) {
|
|
|
60
63
|
* Unlike a substring match, `sh` does not match `bash` or `ssh-agent`.
|
|
61
64
|
*/
|
|
62
65
|
function matchesName(comm, searchName) {
|
|
63
|
-
if (searchName
|
|
66
|
+
if (/[\\/]/.test(searchName))
|
|
64
67
|
return comm === searchName;
|
|
65
|
-
const base = (comm.split(
|
|
66
|
-
const name = searchName.toLowerCase();
|
|
68
|
+
const base = (comm.split(/[\\/]/).pop() ?? comm).toLowerCase().replace(/\.exe$/, '');
|
|
69
|
+
const name = searchName.toLowerCase().replace(/\.exe$/, '');
|
|
67
70
|
if (base === name)
|
|
68
71
|
return true;
|
|
69
72
|
if (name.length > LINUX_COMM_LEN && base === name.slice(0, LINUX_COMM_LEN))
|
|
@@ -72,6 +75,18 @@ function matchesName(comm, searchName) {
|
|
|
72
75
|
}
|
|
73
76
|
/** List every process. Throws if `ps` fails — callers must not read that as "all dead". */
|
|
74
77
|
async function listProcesses() {
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
// Fixed command, no user strings interpolated. Performance counters report a
|
|
80
|
+
// sampling interval (Get-Process.CPU is lifetime CPU seconds, not a percent).
|
|
81
|
+
const executable = (0, node_path_1.join)(process.env['SystemRoot'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
82
|
+
const { stdout } = await execFileAsync(executable, [
|
|
83
|
+
'-NoProfile',
|
|
84
|
+
'-NonInteractive',
|
|
85
|
+
'-Command',
|
|
86
|
+
'$ErrorActionPreference="Stop"; Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | Select-Object IDProcess,Name,PercentProcessorTime,WorkingSetPrivate | ConvertTo-Json -Compress',
|
|
87
|
+
], { timeout: PS_TIMEOUT_MS, maxBuffer: PS_MAX_BUFFER, windowsHide: true });
|
|
88
|
+
return parseWindowsProcesses(stdout, (0, node_os_1.cpus)().length);
|
|
89
|
+
}
|
|
75
90
|
const { stdout } = await execFileAsync('ps', ['-eo', 'pid=,pcpu=,rss=,comm='], {
|
|
76
91
|
timeout: PS_TIMEOUT_MS,
|
|
77
92
|
maxBuffer: PS_MAX_BUFFER,
|
|
@@ -80,6 +95,29 @@ async function listProcesses() {
|
|
|
80
95
|
});
|
|
81
96
|
return parsePsOutput(stdout);
|
|
82
97
|
}
|
|
98
|
+
/** Windows instance suffixes (#1, #2) distinguish copies of the same executable. */
|
|
99
|
+
function parseWindowsProcesses(stdout, cores = 1) {
|
|
100
|
+
const value = JSON.parse(stdout.replace(/^\uFEFF/, ''));
|
|
101
|
+
const rows = Array.isArray(value) ? value : value ? [value] : [];
|
|
102
|
+
return rows.flatMap((row) => {
|
|
103
|
+
const pid = Number(row.IDProcess);
|
|
104
|
+
const cpu = Number(row.PercentProcessorTime);
|
|
105
|
+
const bytes = Number(row.WorkingSetPrivate);
|
|
106
|
+
if (!(pid > 0) ||
|
|
107
|
+
!Number.isFinite(cpu) ||
|
|
108
|
+
!Number.isFinite(bytes) ||
|
|
109
|
+
typeof row.Name !== 'string')
|
|
110
|
+
return [];
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
pid,
|
|
114
|
+
cpu: cpu / Math.max(1, cores),
|
|
115
|
+
memKb: bytes / 1024,
|
|
116
|
+
comm: row.Name.replace(/#\d+$/, ''),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
83
121
|
/** Summarise each watched name against a process list (CPU and memory summed per name). */
|
|
84
122
|
function matchProcesses(names, entries) {
|
|
85
123
|
return names.map((name) => {
|
|
@@ -100,7 +138,7 @@ function matchProcesses(names, entries) {
|
|
|
100
138
|
}
|
|
101
139
|
/**
|
|
102
140
|
* Scan for processes by name and return their status and resource usage.
|
|
103
|
-
*
|
|
141
|
+
* Uses `ps` on macOS/Linux and CIM on Windows. Throws when the process list is unavailable.
|
|
104
142
|
*/
|
|
105
143
|
async function scanProcesses(names) {
|
|
106
144
|
return matchProcesses(names, await listProcesses());
|