@nocville/npc 0.3.0 → 0.4.0
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 +38 -18
- package/dist/cjs/cli.js +24 -3
- package/dist/cjs/core/base-npc.js +1 -0
- package/dist/cjs/core/config.js +23 -1
- package/dist/cjs/core/polling-npc.js +8 -0
- package/dist/cjs/core/user-config.js +2 -0
- package/dist/cjs/doctor.js +19 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/npcs/agent-common/session.js +3 -0
- package/dist/cjs/npcs/codex/watcher.js +1 -0
- package/dist/cjs/npcs/github-watcher/github.js +109 -0
- package/dist/cjs/npcs/office-characters/index.js +11 -1
- 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 +24 -3
- package/dist/core/base-npc.js +1 -0
- package/dist/core/config.js +23 -1
- package/dist/core/polling-npc.d.ts +3 -0
- package/dist/core/polling-npc.js +8 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/user-config.d.ts +1 -0
- package/dist/core/user-config.js +2 -0
- package/dist/doctor.js +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/npcs/agent-common/session.js +3 -0
- package/dist/npcs/codex/watcher.js +1 -0
- 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 +5 -0
- package/dist/npcs/office-characters/index.js +9 -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,23 +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
|
-
| `security`
|
|
151
|
-
| `janitor`
|
|
152
|
-
| `barista`
|
|
153
|
-
| `office-cat`
|
|
154
|
-
| `office-dog`
|
|
155
|
-
| `plant-caretaker`
|
|
156
|
-
| `it-technician`
|
|
157
|
-
| `out-of-touch-executive`
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
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://…` |
|
|
160
161
|
|
|
161
162
|
Run `nocville-npc --help` to see every flag. Poll intervals below 5 seconds are raised to 5
|
|
162
163
|
seconds. A few notes:
|
|
@@ -184,7 +185,7 @@ seconds. A few notes:
|
|
|
184
185
|
exterior doors for 15–30 seconds. Does not claim a desk. An explicit work-room rule
|
|
185
186
|
still confines its route, so leave it unassigned for office-wide rounds.
|
|
186
187
|
- **office characters**: `janitor`, `barista`, `office-cat`, `office-dog`,
|
|
187
|
-
`plant-caretaker`, `it-technician
|
|
188
|
+
`plant-caretaker`, `it-technician`, `out-of-touch-executive`, and `slightly-inappropriate-hr` run without screens or
|
|
188
189
|
external service credentials. They share the usual API key, name, service ID and map
|
|
189
190
|
options. Run each as its own process. Their routes and conversation are synchronized by
|
|
190
191
|
the server, and work-room rules apply. Missing furniture or coworkers cause quiet walks
|
|
@@ -225,3 +226,22 @@ class MyNPC extends PollingNPC {
|
|
|
225
226
|
|
|
226
227
|
`nocville-npc --help` lists every command and flag, and `nocville-npc doctor` checks that your
|
|
227
228
|
key and connection work. More at [nocville.com](https://nocville.com).
|
|
229
|
+
|
|
230
|
+
## Encrypted Cloudflare workspaces
|
|
231
|
+
|
|
232
|
+
Download and verify the producer workspace settings in team privacy settings, then:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
nocville-npc init --workspace=/absolute/path/workspace.json
|
|
236
|
+
# Supply the agent key through NOCVILLE_API_KEY or the existing init prompt.
|
|
237
|
+
nocville-npc system-monitor
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`--workspace`, `NOCVILLE_WORKSPACE`, and the saved workspace path are supported in
|
|
241
|
+
that order. Constructor `workspace` overrides them. The first publication displays
|
|
242
|
+
a signing-device fingerprint for the team owner to approve. Approval is shared by
|
|
243
|
+
the local NPCs using this credential, including coding-agent hooks and watchdogs.
|
|
244
|
+
The workspace pins the HTTPS origin and opaque map ID; changing either requires
|
|
245
|
+
matching verified workspace settings. The `doctor` command uses encrypted enrollment
|
|
246
|
+
and an offline probe for these workspaces. The Node SDK README describes private
|
|
247
|
+
profile storage and the encryption boundary.
|
package/dist/cjs/cli.js
CHANGED
|
@@ -91,10 +91,12 @@ Monitoring NPC types:
|
|
|
91
91
|
plant-caretaker Waters plants and reviews their performance
|
|
92
92
|
it-technician Inspects tech gear and recommends reboots
|
|
93
93
|
out-of-touch-executive Interrupts coworkers between long lounge breaks
|
|
94
|
+
slightly-inappropriate-hr Desk-based HR with questionable people skills
|
|
94
95
|
github-watcher Call out new commits on a repo
|
|
95
96
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
96
97
|
|
|
97
98
|
Common Options:
|
|
99
|
+
--workspace=<file> Verified producer workspace settings from team privacy settings.
|
|
98
100
|
--api-key=<key> Team API key (or NOCVILLE_API_KEY env)
|
|
99
101
|
--api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
|
|
100
102
|
Default: https://nocville.com. For local dev use
|
|
@@ -417,7 +419,8 @@ async function main() {
|
|
|
417
419
|
case 'office-dog':
|
|
418
420
|
case 'plant-caretaker':
|
|
419
421
|
case 'it-technician':
|
|
420
|
-
case 'out-of-touch-executive':
|
|
422
|
+
case 'out-of-touch-executive':
|
|
423
|
+
case 'slightly-inappropriate-hr': {
|
|
421
424
|
if (!config.serviceId)
|
|
422
425
|
config.serviceId = parsed.command;
|
|
423
426
|
const { OFFICE_CHARACTER_NPCS } = await Promise.resolve().then(() => __importStar(require('./npcs/office-characters/index.js')));
|
|
@@ -510,7 +513,20 @@ async function runInit(flags, config) {
|
|
|
510
513
|
let apiKey = (0, config_js_1.stringFlag)(flags, 'api-key');
|
|
511
514
|
const apiUrl = (0, config_js_1.stringFlag)(flags, 'api-url');
|
|
512
515
|
const map = (0, config_js_1.stringFlag)(flags, 'map');
|
|
513
|
-
|
|
516
|
+
const workspace = config.workspace;
|
|
517
|
+
if (workspace)
|
|
518
|
+
apiKey = apiKey || config.apiKey;
|
|
519
|
+
if (workspace && !apiKey) {
|
|
520
|
+
const { resolveApiKey } = await Promise.resolve().then(() => __importStar(require('./core/api-key.js')));
|
|
521
|
+
const resolved = await resolveApiKey({ flags });
|
|
522
|
+
if (!resolved.ok) {
|
|
523
|
+
console.error(resolved.error);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
apiKey = resolved.key;
|
|
528
|
+
}
|
|
529
|
+
if (!apiKey && !apiUrl && !map && !workspace) {
|
|
514
530
|
const saved = loadUserConfig();
|
|
515
531
|
if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
|
|
516
532
|
console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
|
|
@@ -526,7 +542,12 @@ async function runInit(flags, config) {
|
|
|
526
542
|
}
|
|
527
543
|
apiKey = resolved.key;
|
|
528
544
|
}
|
|
529
|
-
const path = saveUserConfig({
|
|
545
|
+
const path = saveUserConfig({
|
|
546
|
+
apiKey,
|
|
547
|
+
apiUrl: workspace ? config.apiUrl : apiUrl,
|
|
548
|
+
map: workspace ? config.map : map,
|
|
549
|
+
workspace,
|
|
550
|
+
});
|
|
530
551
|
const saved = loadUserConfig();
|
|
531
552
|
console.log(`✓ Saved ${path} (mode 0600)`);
|
|
532
553
|
console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
|
|
@@ -17,6 +17,7 @@ class BaseNPC {
|
|
|
17
17
|
// We can't access `this.name` here since it's abstract, so use a placeholder
|
|
18
18
|
this.logger = new logger_js_1.Logger('npc', config.debug);
|
|
19
19
|
this.client = new node_1.NocvilleClient({
|
|
20
|
+
workspace: config.workspace,
|
|
20
21
|
apiUrl: config.apiUrl,
|
|
21
22
|
apiKey: config.apiKey,
|
|
22
23
|
serviceId: config.serviceId,
|
package/dist/cjs/core/config.js
CHANGED
|
@@ -6,6 +6,8 @@ exports.stringFlag = stringFlag;
|
|
|
6
6
|
exports.boolFlag = boolFlag;
|
|
7
7
|
exports.listFlag = listFlag;
|
|
8
8
|
exports.numberFlag = numberFlag;
|
|
9
|
+
const node_1 = require("@nocville/node");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
9
11
|
const user_config_js_1 = require("./user-config.js");
|
|
10
12
|
/**
|
|
11
13
|
* Simple hand-rolled CLI arg parser.
|
|
@@ -56,9 +58,16 @@ function parseArgs(argv) {
|
|
|
56
58
|
*/
|
|
57
59
|
function resolveConfig(flags, overrides) {
|
|
58
60
|
const saved = (0, user_config_js_1.loadUserConfig)();
|
|
61
|
+
const workspacePath = overrides?.workspace ||
|
|
62
|
+
stringFlag(flags, 'workspace') ||
|
|
63
|
+
process.env['NOCVILLE_WORKSPACE'] ||
|
|
64
|
+
saved.workspace;
|
|
65
|
+
const workspace = workspacePath ? (0, node_path_1.resolve)(workspacePath) : undefined;
|
|
66
|
+
const enrollment = workspace ? (0, node_1.loadProducerWorkspace)(workspace) : undefined;
|
|
59
67
|
const apiUrl = overrides?.apiUrl ||
|
|
60
68
|
stringFlag(flags, 'api-url') ||
|
|
61
69
|
process.env['NOCVILLE_API_URL'] ||
|
|
70
|
+
enrollment?.apiUrl ||
|
|
62
71
|
saved.apiUrl ||
|
|
63
72
|
'https://nocville.com';
|
|
64
73
|
const apiKey = overrides?.apiKey ||
|
|
@@ -76,10 +85,23 @@ function resolveConfig(flags, overrides) {
|
|
|
76
85
|
const map = overrides?.map ||
|
|
77
86
|
stringFlag(flags, 'map') ||
|
|
78
87
|
process.env['NOCVILLE_MAP'] ||
|
|
88
|
+
enrollment?.encryption.mapId ||
|
|
79
89
|
saved.map ||
|
|
80
90
|
undefined;
|
|
81
91
|
const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
|
|
82
|
-
|
|
92
|
+
if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
|
|
93
|
+
throw new Error('API origin does not match the verified workspace');
|
|
94
|
+
if (enrollment && map && map !== enrollment.encryption.mapId)
|
|
95
|
+
throw new Error('Encrypted producers use the map ID in their workspace settings');
|
|
96
|
+
return {
|
|
97
|
+
apiUrl,
|
|
98
|
+
apiKey,
|
|
99
|
+
serviceId,
|
|
100
|
+
debug,
|
|
101
|
+
map: enrollment?.encryption.mapId ?? map,
|
|
102
|
+
name,
|
|
103
|
+
...(workspace ? { workspace } : {}),
|
|
104
|
+
};
|
|
83
105
|
}
|
|
84
106
|
function stringFlag(flags, key) {
|
|
85
107
|
const val = flags[key];
|
|
@@ -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();
|
|
@@ -25,6 +25,8 @@ function loadUserConfig(path = userConfigPath()) {
|
|
|
25
25
|
try {
|
|
26
26
|
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
|
|
27
27
|
const out = {};
|
|
28
|
+
if (typeof raw['workspace'] === 'string' && raw['workspace'])
|
|
29
|
+
out.workspace = raw['workspace'];
|
|
28
30
|
if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
|
|
29
31
|
out.apiUrl = raw['apiUrl'];
|
|
30
32
|
if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
|
package/dist/cjs/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runDoctor = runDoctor;
|
|
4
4
|
exports.hasCodexHookWatcherOverlap = hasCodexHookWatcherOverlap;
|
|
5
|
+
const node_1 = require("@nocville/node");
|
|
5
6
|
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
7
8
|
const liveness_js_1 = require("./npcs/agent-common/liveness.js");
|
|
@@ -43,6 +44,24 @@ async function runDoctor(config) {
|
|
|
43
44
|
if (!hasKey) {
|
|
44
45
|
console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
|
|
45
46
|
}
|
|
47
|
+
else if (config.workspace || config.apiKey.includes('.team_')) {
|
|
48
|
+
// Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
|
|
49
|
+
try {
|
|
50
|
+
const client = new node_1.NocvilleClient({
|
|
51
|
+
workspace: config.workspace,
|
|
52
|
+
apiUrl: config.apiUrl,
|
|
53
|
+
apiKey: config.apiKey,
|
|
54
|
+
serviceId: 'nocville-doctor',
|
|
55
|
+
retries: 0,
|
|
56
|
+
});
|
|
57
|
+
const result = await client.pushStatus({ status: 'unknown', offline: true });
|
|
58
|
+
pushOk = result.success;
|
|
59
|
+
console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.log(` [FAIL] Encrypted producer: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
else {
|
|
47
66
|
const start = Date.now();
|
|
48
67
|
const t = withTimeout(5000);
|
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.OFFICE_CHARACTER_NPCS = 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;
|
|
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; } });
|
|
@@ -110,5 +110,6 @@ Object.defineProperty(exports, "OfficeDogNPC", { enumerable: true, get: function
|
|
|
110
110
|
Object.defineProperty(exports, "PlantCaretakerNPC", { enumerable: true, get: function () { return index_js_13.PlantCaretakerNPC; } });
|
|
111
111
|
Object.defineProperty(exports, "ITTechnicianNPC", { enumerable: true, get: function () { return index_js_13.ITTechnicianNPC; } });
|
|
112
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; } });
|
|
113
114
|
Object.defineProperty(exports, "OFFICE_CHARACTER_NPCS", { enumerable: true, get: function () { return index_js_13.OFFICE_CHARACTER_NPCS; } });
|
|
114
115
|
//# sourceMappingURL=index.js.map
|
|
@@ -263,6 +263,7 @@ function buildAgentPayload(adapter, state, config) {
|
|
|
263
263
|
}
|
|
264
264
|
async function defaultPush(config, serviceId, payload, timeoutMs) {
|
|
265
265
|
const client = new node_1.NocvilleClient({
|
|
266
|
+
workspace: config.workspace,
|
|
266
267
|
apiUrl: config.apiUrl,
|
|
267
268
|
apiKey: config.apiKey,
|
|
268
269
|
serviceId,
|
|
@@ -381,6 +382,8 @@ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
|
|
|
381
382
|
if (!cliPath)
|
|
382
383
|
return undefined;
|
|
383
384
|
const env = { ...process.env };
|
|
385
|
+
if (config.workspace)
|
|
386
|
+
env['NOCVILLE_WORKSPACE'] = config.workspace;
|
|
384
387
|
env['NOCVILLE_API_URL'] = config.apiUrl;
|
|
385
388
|
env['NOCVILLE_API_KEY'] = config.apiKey;
|
|
386
389
|
if (config.map)
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OFFICE_CHARACTER_NPCS = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
|
|
3
|
+
exports.OFFICE_CHARACTER_NPCS = exports.SlightlyInappropriateHRNPC = exports.OutOfTouchExecutiveNPC = exports.ITTechnicianNPC = exports.PlantCaretakerNPC = exports.OfficeDogNPC = exports.OfficeCatNPC = exports.BaristaNPC = exports.JanitorNPC = void 0;
|
|
4
4
|
const polling_npc_js_1 = require("../../core/polling-npc.js");
|
|
5
5
|
/** Quiet heartbeats; the authoritative server drives routines and conversation. */
|
|
6
6
|
class OfficeCharacterNPC extends polling_npc_js_1.PollingNPC {
|
|
@@ -80,6 +80,15 @@ class OutOfTouchExecutiveNPC extends OfficeCharacterNPC {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
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;
|
|
83
92
|
exports.OFFICE_CHARACTER_NPCS = {
|
|
84
93
|
janitor: JanitorNPC,
|
|
85
94
|
barista: BaristaNPC,
|
|
@@ -88,5 +97,6 @@ exports.OFFICE_CHARACTER_NPCS = {
|
|
|
88
97
|
'plant-caretaker': PlantCaretakerNPC,
|
|
89
98
|
'it-technician': ITTechnicianNPC,
|
|
90
99
|
'out-of-touch-executive': OutOfTouchExecutiveNPC,
|
|
100
|
+
'slightly-inappropriate-hr': SlightlyInappropriateHRNPC,
|
|
91
101
|
};
|
|
92
102
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractTitle = extractTitle;
|
|
4
|
+
exports.statusFromHttp = statusFromHttp;
|
|
5
|
+
exports.basicCheck = basicCheck;
|
|
6
|
+
exports.hostnameOf = hostnameOf;
|
|
7
|
+
/** Extract the <title> text from an HTML string (empty if none). */
|
|
8
|
+
function extractTitle(html) {
|
|
9
|
+
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
10
|
+
return m ? m[1].replace(/\s+/g, ' ').trim().slice(0, 80) : '';
|
|
11
|
+
}
|
|
12
|
+
/** Map an HTTP status to NPC status: 2xx/3xx ok, 4xx/timeout warn, 5xx/refused crit. */
|
|
13
|
+
function statusFromHttp(code, timedOut = false) {
|
|
14
|
+
if (code >= 200 && code < 400)
|
|
15
|
+
return 'healthy';
|
|
16
|
+
if (code >= 400 && code < 500)
|
|
17
|
+
return 'warning';
|
|
18
|
+
if (code >= 500)
|
|
19
|
+
return 'critical';
|
|
20
|
+
return timedOut ? 'warning' : 'critical'; // 0 = network error
|
|
21
|
+
}
|
|
22
|
+
/** Fetch a page and extract HTTP status + <title> (basic, no browser). */
|
|
23
|
+
async function basicCheck(url, timeoutMs = 8000) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
headers: { 'User-Agent': 'nocville-npc' },
|
|
30
|
+
redirect: 'follow',
|
|
31
|
+
});
|
|
32
|
+
let title = '';
|
|
33
|
+
try {
|
|
34
|
+
title = extractTitle(await res.text());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// body unavailable; leave title blank
|
|
38
|
+
}
|
|
39
|
+
return { status: res.status, title, ok: res.ok, timedOut: false };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const timedOut = err instanceof Error && err.name === 'AbortError';
|
|
43
|
+
return { status: 0, title: '', ok: false, timedOut };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Best-effort hostname for a URL, falling back to the raw string. */
|
|
50
|
+
function hostnameOf(url) {
|
|
51
|
+
try {
|
|
52
|
+
return new URL(url).hostname;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return url.slice(0, 40);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=basic.js.map
|
|
@@ -5,6 +5,7 @@ exports.chromeArgs = chromeArgs;
|
|
|
5
5
|
exports.runningAsRoot = runningAsRoot;
|
|
6
6
|
exports.loadPuppeteer = loadPuppeteer;
|
|
7
7
|
exports.browserConnected = browserConnected;
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_child_process_1 = require("node:child_process");
|
|
10
11
|
const MAC_PATHS = [
|
|
@@ -50,6 +51,25 @@ function findChrome(explicit) {
|
|
|
50
51
|
return p;
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
const roots = [
|
|
56
|
+
process.env['LOCALAPPDATA'],
|
|
57
|
+
process.env['PROGRAMFILES'],
|
|
58
|
+
process.env['PROGRAMFILES(X86)'],
|
|
59
|
+
].filter((p) => !!p);
|
|
60
|
+
for (const root of roots)
|
|
61
|
+
for (const relative of [
|
|
62
|
+
['Google', 'Chrome', 'Application', 'chrome.exe'],
|
|
63
|
+
['Microsoft', 'Edge', 'Application', 'msedge.exe'],
|
|
64
|
+
['Chromium', 'Application', 'chrome.exe'],
|
|
65
|
+
['BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'],
|
|
66
|
+
]) {
|
|
67
|
+
const path = (0, node_path_1.join)(root, ...relative);
|
|
68
|
+
if ((0, node_fs_1.existsSync)(path))
|
|
69
|
+
return path;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
53
73
|
if (process.platform === 'linux') {
|
|
54
74
|
for (const bin of LINUX_BINARIES) {
|
|
55
75
|
const resolved = which(bin);
|
|
@@ -83,6 +103,12 @@ function runningAsRoot() {
|
|
|
83
103
|
*/
|
|
84
104
|
async function loadPuppeteer() {
|
|
85
105
|
try {
|
|
106
|
+
// Prefer CommonJS in bundled runtimes so Electron resolves through its
|
|
107
|
+
// ASAR loader. Native ESM installations use the optional dynamic import below.
|
|
108
|
+
if (typeof require === 'function') {
|
|
109
|
+
const mod = require('puppeteer-core');
|
|
110
|
+
return mod.default ?? mod;
|
|
111
|
+
}
|
|
86
112
|
const dynImport = new Function('m', 'return import(m)');
|
|
87
113
|
const mod = await dynImport('puppeteer-core');
|
|
88
114
|
return (mod.default ?? mod);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findChrome = findChrome;
|
|
4
|
+
exports.loadPuppeteer = loadPuppeteer;
|
|
5
|
+
exports.browserConnected = browserConnected;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const CHROME_PATHS = {
|
|
8
|
+
darwin: [
|
|
9
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
10
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
11
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
12
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
13
|
+
],
|
|
14
|
+
linux: [
|
|
15
|
+
'/usr/bin/google-chrome',
|
|
16
|
+
'/usr/bin/google-chrome-stable',
|
|
17
|
+
'/usr/bin/chromium',
|
|
18
|
+
'/usr/bin/chromium-browser',
|
|
19
|
+
'/usr/bin/microsoft-edge',
|
|
20
|
+
'/snap/bin/chromium',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
/** Find a usable Chrome/Chromium executable: explicit path → env → known paths. */
|
|
24
|
+
function findChrome(explicit) {
|
|
25
|
+
const envPath = process.env['CHROME_PATH'];
|
|
26
|
+
const candidates = [
|
|
27
|
+
...(explicit ? [explicit] : []),
|
|
28
|
+
...(envPath ? [envPath] : []),
|
|
29
|
+
...(CHROME_PATHS[process.platform] ?? []),
|
|
30
|
+
];
|
|
31
|
+
for (const p of candidates) {
|
|
32
|
+
if (p && (0, node_fs_1.existsSync)(p))
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load the optional `puppeteer-core` peer dependency without a static import,
|
|
39
|
+
* so the package builds and runs when it isn't installed. Returns null if the
|
|
40
|
+
* module is absent.
|
|
41
|
+
*/
|
|
42
|
+
async function loadPuppeteer() {
|
|
43
|
+
try {
|
|
44
|
+
// Indirect dynamic import so TypeScript/bundlers don't require the module
|
|
45
|
+
// at build time.
|
|
46
|
+
const dynImport = new Function('m', 'return import(m)');
|
|
47
|
+
const mod = await dynImport('puppeteer-core');
|
|
48
|
+
return (mod.default ?? mod);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** True if the browser reports a live connection. */
|
|
55
|
+
function browserConnected(browser) {
|
|
56
|
+
if (typeof browser.connected === 'boolean')
|
|
57
|
+
return browser.connected;
|
|
58
|
+
if (typeof browser.isConnected === 'function')
|
|
59
|
+
return browser.isConnected();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=chrome.js.map
|