@nocville/npc 0.0.0-stage → 0.2.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 +268 -2
- package/dist/cjs/cli.js +584 -0
- package/dist/cjs/core/api-key.js +69 -0
- package/dist/cjs/core/base-npc.js +160 -0
- package/dist/cjs/core/config.js +108 -0
- package/dist/cjs/core/event-npc.js +14 -0
- package/dist/cjs/core/logger.js +39 -0
- package/dist/cjs/core/polling-npc.js +65 -0
- package/dist/cjs/core/screen.js +28 -0
- package/dist/cjs/core/session-filter.js +150 -0
- package/dist/cjs/core/text.js +15 -0
- package/dist/cjs/core/thresholds.js +48 -0
- package/dist/cjs/core/types.js +10 -0
- package/dist/cjs/core/user-config.js +92 -0
- package/dist/cjs/doctor.js +130 -0
- package/dist/cjs/index.js +95 -0
- package/dist/cjs/npcs/agent-common/activity.js +291 -0
- package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
- package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
- package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
- package/dist/cjs/npcs/agent-common/liveness.js +112 -0
- package/dist/cjs/npcs/agent-common/lock.js +99 -0
- package/dist/cjs/npcs/agent-common/runtime.js +154 -0
- package/dist/cjs/npcs/agent-common/session.js +412 -0
- package/dist/cjs/npcs/agent-common/shim.js +96 -0
- package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
- package/dist/cjs/npcs/claude-code/defaults.js +20 -0
- package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
- package/dist/cjs/npcs/claude-code/index.js +104 -0
- package/dist/cjs/npcs/claude-code/setup.js +133 -0
- package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
- package/dist/cjs/npcs/codex/defaults.js +32 -0
- package/dist/cjs/npcs/codex/hook-handler.js +116 -0
- package/dist/cjs/npcs/codex/index.js +92 -0
- package/dist/cjs/npcs/codex/rollout.js +246 -0
- package/dist/cjs/npcs/codex/setup.js +104 -0
- package/dist/cjs/npcs/codex/watcher.js +361 -0
- package/dist/cjs/npcs/cursor/defaults.js +15 -0
- package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
- package/dist/cjs/npcs/cursor/index.js +72 -0
- package/dist/cjs/npcs/cursor/normalize.js +54 -0
- package/dist/cjs/npcs/cursor/setup.js +98 -0
- package/dist/cjs/npcs/gemini/defaults.js +15 -0
- package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
- package/dist/cjs/npcs/gemini/index.js +69 -0
- package/dist/cjs/npcs/gemini/setup.js +98 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
- package/dist/cjs/npcs/github-watcher/format.js +21 -0
- package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
- package/dist/cjs/npcs/github-watcher/index.js +148 -0
- package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
- package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
- package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
- package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
- package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
- package/dist/cjs/npcs/log-watcher/index.js +161 -0
- package/dist/cjs/npcs/log-watcher/tail.js +81 -0
- package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
- package/dist/cjs/npcs/process-watcher/index.js +140 -0
- package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
- package/dist/cjs/npcs/receptionist/defaults.js +9 -0
- package/dist/cjs/npcs/receptionist/index.js +120 -0
- package/dist/cjs/npcs/receptionist/weather.js +129 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
- package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
- package/dist/cjs/npcs/system-monitor/index.js +105 -0
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
- package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
- package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +549 -0
- package/dist/core/api-key.d.ts +34 -0
- package/dist/core/api-key.js +64 -0
- package/dist/core/base-npc.d.ts +41 -0
- package/dist/core/base-npc.js +156 -0
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +101 -0
- package/dist/core/event-npc.d.ts +8 -0
- package/dist/core/event-npc.js +10 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +35 -0
- package/dist/core/polling-npc.d.ts +18 -0
- package/dist/core/polling-npc.js +61 -0
- package/dist/core/screen.d.ts +11 -0
- package/dist/core/screen.js +24 -0
- package/dist/core/session-filter.d.ts +59 -0
- package/dist/core/session-filter.js +143 -0
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +10 -0
- package/dist/core/thresholds.js +44 -0
- package/dist/core/types.d.ts +60 -0
- package/dist/core/types.js +7 -0
- package/dist/core/user-config.d.ts +43 -0
- package/dist/core/user-config.js +85 -0
- package/dist/doctor.d.ts +8 -0
- package/dist/doctor.js +126 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/npcs/agent-common/activity.d.ts +42 -0
- package/dist/npcs/agent-common/activity.js +280 -0
- package/dist/npcs/agent-common/git-stats.d.ts +11 -0
- package/dist/npcs/agent-common/git-stats.js +67 -0
- package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
- package/dist/npcs/agent-common/hook-runner.js +245 -0
- package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
- package/dist/npcs/agent-common/hook-settings.js +265 -0
- package/dist/npcs/agent-common/liveness.d.ts +12 -0
- package/dist/npcs/agent-common/liveness.js +105 -0
- package/dist/npcs/agent-common/lock.d.ts +6 -0
- package/dist/npcs/agent-common/lock.js +95 -0
- package/dist/npcs/agent-common/runtime.d.ts +16 -0
- package/dist/npcs/agent-common/runtime.js +149 -0
- package/dist/npcs/agent-common/session.d.ts +192 -0
- package/dist/npcs/agent-common/session.js +390 -0
- package/dist/npcs/agent-common/shim.d.ts +4 -0
- package/dist/npcs/agent-common/shim.js +90 -0
- package/dist/npcs/agent-common/watchdog.d.ts +16 -0
- package/dist/npcs/agent-common/watchdog.js +144 -0
- package/dist/npcs/claude-code/defaults.d.ts +15 -0
- package/dist/npcs/claude-code/defaults.js +17 -0
- package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
- package/dist/npcs/claude-code/hook-handler.js +131 -0
- package/dist/npcs/claude-code/index.d.ts +36 -0
- package/dist/npcs/claude-code/index.js +92 -0
- package/dist/npcs/claude-code/setup.d.ts +58 -0
- package/dist/npcs/claude-code/setup.js +121 -0
- package/dist/npcs/claude-code/state-machine.d.ts +28 -0
- package/dist/npcs/claude-code/state-machine.js +125 -0
- package/dist/npcs/codex/defaults.d.ts +24 -0
- package/dist/npcs/codex/defaults.js +28 -0
- package/dist/npcs/codex/hook-handler.d.ts +24 -0
- package/dist/npcs/codex/hook-handler.js +110 -0
- package/dist/npcs/codex/index.d.ts +21 -0
- package/dist/npcs/codex/index.js +76 -0
- package/dist/npcs/codex/rollout.d.ts +39 -0
- package/dist/npcs/codex/rollout.js +242 -0
- package/dist/npcs/codex/setup.d.ts +37 -0
- package/dist/npcs/codex/setup.js +96 -0
- package/dist/npcs/codex/watcher.d.ts +65 -0
- package/dist/npcs/codex/watcher.js +356 -0
- package/dist/npcs/cursor/defaults.d.ts +9 -0
- package/dist/npcs/cursor/defaults.js +11 -0
- package/dist/npcs/cursor/hook-handler.d.ts +8 -0
- package/dist/npcs/cursor/hook-handler.js +60 -0
- package/dist/npcs/cursor/index.d.ts +18 -0
- package/dist/npcs/cursor/index.js +57 -0
- package/dist/npcs/cursor/normalize.d.ts +6 -0
- package/dist/npcs/cursor/normalize.js +49 -0
- package/dist/npcs/cursor/setup.d.ts +29 -0
- package/dist/npcs/cursor/setup.js +90 -0
- package/dist/npcs/gemini/defaults.d.ts +9 -0
- package/dist/npcs/gemini/defaults.js +11 -0
- package/dist/npcs/gemini/hook-handler.d.ts +10 -0
- package/dist/npcs/gemini/hook-handler.js +87 -0
- package/dist/npcs/gemini/index.d.ts +17 -0
- package/dist/npcs/gemini/index.js +56 -0
- package/dist/npcs/gemini/setup.d.ts +29 -0
- package/dist/npcs/gemini/setup.js +90 -0
- package/dist/npcs/github-watcher/defaults.d.ts +8 -0
- package/dist/npcs/github-watcher/defaults.js +8 -0
- package/dist/npcs/github-watcher/format.d.ts +3 -0
- package/dist/npcs/github-watcher/format.js +18 -0
- package/dist/npcs/github-watcher/github-api.d.ts +55 -0
- package/dist/npcs/github-watcher/github-api.js +120 -0
- package/dist/npcs/github-watcher/index.d.ts +35 -0
- package/dist/npcs/github-watcher/index.js +144 -0
- package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
- package/dist/npcs/internet-connectivity/checks.js +84 -0
- package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
- package/dist/npcs/internet-connectivity/defaults.js +19 -0
- package/dist/npcs/internet-connectivity/index.d.ts +55 -0
- package/dist/npcs/internet-connectivity/index.js +246 -0
- package/dist/npcs/log-watcher/buffer.d.ts +18 -0
- package/dist/npcs/log-watcher/buffer.js +34 -0
- package/dist/npcs/log-watcher/defaults.d.ts +9 -0
- package/dist/npcs/log-watcher/defaults.js +9 -0
- package/dist/npcs/log-watcher/index.d.ts +45 -0
- package/dist/npcs/log-watcher/index.js +157 -0
- package/dist/npcs/log-watcher/tail.d.ts +9 -0
- package/dist/npcs/log-watcher/tail.js +78 -0
- package/dist/npcs/process-watcher/defaults.d.ts +6 -0
- package/dist/npcs/process-watcher/defaults.js +6 -0
- package/dist/npcs/process-watcher/index.d.ts +26 -0
- package/dist/npcs/process-watcher/index.js +136 -0
- package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
- package/dist/npcs/process-watcher/process-scanner.js +110 -0
- package/dist/npcs/receptionist/defaults.d.ts +6 -0
- package/dist/npcs/receptionist/defaults.js +6 -0
- package/dist/npcs/receptionist/index.d.ts +29 -0
- package/dist/npcs/receptionist/index.js +116 -0
- package/dist/npcs/receptionist/weather.d.ts +37 -0
- package/dist/npcs/receptionist/weather.js +121 -0
- package/dist/npcs/system-monitor/collectors.d.ts +45 -0
- package/dist/npcs/system-monitor/collectors.js +60 -0
- package/dist/npcs/system-monitor/defaults.d.ts +7 -0
- package/dist/npcs/system-monitor/defaults.js +10 -0
- package/dist/npcs/system-monitor/index.d.ts +27 -0
- package/dist/npcs/system-monitor/index.js +101 -0
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
- package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
- package/dist/npcs/web-page-watcher/capture.js +75 -0
- package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
- package/dist/npcs/web-page-watcher/defaults.js +27 -0
- package/dist/npcs/web-page-watcher/index.d.ts +42 -0
- package/dist/npcs/web-page-watcher/index.js +230 -0
- package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
- package/dist/npcs/web-page-watcher/pixelate.js +172 -0
- package/package.json +73 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function toNum(v) {
|
|
2
|
+
if (v === null)
|
|
3
|
+
return null;
|
|
4
|
+
const n = Number(v);
|
|
5
|
+
return Number.isNaN(n) ? null : n;
|
|
6
|
+
}
|
|
7
|
+
function ghHeaders(opts) {
|
|
8
|
+
const headers = {
|
|
9
|
+
Accept: 'application/vnd.github+json',
|
|
10
|
+
'User-Agent': 'nocville-npc',
|
|
11
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
12
|
+
};
|
|
13
|
+
if (opts.token)
|
|
14
|
+
headers['Authorization'] = `Bearer ${opts.token}`;
|
|
15
|
+
return headers;
|
|
16
|
+
}
|
|
17
|
+
/** Look up a repo's default branch (one call at startup). Null on failure. */
|
|
18
|
+
export async function fetchDefaultBranch(repo, opts = {}) {
|
|
19
|
+
const doFetch = opts.fetchFn ?? globalThis.fetch;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
|
|
22
|
+
try {
|
|
23
|
+
const res = await doFetch(`https://api.github.com/repos/${repo}`, {
|
|
24
|
+
headers: ghHeaders(opts),
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
return null;
|
|
29
|
+
const body = (await res.json());
|
|
30
|
+
return body.default_branch ?? null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch recent commits from the GitHub REST API using conditional requests.
|
|
41
|
+
* Passing the previous ETag yields a 304 (which does not count against the
|
|
42
|
+
* unauthenticated 60/hour limit) when nothing has changed. Never throws —
|
|
43
|
+
* network errors surface as `status: 0`.
|
|
44
|
+
*/
|
|
45
|
+
export async function fetchCommits(repo, opts = {}) {
|
|
46
|
+
const perPage = opts.perPage ?? 10;
|
|
47
|
+
const doFetch = opts.fetchFn ?? globalThis.fetch;
|
|
48
|
+
let url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
|
|
49
|
+
if (opts.branch)
|
|
50
|
+
url += `&sha=${encodeURIComponent(opts.branch)}`;
|
|
51
|
+
const headers = ghHeaders(opts);
|
|
52
|
+
if (opts.etag)
|
|
53
|
+
headers['If-None-Match'] = opts.etag;
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
|
|
56
|
+
try {
|
|
57
|
+
const res = await doFetch(url, { headers, signal: controller.signal });
|
|
58
|
+
const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
|
|
59
|
+
const etag = res.headers.get('etag');
|
|
60
|
+
if (res.status === 304) {
|
|
61
|
+
return {
|
|
62
|
+
notModified: true,
|
|
63
|
+
commits: [],
|
|
64
|
+
etag: opts.etag ?? null,
|
|
65
|
+
rateRemaining,
|
|
66
|
+
status: 304,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
await res.text().catch(() => undefined);
|
|
71
|
+
return { notModified: false, commits: [], etag, rateRemaining, status: res.status };
|
|
72
|
+
}
|
|
73
|
+
const body = (await res.json());
|
|
74
|
+
const commits = body.map((c) => parseCommit(c));
|
|
75
|
+
return { notModified: false, commits, etag, rateRemaining, status: res.status };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { notModified: false, commits: [], etag: null, rateRemaining: null, status: 0 };
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseCommit(c) {
|
|
85
|
+
const commit = c['commit'] ?? {};
|
|
86
|
+
const commitAuthor = commit['author'] ?? {};
|
|
87
|
+
const ghAuthor = c['author'] ?? {};
|
|
88
|
+
const message = String(commit['message'] ?? '').split('\n')[0] ?? '';
|
|
89
|
+
return {
|
|
90
|
+
sha: String(c['sha'] ?? ''),
|
|
91
|
+
author: String(commitAuthor['name'] ?? ghAuthor['login'] ?? 'unknown'),
|
|
92
|
+
message,
|
|
93
|
+
date: String(commitAuthor['date'] ?? ''),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Count how many commits landed within the last `hours`. */
|
|
97
|
+
export function commitsWithin(commits, hours, now = Date.now()) {
|
|
98
|
+
const cutoff = now - hours * 3600_000;
|
|
99
|
+
return commits.filter((c) => {
|
|
100
|
+
const t = Date.parse(c.date);
|
|
101
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
102
|
+
}).length;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
|
|
106
|
+
* present in the window (more than a page of new commits, or rewritten history)
|
|
107
|
+
* every fetched commit is treated as new. An undefined sha means "not seeded
|
|
108
|
+
* yet" — returns empty so the caller can seed without a spurious announcement.
|
|
109
|
+
*/
|
|
110
|
+
export function newCommitsSince(commits, lastSeenSha) {
|
|
111
|
+
if (!lastSeenSha)
|
|
112
|
+
return [];
|
|
113
|
+
const idx = commits.findIndex((c) => c.sha === lastSeenSha);
|
|
114
|
+
if (idx === 0)
|
|
115
|
+
return []; // newest is already seen
|
|
116
|
+
if (idx === -1)
|
|
117
|
+
return commits; // fell out of the window — treat all as new
|
|
118
|
+
return commits.slice(0, idx);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=github-api.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Archetype, CompanionDecl } from '../../core/types.js';
|
|
4
|
+
import type { FetchFn } from './github-api.js';
|
|
5
|
+
export interface GitHubWatcherConfig extends PollingNpcConfig {
|
|
6
|
+
/** "owner/name" */
|
|
7
|
+
repo: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
branch?: string;
|
|
10
|
+
/** Injectable fetch (for tests). */
|
|
11
|
+
fetchFn?: FetchFn;
|
|
12
|
+
}
|
|
13
|
+
export declare class GitHubWatcherNPC extends PollingNPC {
|
|
14
|
+
private repo;
|
|
15
|
+
private token?;
|
|
16
|
+
private branch?;
|
|
17
|
+
private fetchFn?;
|
|
18
|
+
private etag;
|
|
19
|
+
private lastSeenSha;
|
|
20
|
+
private commits;
|
|
21
|
+
private rateRemaining;
|
|
22
|
+
private newCommitPollsLeft;
|
|
23
|
+
constructor(config: GitHubWatcherConfig);
|
|
24
|
+
get name(): string;
|
|
25
|
+
protected get defaultDisplayName(): string;
|
|
26
|
+
get archetype(): Archetype;
|
|
27
|
+
get companion(): CompanionDecl;
|
|
28
|
+
protected onStart(): Promise<void>;
|
|
29
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
30
|
+
private reportNewCommits;
|
|
31
|
+
private reportIdle;
|
|
32
|
+
private report;
|
|
33
|
+
private buildBoard;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import { truncate } from '../../core/text.js';
|
|
3
|
+
import { DEFAULTS } from './defaults.js';
|
|
4
|
+
import { fetchCommits, fetchDefaultBranch, newCommitsSince, commitsWithin } from './github-api.js';
|
|
5
|
+
import { relativeTime } from './format.js';
|
|
6
|
+
export class GitHubWatcherNPC extends PollingNPC {
|
|
7
|
+
repo;
|
|
8
|
+
token;
|
|
9
|
+
branch;
|
|
10
|
+
fetchFn;
|
|
11
|
+
etag = null;
|
|
12
|
+
lastSeenSha = null;
|
|
13
|
+
commits = [];
|
|
14
|
+
rateRemaining = null;
|
|
15
|
+
newCommitPollsLeft = 0;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
super({ ...config, pollInterval: config.pollInterval ?? DEFAULTS.pollInterval });
|
|
18
|
+
this.repo = config.repo;
|
|
19
|
+
this.token = config.token;
|
|
20
|
+
this.branch = config.branch;
|
|
21
|
+
this.fetchFn = config.fetchFn;
|
|
22
|
+
}
|
|
23
|
+
get name() {
|
|
24
|
+
return 'github-watcher';
|
|
25
|
+
}
|
|
26
|
+
get defaultDisplayName() {
|
|
27
|
+
const repoName = this.repo.split('/').pop() || this.repo;
|
|
28
|
+
return `GitHub: ${repoName}`;
|
|
29
|
+
}
|
|
30
|
+
get archetype() {
|
|
31
|
+
return 'pipeline';
|
|
32
|
+
}
|
|
33
|
+
get companion() {
|
|
34
|
+
return {
|
|
35
|
+
screenType: 'commit-board',
|
|
36
|
+
size: { w: 3, h: 2 },
|
|
37
|
+
placement: 'adjacent-wall',
|
|
38
|
+
label: this.repo,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async onStart() {
|
|
42
|
+
// Resolve the default branch once if the user didn't pin one.
|
|
43
|
+
if (!this.branch) {
|
|
44
|
+
const def = await fetchDefaultBranch(this.repo, {
|
|
45
|
+
token: this.token,
|
|
46
|
+
timeoutMs: DEFAULTS.fetchTimeoutMs,
|
|
47
|
+
fetchFn: this.fetchFn,
|
|
48
|
+
});
|
|
49
|
+
if (def) {
|
|
50
|
+
this.branch = def;
|
|
51
|
+
this.logger.info(`Watching default branch "${def}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await super.onStart();
|
|
55
|
+
}
|
|
56
|
+
async collect() {
|
|
57
|
+
const result = await fetchCommits(this.repo, {
|
|
58
|
+
token: this.token,
|
|
59
|
+
etag: this.etag,
|
|
60
|
+
branch: this.branch,
|
|
61
|
+
timeoutMs: DEFAULTS.fetchTimeoutMs,
|
|
62
|
+
perPage: DEFAULTS.perPage,
|
|
63
|
+
fetchFn: this.fetchFn,
|
|
64
|
+
});
|
|
65
|
+
if (result.rateRemaining !== null)
|
|
66
|
+
this.rateRemaining = result.rateRemaining;
|
|
67
|
+
if (result.status === 403) {
|
|
68
|
+
return this.report('warning', 'concerned', 'GitHub API rate-limited (403)', 'rate-limited');
|
|
69
|
+
}
|
|
70
|
+
if (result.status === 404) {
|
|
71
|
+
return this.report('warning', 'concerned', `Repo not found: ${this.repo}`, 'not-found');
|
|
72
|
+
}
|
|
73
|
+
if (result.status === 0 && this.commits.length === 0) {
|
|
74
|
+
return this.report('warning', 'concerned', 'GitHub API unreachable', 'unreachable');
|
|
75
|
+
}
|
|
76
|
+
if (result.notModified) {
|
|
77
|
+
return this.reportIdle();
|
|
78
|
+
}
|
|
79
|
+
if (result.commits.length > 0) {
|
|
80
|
+
const fresh = newCommitsSince(result.commits, this.lastSeenSha);
|
|
81
|
+
this.commits = result.commits;
|
|
82
|
+
this.etag = result.etag ?? this.etag;
|
|
83
|
+
if (this.lastSeenSha === null) {
|
|
84
|
+
this.lastSeenSha = result.commits[0].sha;
|
|
85
|
+
return this.reportIdle();
|
|
86
|
+
}
|
|
87
|
+
if (fresh.length > 0) {
|
|
88
|
+
this.lastSeenSha = result.commits[0].sha;
|
|
89
|
+
this.newCommitPollsLeft = DEFAULTS.newCommitPolls;
|
|
90
|
+
return this.reportNewCommits(fresh);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return this.reportIdle();
|
|
94
|
+
}
|
|
95
|
+
reportNewCommits(fresh) {
|
|
96
|
+
const newest = fresh[0];
|
|
97
|
+
const latest = `${newest.author}: ${truncate(newest.message, 120)} (${newest.sha.slice(0, 7)})`;
|
|
98
|
+
const message = fresh.length > 1 ? `${fresh.length} new commits — latest: ${latest}` : latest;
|
|
99
|
+
return this.report('healthy', 'happy', message, 'new-commit');
|
|
100
|
+
}
|
|
101
|
+
reportIdle() {
|
|
102
|
+
if (this.newCommitPollsLeft > 0)
|
|
103
|
+
this.newCommitPollsLeft--;
|
|
104
|
+
const subStatus = this.newCommitPollsLeft > 0 ? 'new-commit' : undefined;
|
|
105
|
+
const newest = this.commits[0];
|
|
106
|
+
const message = newest
|
|
107
|
+
? `Last: ${truncate(newest.message, 80)} (${relativeTime(newest.date)})`
|
|
108
|
+
: `Watching ${this.repo}`;
|
|
109
|
+
return this.report('healthy', 'happy', message, subStatus);
|
|
110
|
+
}
|
|
111
|
+
report(status, mood, message, subStatus) {
|
|
112
|
+
const newest = this.commits[0];
|
|
113
|
+
const secondsSinceLast = newest && newest.date
|
|
114
|
+
? Math.max(0, Math.round((Date.now() - Date.parse(newest.date)) / 1000))
|
|
115
|
+
: 0;
|
|
116
|
+
return {
|
|
117
|
+
status,
|
|
118
|
+
mood,
|
|
119
|
+
message,
|
|
120
|
+
subStatus,
|
|
121
|
+
screenData: this.buildBoard(),
|
|
122
|
+
metrics: {
|
|
123
|
+
seconds_since_last_commit: secondsSinceLast,
|
|
124
|
+
commits_last_24h: commitsWithin(this.commits, 24),
|
|
125
|
+
api_rate_remaining: this.rateRemaining ?? -1,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
buildBoard() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const header = this.branch ? `${this.repo}@${this.branch}` : this.repo;
|
|
132
|
+
const values = [{ label: 'repo', value: header }];
|
|
133
|
+
for (const c of this.commits.slice(0, 8)) {
|
|
134
|
+
const recent = c.date && now - Date.parse(c.date) < 3600_000;
|
|
135
|
+
values.push({
|
|
136
|
+
label: c.sha.slice(0, 7),
|
|
137
|
+
value: truncate(`${c.author}: ${c.message}`, 60),
|
|
138
|
+
color: recent ? 'green' : 'gray',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { layout: 'commit-board', values };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LatencyHost } from './defaults.js';
|
|
2
|
+
export interface DnsCheckResult {
|
|
3
|
+
host: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
timeMs: number;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function checkDns(hosts: string[]): Promise<DnsCheckResult[]>;
|
|
9
|
+
export interface HttpCheckResult {
|
|
10
|
+
url: string;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
timeMs: number;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function checkHttp(urls: string[], timeout: number): Promise<HttpCheckResult[]>;
|
|
17
|
+
export interface LatencyCheckResult {
|
|
18
|
+
host: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
timeMs: number;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function checkLatency(hosts: LatencyHost[]): Promise<LatencyCheckResult[]>;
|
|
24
|
+
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { promises as dns } from 'node:dns';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
export async function checkDns(hosts) {
|
|
4
|
+
return Promise.all(hosts.map(resolveSingleDns));
|
|
5
|
+
}
|
|
6
|
+
async function resolveSingleDns(host) {
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
try {
|
|
9
|
+
await dns.resolve(host);
|
|
10
|
+
return { host, ok: true, timeMs: performance.now() - start };
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return {
|
|
14
|
+
host,
|
|
15
|
+
ok: false,
|
|
16
|
+
timeMs: performance.now() - start,
|
|
17
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function checkHttp(urls, timeout) {
|
|
22
|
+
return Promise.all(urls.map((url) => fetchSingleUrl(url, timeout)));
|
|
23
|
+
}
|
|
24
|
+
async function fetchSingleUrl(url, timeout) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
27
|
+
const start = performance.now();
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
30
|
+
// Consume body to ensure connection is completed
|
|
31
|
+
await response.text();
|
|
32
|
+
return {
|
|
33
|
+
url,
|
|
34
|
+
ok: response.ok,
|
|
35
|
+
timeMs: performance.now() - start,
|
|
36
|
+
statusCode: response.status,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return {
|
|
41
|
+
url,
|
|
42
|
+
ok: false,
|
|
43
|
+
timeMs: performance.now() - start,
|
|
44
|
+
error: err instanceof Error ? err.message : String(err),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function checkLatency(hosts) {
|
|
52
|
+
return Promise.all(hosts.map(measureTcpConnect));
|
|
53
|
+
}
|
|
54
|
+
function measureTcpConnect(target) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const start = performance.now();
|
|
57
|
+
const socket = net.connect({ host: target.host, port: target.port });
|
|
58
|
+
const timeout = setTimeout(() => {
|
|
59
|
+
socket.destroy();
|
|
60
|
+
resolve({
|
|
61
|
+
host: target.host,
|
|
62
|
+
ok: false,
|
|
63
|
+
timeMs: performance.now() - start,
|
|
64
|
+
error: 'Connection timed out',
|
|
65
|
+
});
|
|
66
|
+
}, 5000);
|
|
67
|
+
socket.on('connect', () => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
const timeMs = performance.now() - start;
|
|
70
|
+
socket.destroy();
|
|
71
|
+
resolve({ host: target.host, ok: true, timeMs });
|
|
72
|
+
});
|
|
73
|
+
socket.on('error', (err) => {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
resolve({
|
|
76
|
+
host: target.host,
|
|
77
|
+
ok: false,
|
|
78
|
+
timeMs: performance.now() - start,
|
|
79
|
+
error: err.message,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ThresholdConfig } from '../../core/types.js';
|
|
2
|
+
export interface LatencyHost {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
}
|
|
6
|
+
export interface InternetConnectivityDefaults {
|
|
7
|
+
pollInterval: number;
|
|
8
|
+
dnsHosts: string[];
|
|
9
|
+
httpUrls: string[];
|
|
10
|
+
latencyHosts: LatencyHost[];
|
|
11
|
+
slowThreshold: number;
|
|
12
|
+
httpTimeout: number;
|
|
13
|
+
thresholds: {
|
|
14
|
+
dnsTime: ThresholdConfig;
|
|
15
|
+
httpTime: ThresholdConfig;
|
|
16
|
+
latencyTime: ThresholdConfig;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULTS: InternetConnectivityDefaults;
|
|
20
|
+
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DEFAULTS = {
|
|
2
|
+
pollInterval: 30000, // 30 seconds
|
|
3
|
+
dnsHosts: ['google.com', 'cloudflare.com'],
|
|
4
|
+
// gstatic generate_204 is a stable, low-latency 204 endpoint used by many
|
|
5
|
+
// OSes for connectivity checks (replaces the flaky httpbin.org default).
|
|
6
|
+
httpUrls: ['https://1.1.1.1/cdn-cgi/trace', 'https://www.gstatic.com/generate_204'],
|
|
7
|
+
latencyHosts: [
|
|
8
|
+
{ host: '8.8.8.8', port: 80 },
|
|
9
|
+
{ host: '1.1.1.1', port: 80 },
|
|
10
|
+
],
|
|
11
|
+
slowThreshold: 1000, // ms
|
|
12
|
+
httpTimeout: 5000, // ms
|
|
13
|
+
thresholds: {
|
|
14
|
+
dnsTime: { warning: 500, critical: 2000, direction: 'above' },
|
|
15
|
+
httpTime: { warning: 1000, critical: 3000, direction: 'above' },
|
|
16
|
+
latencyTime: { warning: 300, critical: 1000, direction: 'above' },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
|
+
import type { PollingNpcConfig } from '../../core/polling-npc.js';
|
|
3
|
+
import type { CollectedMetrics, Status, ThresholdConfig, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
|
|
4
|
+
import type { LatencyHost } from './defaults.js';
|
|
5
|
+
export interface ConnectivityThresholds {
|
|
6
|
+
dnsTime: ThresholdConfig;
|
|
7
|
+
httpTime: ThresholdConfig;
|
|
8
|
+
latencyTime: ThresholdConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface InternetConnectivityConfig extends PollingNpcConfig {
|
|
11
|
+
dnsHosts?: string[];
|
|
12
|
+
httpUrls?: string[];
|
|
13
|
+
latencyHosts?: LatencyHost[] | string[];
|
|
14
|
+
slowThreshold?: number;
|
|
15
|
+
httpTimeout?: number;
|
|
16
|
+
thresholds?: ConnectivityThresholds;
|
|
17
|
+
}
|
|
18
|
+
interface ConnectivitySample {
|
|
19
|
+
allFailed: boolean;
|
|
20
|
+
anyFailed: boolean;
|
|
21
|
+
dnsAvg: number;
|
|
22
|
+
httpAvg: number;
|
|
23
|
+
latencyAvg: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive overall status from live per-check thresholds. All checks failing is
|
|
27
|
+
* critical; any partial failure is at least a warning; slow-but-working checks
|
|
28
|
+
* escalate per their own thresholds.
|
|
29
|
+
*/
|
|
30
|
+
export declare function evaluateConnectivityStatus(sample: ConnectivitySample, thresholds: ConnectivityThresholds): Status;
|
|
31
|
+
export declare class InternetConnectivityNPC extends PollingNPC {
|
|
32
|
+
private dnsHosts;
|
|
33
|
+
private httpUrls;
|
|
34
|
+
private latencyHosts;
|
|
35
|
+
private httpTimeout;
|
|
36
|
+
private thresholds;
|
|
37
|
+
private dnsHistory;
|
|
38
|
+
private httpHistory;
|
|
39
|
+
private latencyHistory;
|
|
40
|
+
constructor(config: InternetConnectivityConfig);
|
|
41
|
+
get name(): string;
|
|
42
|
+
protected get defaultDisplayName(): string;
|
|
43
|
+
get archetype(): Archetype;
|
|
44
|
+
get behavior(): BehaviorHints;
|
|
45
|
+
get companion(): CompanionDecl;
|
|
46
|
+
protected collect(): Promise<CollectedMetrics>;
|
|
47
|
+
private buildOnlineMessage;
|
|
48
|
+
private buildOfflineMessage;
|
|
49
|
+
/**
|
|
50
|
+
* Build config from CLI flags. Merges with any programmatic overrides.
|
|
51
|
+
*/
|
|
52
|
+
static configFromFlags(flags: Record<string, string | boolean>): Partial<InternetConnectivityConfig>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=index.d.ts.map
|