@nocville/npc 0.2.2 → 0.3.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 +55 -17
- package/dist/cjs/cli.js +94 -39
- package/dist/cjs/core/base-npc.js +15 -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/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +21 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/agent-common/shim.js +8 -2
- 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/office-characters/index.js +92 -0
- package/dist/cjs/npcs/process-watcher/index.js +60 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +100 -68
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +6 -93
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/security/index.js +26 -0
- 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 +95 -40
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +15 -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/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/core/types.d.ts +5 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/agent-common/shim.js +8 -2
- 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/office-characters/index.d.ts +50 -0
- package/dist/npcs/office-characters/index.js +82 -0
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +61 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +31 -1
- package/dist/npcs/process-watcher/process-scanner.js +97 -70
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +3 -12
- package/dist/npcs/receptionist/index.js +6 -93
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/security/index.d.ts +12 -0
- package/dist/npcs/security/index.js +22 -0
- 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 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodePixelArt = encodePixelArt;
|
|
4
|
+
exports.pixelArtFromPng = pixelArtFromPng;
|
|
5
|
+
exports.pixelDevice = pixelDevice;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const pngjs_1 = require("pngjs");
|
|
8
|
+
/**
|
|
9
|
+
* Helpers for custom pixel-art companions (`screenType: 'pixel-device'`): your NPC's own
|
|
10
|
+
* device, sign or machine, drawn frameless beside it in the office.
|
|
11
|
+
*
|
|
12
|
+
* Art is 32 pixels per tile (a 2×3-tile device is 64×96, like the office furniture) and is
|
|
13
|
+
* sent as `pixel-image` screen data: a palette of up to 255 colours plus one byte per pixel.
|
|
14
|
+
* Fully transparent pixels (alpha < 128) become see-through, and several frames stacked top
|
|
15
|
+
* to bottom animate at `fps`. Push a new image whenever its state changes (e.g. an LED turns
|
|
16
|
+
* red); the server rate-limits pushes, so animate with frames rather than many pushes.
|
|
17
|
+
*/
|
|
18
|
+
/** Largest single screen-data value the API accepts (base64 pixels). */
|
|
19
|
+
const MAX_VALUE = 220_000;
|
|
20
|
+
const MAX_COLORS = 255;
|
|
21
|
+
const MAX_FRAMES = 16;
|
|
22
|
+
/** Encode RGBA frames as `pixel-image` screen data. Throws with a fix-it message. */
|
|
23
|
+
function encodePixelArt(art) {
|
|
24
|
+
const { width, height, frames } = art;
|
|
25
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1)
|
|
26
|
+
throw new Error('Pixel art needs a positive whole-number width and height.');
|
|
27
|
+
if (width > 192 || height > 192)
|
|
28
|
+
throw new Error(`Pixel art is ${width}×${height}; keep it within 192×192 (6×6 tiles).`);
|
|
29
|
+
if (frames.length < 1 || frames.length > MAX_FRAMES)
|
|
30
|
+
throw new Error(`Pixel art needs 1–${MAX_FRAMES} frames (got ${frames.length}).`);
|
|
31
|
+
const size = width * height;
|
|
32
|
+
for (const frame of frames)
|
|
33
|
+
if (frame.length !== size * 4)
|
|
34
|
+
throw new Error(`Each frame must be ${width}×${height} RGBA (${size * 4} bytes).`);
|
|
35
|
+
// Index 0 is reserved for transparency when any pixel is see-through.
|
|
36
|
+
const transparent = frames.some((f) => {
|
|
37
|
+
for (let i = 3; i < f.length; i += 4)
|
|
38
|
+
if (f[i] < 128)
|
|
39
|
+
return true;
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
const colors = new Map();
|
|
43
|
+
const palette = transparent ? [0, 0, 0] : [];
|
|
44
|
+
const pixels = Buffer.alloc(size * frames.length);
|
|
45
|
+
frames.forEach((frame, f) => {
|
|
46
|
+
for (let i = 0; i < size; i++) {
|
|
47
|
+
const r = frame[i * 4], g = frame[i * 4 + 1], b = frame[i * 4 + 2];
|
|
48
|
+
if (transparent && frame[i * 4 + 3] < 128) {
|
|
49
|
+
pixels[f * size + i] = 0;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = (r << 16) | (g << 8) | b;
|
|
53
|
+
let index = colors.get(key);
|
|
54
|
+
if (index === undefined) {
|
|
55
|
+
index = palette.length / 3;
|
|
56
|
+
if (index > MAX_COLORS)
|
|
57
|
+
throw new Error(`Pixel art uses more than ${MAX_COLORS} colours; reduce the palette (pixel art usually needs 8–32).`);
|
|
58
|
+
colors.set(key, index);
|
|
59
|
+
palette.push(r, g, b);
|
|
60
|
+
}
|
|
61
|
+
pixels[f * size + i] = index;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const tokens = [`idx8`, `${width}x${height}`, `p${palette.length / 3}`];
|
|
65
|
+
if (transparent)
|
|
66
|
+
tokens.push('t0');
|
|
67
|
+
if (frames.length > 1) {
|
|
68
|
+
const fps = Math.min(12, Math.max(1, Math.round(art.fps ?? 4)));
|
|
69
|
+
tokens.push(`f${frames.length}`, `fps${fps}`);
|
|
70
|
+
}
|
|
71
|
+
const encoded = pixels.toString('base64');
|
|
72
|
+
if (encoded.length > MAX_VALUE)
|
|
73
|
+
throw new Error('Pixel art is too large to send; use fewer frames or a smaller size.');
|
|
74
|
+
return {
|
|
75
|
+
layout: 'pixel-image',
|
|
76
|
+
values: [
|
|
77
|
+
{ label: 'fmt', value: tokens.join(';') },
|
|
78
|
+
{ label: 'palette', value: Buffer.from(palette).toString('base64') },
|
|
79
|
+
{ label: 'pixels', value: encoded },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load a PNG (path or bytes) as pixel-device screen data. For animation, stack frames
|
|
85
|
+
* top to bottom in one PNG and pass `frameHeight`.
|
|
86
|
+
*/
|
|
87
|
+
function pixelArtFromPng(png, opts = {}) {
|
|
88
|
+
const image = pngjs_1.PNG.sync.read(typeof png === 'string' ? (0, node_fs_1.readFileSync)(png) : png);
|
|
89
|
+
const frameHeight = opts.frameHeight ?? image.height;
|
|
90
|
+
if (frameHeight < 1 || image.height % frameHeight !== 0)
|
|
91
|
+
throw new Error(`PNG height ${image.height} is not a whole number of ${frameHeight}px frames.`);
|
|
92
|
+
const frameBytes = image.width * frameHeight * 4;
|
|
93
|
+
const frames = [];
|
|
94
|
+
for (let offset = 0; offset < image.data.length; offset += frameBytes)
|
|
95
|
+
frames.push(image.data.subarray(offset, offset + frameBytes));
|
|
96
|
+
return encodePixelArt({ width: image.width, height: frameHeight, frames, fps: opts.fps });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Companion declaration for a pixel device: `size` in tiles (art is 32px per tile).
|
|
100
|
+
* Freestanding by default so it sits on the floor next to the NPC's home.
|
|
101
|
+
*/
|
|
102
|
+
function pixelDevice(opts) {
|
|
103
|
+
return {
|
|
104
|
+
screenType: 'pixel-device',
|
|
105
|
+
size: { w: opts.w, h: opts.h },
|
|
106
|
+
placement: opts.placement ?? 'freestanding',
|
|
107
|
+
...(opts.label ? { label: opts.label } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=pixel-art.js.map
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PollingNPC = void 0;
|
|
3
|
+
exports.PollingNPC = exports.MIN_POLL_INTERVAL_MS = void 0;
|
|
4
4
|
const base_npc_js_1 = require("./base-npc.js");
|
|
5
|
+
/**
|
|
6
|
+
* Lower bound for any poll interval, so a typo can't exhaust the API's
|
|
7
|
+
* per-service rate limit (60 pushes/min) or hammer the watched source.
|
|
8
|
+
*/
|
|
9
|
+
exports.MIN_POLL_INTERVAL_MS = 5000;
|
|
10
|
+
/** How long `stop()` waits for an in-flight collection before pushing offline anyway. */
|
|
11
|
+
const STOP_DRAIN_TIMEOUT_MS = 10_000;
|
|
5
12
|
function clamp(n, min, max) {
|
|
6
13
|
return Math.min(max, Math.max(min, n));
|
|
7
14
|
}
|
|
8
15
|
class PollingNPC extends base_npc_js_1.BaseNPC {
|
|
9
16
|
timer = null;
|
|
10
|
-
|
|
17
|
+
keepaliveTimer = null;
|
|
18
|
+
/** The running collection + push, if any (awaited by `onStop`). */
|
|
19
|
+
inflight = null;
|
|
20
|
+
/** A `collectNow()` arrived while a collection was in flight: run once more after it. */
|
|
21
|
+
rerunRequested = false;
|
|
22
|
+
/** The last successfully pushed cycle, replayed (without its screen) by the keepalive. */
|
|
23
|
+
lastPushed = null;
|
|
24
|
+
lastPushAt = 0;
|
|
11
25
|
pollInterval;
|
|
12
26
|
constructor(config) {
|
|
13
27
|
super(config);
|
|
14
|
-
|
|
28
|
+
const requested = config.pollInterval ?? 15000;
|
|
29
|
+
this.pollInterval = Number.isFinite(requested)
|
|
30
|
+
? Math.max(exports.MIN_POLL_INTERVAL_MS, requested)
|
|
31
|
+
: 15000;
|
|
15
32
|
}
|
|
16
33
|
/** Scale staleness to the poll cadence: the server marks unknown at ~2x the interval. */
|
|
17
34
|
get heartbeatSeconds() {
|
|
@@ -20,14 +37,22 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
|
|
|
20
37
|
async onStart() {
|
|
21
38
|
// Collect immediately on start
|
|
22
39
|
await this.runCollection();
|
|
40
|
+
// Stopped while the first collection ran: don't arm timers nobody will clear.
|
|
41
|
+
if (this.state !== 'running')
|
|
42
|
+
return;
|
|
23
43
|
// Then on interval
|
|
24
44
|
this.timer = setInterval(() => {
|
|
25
45
|
void this.runCollection();
|
|
26
46
|
}, this.pollInterval);
|
|
27
|
-
//
|
|
28
|
-
// collections
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
// Only very slow pollers (> heartbeatSeconds, i.e. > 30 min given the clamp)
|
|
48
|
+
// could go stale between collections. Keep them alive by replaying the
|
|
49
|
+
// last cycle. The SDK's own heartbeat is not used: it omits `map`, `name`
|
|
50
|
+
// and `alertConfig`, so it would spawn a twin on the team's default map.
|
|
51
|
+
const heartbeatMs = this.heartbeatSeconds * 1000;
|
|
52
|
+
if (this.pollInterval > heartbeatMs) {
|
|
53
|
+
this.keepaliveTimer = setInterval(() => {
|
|
54
|
+
void this.keepalive(heartbeatMs);
|
|
55
|
+
}, heartbeatMs);
|
|
31
56
|
}
|
|
32
57
|
}
|
|
33
58
|
async onStop() {
|
|
@@ -35,29 +60,89 @@ class PollingNPC extends base_npc_js_1.BaseNPC {
|
|
|
35
60
|
clearInterval(this.timer);
|
|
36
61
|
this.timer = null;
|
|
37
62
|
}
|
|
38
|
-
this.
|
|
63
|
+
if (this.keepaliveTimer) {
|
|
64
|
+
clearInterval(this.keepaliveTimer);
|
|
65
|
+
this.keepaliveTimer = null;
|
|
66
|
+
}
|
|
67
|
+
this.rerunRequested = false;
|
|
68
|
+
// Let an in-flight push land before the offline push, so it can't arrive
|
|
69
|
+
// afterwards and respawn the NPC. Bounded: a hung collector never blocks exit.
|
|
70
|
+
if (this.inflight) {
|
|
71
|
+
let timeout;
|
|
72
|
+
await Promise.race([
|
|
73
|
+
this.inflight,
|
|
74
|
+
new Promise((resolve) => {
|
|
75
|
+
timeout = setTimeout(resolve, STOP_DRAIN_TIMEOUT_MS);
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
39
80
|
}
|
|
40
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Collect and push now instead of waiting for the next tick — for NPCs that
|
|
83
|
+
* also react to events (e.g. a fresh error line). Coalesces: if a collection
|
|
84
|
+
* is already running, exactly one more runs after it.
|
|
85
|
+
*/
|
|
86
|
+
collectNow() {
|
|
41
87
|
if (this.state !== 'running')
|
|
42
88
|
return;
|
|
89
|
+
if (this.inflight) {
|
|
90
|
+
this.rerunRequested = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
void this.runCollection();
|
|
94
|
+
}
|
|
95
|
+
runCollection() {
|
|
96
|
+
if (this.state !== 'running')
|
|
97
|
+
return Promise.resolve();
|
|
43
98
|
// Overlap guard: if a slow collector is still running, skip this tick rather
|
|
44
99
|
// than stacking collections (load-bearing for screenshot/render collectors).
|
|
45
|
-
if (this.
|
|
100
|
+
if (this.inflight) {
|
|
46
101
|
this.logger.debug('Previous collection still in flight, skipping this tick');
|
|
47
|
-
return;
|
|
102
|
+
return this.inflight;
|
|
48
103
|
}
|
|
49
|
-
this.
|
|
104
|
+
const run = this.collectAndPush().finally(() => {
|
|
105
|
+
this.inflight = null;
|
|
106
|
+
if (this.rerunRequested && this.state === 'running') {
|
|
107
|
+
this.rerunRequested = false;
|
|
108
|
+
void this.runCollection();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
this.inflight = run;
|
|
112
|
+
return run;
|
|
113
|
+
}
|
|
114
|
+
async collectAndPush() {
|
|
50
115
|
try {
|
|
51
116
|
const metrics = await this.collect();
|
|
117
|
+
// Stopped mid-collection: never push after (or race) the offline push.
|
|
118
|
+
if (this.state !== 'running')
|
|
119
|
+
return;
|
|
52
120
|
await this.pushMetrics(metrics);
|
|
121
|
+
this.lastPushed = metrics;
|
|
122
|
+
this.lastPushAt = Date.now();
|
|
53
123
|
}
|
|
54
124
|
catch (err) {
|
|
125
|
+
if (this.state !== 'running')
|
|
126
|
+
return; // teardown aborted the collector
|
|
55
127
|
this.logger.error('Collection failed', {
|
|
56
128
|
error: err instanceof Error ? err.message : String(err),
|
|
57
129
|
});
|
|
58
130
|
}
|
|
59
|
-
|
|
60
|
-
|
|
131
|
+
}
|
|
132
|
+
/** Replay the last cycle (minus its screen, which the server keeps) if nothing was pushed recently. */
|
|
133
|
+
async keepalive(heartbeatMs) {
|
|
134
|
+
if (this.state !== 'running' || this.inflight || !this.lastPushed)
|
|
135
|
+
return;
|
|
136
|
+
if (Date.now() - this.lastPushAt < heartbeatMs / 2)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
await this.pushMetrics({ ...this.lastPushed, screenData: undefined });
|
|
140
|
+
this.lastPushAt = Date.now();
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
this.logger.debug('Keepalive push failed', {
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
});
|
|
61
146
|
}
|
|
62
147
|
}
|
|
63
148
|
}
|
package/dist/cjs/core/text.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.truncate = truncate;
|
|
4
|
+
exports.sanitizeText = sanitizeText;
|
|
4
5
|
/**
|
|
5
6
|
* Truncate a string to at most `max` characters, appending an ellipsis when cut.
|
|
6
7
|
* Used to keep messages and screen lines within API limits.
|
|
@@ -12,4 +13,16 @@ function truncate(s, max = 200) {
|
|
|
12
13
|
return s.slice(0, Math.max(0, max));
|
|
13
14
|
return s.slice(0, max - 1) + '…';
|
|
14
15
|
}
|
|
16
|
+
// C0/C1 controls (incl. ESC, so no terminal escapes) plus the bidi overrides,
|
|
17
|
+
// isolates and marks that can visually reorder text on a banner.
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
const UNSAFE_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F--]/g;
|
|
20
|
+
/**
|
|
21
|
+
* Make untrusted remote text (commit messages, page titles, log lines) safe to
|
|
22
|
+
* display: drop control and bidi-override characters, collapse whitespace
|
|
23
|
+
* (including newlines and tabs) to single spaces, trim, and truncate.
|
|
24
|
+
*/
|
|
25
|
+
function sanitizeText(s, max = 200) {
|
|
26
|
+
return truncate(s.replace(UNSAFE_CHARS, '').replace(/\s+/g, ' ').trim(), max);
|
|
27
|
+
}
|
|
15
28
|
//# sourceMappingURL=text.js.map
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.evaluateThreshold = evaluateThreshold;
|
|
4
4
|
exports.worstStatus = worstStatus;
|
|
5
|
+
exports.moodForStatus = moodForStatus;
|
|
6
|
+
exports.thresholdProblem = thresholdProblem;
|
|
5
7
|
/**
|
|
6
8
|
* Evaluate a numeric value against thresholds to determine status.
|
|
7
9
|
*/
|
|
@@ -45,4 +47,31 @@ function worstStatus(statuses) {
|
|
|
45
47
|
}
|
|
46
48
|
return worst;
|
|
47
49
|
}
|
|
50
|
+
/** The NPC mood that matches a status (drives the NPC's demeanour and emotes). */
|
|
51
|
+
function moodForStatus(status) {
|
|
52
|
+
switch (status) {
|
|
53
|
+
case 'healthy':
|
|
54
|
+
return 'happy';
|
|
55
|
+
case 'warning':
|
|
56
|
+
return 'concerned';
|
|
57
|
+
case 'critical':
|
|
58
|
+
return 'stressed';
|
|
59
|
+
default:
|
|
60
|
+
return 'idle';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate a warning/critical pair from user flags: both finite, and critical
|
|
65
|
+
* at least as severe as warning for the threshold's direction. Returns an error
|
|
66
|
+
* message, or null when the pair is usable.
|
|
67
|
+
*/
|
|
68
|
+
function thresholdProblem(label, t) {
|
|
69
|
+
if (!Number.isFinite(t.warning) || !Number.isFinite(t.critical)) {
|
|
70
|
+
return `${label}: thresholds must be numbers`;
|
|
71
|
+
}
|
|
72
|
+
const inverted = (t.direction ?? 'above') === 'above' ? t.critical < t.warning : t.critical > t.warning;
|
|
73
|
+
return inverted
|
|
74
|
+
? `${label}: critical (${t.critical}) must be at least as severe as warning (${t.warning})`
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
48
77
|
//# sourceMappingURL=thresholds.js.map
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
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 = void 0;
|
|
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;
|
|
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; } });
|
|
@@ -20,8 +20,12 @@ Object.defineProperty(exports, "numberFlag", { enumerable: true, get: function (
|
|
|
20
20
|
var thresholds_js_1 = require("./core/thresholds.js");
|
|
21
21
|
Object.defineProperty(exports, "evaluateThreshold", { enumerable: true, get: function () { return thresholds_js_1.evaluateThreshold; } });
|
|
22
22
|
Object.defineProperty(exports, "worstStatus", { enumerable: true, get: function () { return thresholds_js_1.worstStatus; } });
|
|
23
|
+
Object.defineProperty(exports, "moodForStatus", { enumerable: true, get: function () { return thresholds_js_1.moodForStatus; } });
|
|
23
24
|
var text_js_1 = require("./core/text.js");
|
|
24
25
|
Object.defineProperty(exports, "truncate", { enumerable: true, get: function () { return text_js_1.truncate; } });
|
|
26
|
+
Object.defineProperty(exports, "sanitizeText", { enumerable: true, get: function () { return text_js_1.sanitizeText; } });
|
|
27
|
+
var errors_js_1 = require("./core/errors.js");
|
|
28
|
+
Object.defineProperty(exports, "NpcConfigError", { enumerable: true, get: function () { return errors_js_1.NpcConfigError; } });
|
|
25
29
|
var screen_js_1 = require("./core/screen.js");
|
|
26
30
|
Object.defineProperty(exports, "statusColor", { enumerable: true, get: function () { return screen_js_1.statusColor; } });
|
|
27
31
|
Object.defineProperty(exports, "pushHistory", { enumerable: true, get: function () { return screen_js_1.pushHistory; } });
|
|
@@ -53,6 +57,10 @@ var user_config_js_1 = require("./core/user-config.js");
|
|
|
53
57
|
Object.defineProperty(exports, "loadUserConfig", { enumerable: true, get: function () { return user_config_js_1.loadUserConfig; } });
|
|
54
58
|
Object.defineProperty(exports, "saveUserConfig", { enumerable: true, get: function () { return user_config_js_1.saveUserConfig; } });
|
|
55
59
|
Object.defineProperty(exports, "userConfigPath", { enumerable: true, get: function () { return user_config_js_1.userConfigPath; } });
|
|
60
|
+
var pixel_art_js_1 = require("./core/pixel-art.js");
|
|
61
|
+
Object.defineProperty(exports, "encodePixelArt", { enumerable: true, get: function () { return pixel_art_js_1.encodePixelArt; } });
|
|
62
|
+
Object.defineProperty(exports, "pixelArtFromPng", { enumerable: true, get: function () { return pixel_art_js_1.pixelArtFromPng; } });
|
|
63
|
+
Object.defineProperty(exports, "pixelDevice", { enumerable: true, get: function () { return pixel_art_js_1.pixelDevice; } });
|
|
56
64
|
var session_filter_js_1 = require("./core/session-filter.js");
|
|
57
65
|
Object.defineProperty(exports, "loadSessionFilter", { enumerable: true, get: function () { return session_filter_js_1.loadSessionFilter; } });
|
|
58
66
|
Object.defineProperty(exports, "sessionFilterPath", { enumerable: true, get: function () { return session_filter_js_1.sessionFilterPath; } });
|
|
@@ -92,4 +100,15 @@ Object.defineProperty(exports, "CURSOR_HOOK_EVENTS", { enumerable: true, get: fu
|
|
|
92
100
|
var normalize_js_1 = require("./npcs/cursor/normalize.js");
|
|
93
101
|
Object.defineProperty(exports, "isCursorInvocation", { enumerable: true, get: function () { return normalize_js_1.isCursorInvocation; } });
|
|
94
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, "OFFICE_CHARACTER_NPCS", { enumerable: true, get: function () { return index_js_13.OFFICE_CHARACTER_NPCS; } });
|
|
95
114
|
//# sourceMappingURL=index.js.map
|
|
@@ -244,6 +244,7 @@ function buildAgentPayload(adapter, state, config) {
|
|
|
244
244
|
name,
|
|
245
245
|
message: current ? (0, text_js_1.truncate)(current, 120) : undefined,
|
|
246
246
|
archetype: 'ai-agent',
|
|
247
|
+
kind: adapter.command,
|
|
247
248
|
metrics: { sessionStartedAt: state.startedAt ?? state.lastUpdate },
|
|
248
249
|
appearance: { family: adapter.family },
|
|
249
250
|
agentStats: state.agentStats,
|
|
@@ -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
|
}
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULTS = void 0;
|
|
4
4
|
exports.DEFAULTS = {
|
|
5
|
-
pollInterval: 60000, // 60s
|
|
5
|
+
pollInterval: 60000, // 60s with a token
|
|
6
|
+
/** Without a token GitHub allows 60 requests/hour (304s included): poll every 90s. */
|
|
7
|
+
unauthenticatedPollInterval: 90000,
|
|
6
8
|
fetchTimeoutMs: 5000,
|
|
7
9
|
perPage: 10,
|
|
10
|
+
/** Commit rows on the board (plus a header row). */
|
|
11
|
+
boardRows: 8,
|
|
8
12
|
/** Polls to keep the 'new-commit' sub-status active after a fresh commit. */
|
|
9
13
|
newCommitPolls: 2,
|
|
14
|
+
/** Consecutive network/5xx failures tolerated (board kept, healthy) before warning. */
|
|
15
|
+
failuresBeforeWarning: 3,
|
|
10
16
|
};
|
|
11
17
|
//# sourceMappingURL=defaults.js.map
|
|
@@ -1,102 +1,139 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidRepo = isValidRepo;
|
|
3
4
|
exports.fetchDefaultBranch = fetchDefaultBranch;
|
|
4
5
|
exports.fetchCommits = fetchCommits;
|
|
6
|
+
exports.parseCommit = parseCommit;
|
|
5
7
|
exports.commitsWithin = commitsWithin;
|
|
6
8
|
exports.newCommitsSince = newCommitsSince;
|
|
9
|
+
const http_js_1 = require("../../core/http.js");
|
|
10
|
+
const text_js_1 = require("../../core/text.js");
|
|
11
|
+
/** Largest GitHub API response we read (10 commits are ~30-60 KB). */
|
|
12
|
+
const MAX_BODY_BYTES = 512 * 1024;
|
|
13
|
+
// GitHub owner and repository names: letters, digits, '-', '_' and '.'.
|
|
14
|
+
const REPO_RE = /^([A-Za-z0-9-]{1,39})\/([A-Za-z0-9._-]{1,100})$/;
|
|
15
|
+
/**
|
|
16
|
+
* Validate an "owner/name" repository slug. It is interpolated into the API
|
|
17
|
+
* URL path, so anything else (`../`, extra segments, query characters) is
|
|
18
|
+
* rejected rather than letting it — and the token — reach another endpoint.
|
|
19
|
+
*/
|
|
20
|
+
function isValidRepo(repo) {
|
|
21
|
+
const m = repo.match(REPO_RE);
|
|
22
|
+
return !!m && m[2] !== '.' && m[2] !== '..';
|
|
23
|
+
}
|
|
7
24
|
function toNum(v) {
|
|
8
|
-
if (v === null)
|
|
25
|
+
if (v === null || v.trim() === '')
|
|
9
26
|
return null;
|
|
10
27
|
const n = Number(v);
|
|
11
|
-
return Number.
|
|
28
|
+
return Number.isFinite(n) ? n : null;
|
|
12
29
|
}
|
|
13
30
|
function ghHeaders(opts) {
|
|
14
31
|
const headers = {
|
|
15
32
|
Accept: 'application/vnd.github+json',
|
|
16
|
-
'User-Agent':
|
|
33
|
+
'User-Agent': http_js_1.NPC_USER_AGENT,
|
|
17
34
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
18
35
|
};
|
|
19
36
|
if (opts.token)
|
|
20
37
|
headers['Authorization'] = `Bearer ${opts.token}`;
|
|
21
38
|
return headers;
|
|
22
39
|
}
|
|
40
|
+
function doFetchFn(opts) {
|
|
41
|
+
return opts.fetchFn ?? globalThis.fetch;
|
|
42
|
+
}
|
|
43
|
+
async function readJson(res) {
|
|
44
|
+
const { text, truncated } = await (0, http_js_1.readTextCapped)(res, MAX_BODY_BYTES);
|
|
45
|
+
if (truncated)
|
|
46
|
+
throw new Error('response too large');
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
}
|
|
23
49
|
/** Look up a repo's default branch (one call at startup). Null on failure. */
|
|
24
50
|
async function fetchDefaultBranch(repo, opts = {}) {
|
|
25
|
-
const doFetch = opts.fetchFn ?? globalThis.fetch;
|
|
26
|
-
const controller = new AbortController();
|
|
27
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
|
|
28
51
|
try {
|
|
29
|
-
const res = await
|
|
52
|
+
const res = await doFetchFn(opts)(`https://api.github.com/repos/${repo}`, {
|
|
30
53
|
headers: ghHeaders(opts),
|
|
31
|
-
signal:
|
|
54
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5000),
|
|
32
55
|
});
|
|
33
56
|
if (!res.ok)
|
|
34
57
|
return null;
|
|
35
|
-
const
|
|
36
|
-
return
|
|
58
|
+
const branch = (0, http_js_1.asRecord)(await readJson(res))?.['default_branch'];
|
|
59
|
+
return typeof branch === 'string' && branch.length > 0 && branch.length <= 255
|
|
60
|
+
? (0, text_js_1.sanitizeText)(branch, 255)
|
|
61
|
+
: null;
|
|
37
62
|
}
|
|
38
63
|
catch {
|
|
39
64
|
return null;
|
|
40
65
|
}
|
|
41
|
-
finally {
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
}
|
|
44
66
|
}
|
|
45
67
|
/**
|
|
46
68
|
* Fetch recent commits from the GitHub REST API using conditional requests.
|
|
47
|
-
* Passing the previous ETag yields a 304
|
|
48
|
-
*
|
|
49
|
-
*
|
|
69
|
+
* Passing the previous ETag yields a cheap 304 when nothing has changed (free
|
|
70
|
+
* of the rate limit for authenticated requests). Never throws — network
|
|
71
|
+
* errors and unreadable bodies surface as `status: 0`.
|
|
50
72
|
*/
|
|
51
73
|
async function fetchCommits(repo, opts = {}) {
|
|
52
74
|
const perPage = opts.perPage ?? 10;
|
|
53
|
-
const doFetch = opts.fetchFn ?? globalThis.fetch;
|
|
54
75
|
let url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
|
|
55
76
|
if (opts.branch)
|
|
56
77
|
url += `&sha=${encodeURIComponent(opts.branch)}`;
|
|
57
78
|
const headers = ghHeaders(opts);
|
|
58
79
|
if (opts.etag)
|
|
59
80
|
headers['If-None-Match'] = opts.etag;
|
|
60
|
-
const
|
|
61
|
-
|
|
81
|
+
const failed = {
|
|
82
|
+
notModified: false,
|
|
83
|
+
commits: [],
|
|
84
|
+
etag: null,
|
|
85
|
+
rateRemaining: null,
|
|
86
|
+
rateResetAt: null,
|
|
87
|
+
status: 0,
|
|
88
|
+
};
|
|
62
89
|
try {
|
|
63
|
-
const res = await
|
|
90
|
+
const res = await doFetchFn(opts)(url, {
|
|
91
|
+
headers,
|
|
92
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5000),
|
|
93
|
+
});
|
|
64
94
|
const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
|
|
95
|
+
const resetSecs = toNum(res.headers.get('x-ratelimit-reset'));
|
|
96
|
+
const rateResetAt = resetSecs === null ? null : resetSecs * 1000;
|
|
65
97
|
const etag = res.headers.get('etag');
|
|
98
|
+
const meta = { rateRemaining, rateResetAt, status: res.status };
|
|
66
99
|
if (res.status === 304) {
|
|
67
|
-
return {
|
|
68
|
-
notModified: true,
|
|
69
|
-
commits: [],
|
|
70
|
-
etag: opts.etag ?? null,
|
|
71
|
-
rateRemaining,
|
|
72
|
-
status: 304,
|
|
73
|
-
};
|
|
100
|
+
return { ...meta, notModified: true, commits: [], etag: opts.etag ?? null };
|
|
74
101
|
}
|
|
75
102
|
if (!res.ok) {
|
|
76
|
-
await res.
|
|
77
|
-
return { notModified: false, commits: [], etag
|
|
103
|
+
await res.body?.cancel().catch(() => undefined);
|
|
104
|
+
return { ...meta, notModified: false, commits: [], etag };
|
|
78
105
|
}
|
|
79
|
-
const body =
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
const body = await readJson(res);
|
|
107
|
+
if (!Array.isArray(body))
|
|
108
|
+
return failed;
|
|
109
|
+
const commits = body.map(parseCommit).filter((c) => c !== null);
|
|
110
|
+
return { ...meta, notModified: false, commits, etag };
|
|
82
111
|
}
|
|
83
112
|
catch {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
clearTimeout(timer);
|
|
113
|
+
return failed;
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Extract the displayed fields from one (untrusted) commit object: a hex sha,
|
|
118
|
+
* the subject line and author name with control/bidi characters removed and
|
|
119
|
+
* lengths capped. Null for anything without a valid sha.
|
|
120
|
+
*/
|
|
121
|
+
function parseCommit(raw) {
|
|
122
|
+
const c = (0, http_js_1.asRecord)(raw);
|
|
123
|
+
const sha = c?.['sha'];
|
|
124
|
+
if (typeof sha !== 'string' || !/^[0-9a-f]{7,64}$/i.test(sha))
|
|
125
|
+
return null;
|
|
126
|
+
const commit = (0, http_js_1.asRecord)(c['commit']) ?? {};
|
|
127
|
+
const commitAuthor = (0, http_js_1.asRecord)(commit['author']) ?? {};
|
|
128
|
+
const ghAuthor = (0, http_js_1.asRecord)(c['author']) ?? {};
|
|
129
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v : null);
|
|
130
|
+
const message = str(commit['message'])?.split('\n')[0] ?? '';
|
|
131
|
+
const date = str(commitAuthor['date']) ?? '';
|
|
95
132
|
return {
|
|
96
|
-
sha:
|
|
97
|
-
author:
|
|
98
|
-
message,
|
|
99
|
-
date:
|
|
133
|
+
sha: sha.toLowerCase(),
|
|
134
|
+
author: (0, text_js_1.sanitizeText)(str(commitAuthor['name']) ?? str(ghAuthor['login']) ?? 'unknown', 40),
|
|
135
|
+
message: (0, text_js_1.sanitizeText)(message, 200),
|
|
136
|
+
date: Number.isFinite(Date.parse(date)) ? date : '',
|
|
100
137
|
};
|
|
101
138
|
}
|
|
102
139
|
/** Count how many commits landed within the last `hours`. */
|