@nocville/npc 0.0.0-stage → 0.2.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.
Files changed (210) hide show
  1. package/README.md +243 -2
  2. package/dist/cjs/cli.js +533 -0
  3. package/dist/cjs/core/api-key.js +69 -0
  4. package/dist/cjs/core/base-npc.js +160 -0
  5. package/dist/cjs/core/config.js +108 -0
  6. package/dist/cjs/core/event-npc.js +14 -0
  7. package/dist/cjs/core/logger.js +39 -0
  8. package/dist/cjs/core/polling-npc.js +65 -0
  9. package/dist/cjs/core/screen.js +28 -0
  10. package/dist/cjs/core/text.js +15 -0
  11. package/dist/cjs/core/thresholds.js +48 -0
  12. package/dist/cjs/core/types.js +10 -0
  13. package/dist/cjs/core/user-config.js +92 -0
  14. package/dist/cjs/doctor.js +130 -0
  15. package/dist/cjs/index.js +90 -0
  16. package/dist/cjs/npcs/agent-common/activity.js +291 -0
  17. package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
  18. package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
  19. package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
  20. package/dist/cjs/npcs/agent-common/liveness.js +112 -0
  21. package/dist/cjs/npcs/agent-common/lock.js +99 -0
  22. package/dist/cjs/npcs/agent-common/runtime.js +154 -0
  23. package/dist/cjs/npcs/agent-common/session.js +390 -0
  24. package/dist/cjs/npcs/agent-common/shim.js +96 -0
  25. package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
  26. package/dist/cjs/npcs/claude-code/defaults.js +20 -0
  27. package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
  28. package/dist/cjs/npcs/claude-code/index.js +104 -0
  29. package/dist/cjs/npcs/claude-code/setup.js +133 -0
  30. package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
  31. package/dist/cjs/npcs/codex/defaults.js +32 -0
  32. package/dist/cjs/npcs/codex/hook-handler.js +116 -0
  33. package/dist/cjs/npcs/codex/index.js +92 -0
  34. package/dist/cjs/npcs/codex/rollout.js +246 -0
  35. package/dist/cjs/npcs/codex/setup.js +104 -0
  36. package/dist/cjs/npcs/codex/watcher.js +346 -0
  37. package/dist/cjs/npcs/cursor/defaults.js +15 -0
  38. package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
  39. package/dist/cjs/npcs/cursor/index.js +72 -0
  40. package/dist/cjs/npcs/cursor/normalize.js +54 -0
  41. package/dist/cjs/npcs/cursor/setup.js +98 -0
  42. package/dist/cjs/npcs/gemini/defaults.js +15 -0
  43. package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
  44. package/dist/cjs/npcs/gemini/index.js +69 -0
  45. package/dist/cjs/npcs/gemini/setup.js +98 -0
  46. package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
  47. package/dist/cjs/npcs/github-watcher/format.js +21 -0
  48. package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
  49. package/dist/cjs/npcs/github-watcher/index.js +148 -0
  50. package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
  51. package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
  52. package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
  53. package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
  54. package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
  55. package/dist/cjs/npcs/log-watcher/index.js +161 -0
  56. package/dist/cjs/npcs/log-watcher/tail.js +81 -0
  57. package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
  58. package/dist/cjs/npcs/process-watcher/index.js +140 -0
  59. package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
  60. package/dist/cjs/npcs/receptionist/defaults.js +9 -0
  61. package/dist/cjs/npcs/receptionist/index.js +120 -0
  62. package/dist/cjs/npcs/receptionist/weather.js +129 -0
  63. package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
  64. package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
  65. package/dist/cjs/npcs/system-monitor/index.js +105 -0
  66. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
  67. package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
  68. package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
  69. package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
  70. package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
  71. package/dist/cjs/package.json +1 -0
  72. package/dist/cli.d.ts +3 -0
  73. package/dist/cli.js +498 -0
  74. package/dist/core/api-key.d.ts +34 -0
  75. package/dist/core/api-key.js +64 -0
  76. package/dist/core/base-npc.d.ts +41 -0
  77. package/dist/core/base-npc.js +156 -0
  78. package/dist/core/config.d.ts +22 -0
  79. package/dist/core/config.js +101 -0
  80. package/dist/core/event-npc.d.ts +8 -0
  81. package/dist/core/event-npc.js +10 -0
  82. package/dist/core/logger.d.ts +11 -0
  83. package/dist/core/logger.js +35 -0
  84. package/dist/core/polling-npc.d.ts +18 -0
  85. package/dist/core/polling-npc.js +61 -0
  86. package/dist/core/screen.d.ts +11 -0
  87. package/dist/core/screen.js +24 -0
  88. package/dist/core/text.d.ts +6 -0
  89. package/dist/core/text.js +12 -0
  90. package/dist/core/thresholds.d.ts +10 -0
  91. package/dist/core/thresholds.js +44 -0
  92. package/dist/core/types.d.ts +60 -0
  93. package/dist/core/types.js +7 -0
  94. package/dist/core/user-config.d.ts +43 -0
  95. package/dist/core/user-config.js +85 -0
  96. package/dist/doctor.d.ts +8 -0
  97. package/dist/doctor.js +126 -0
  98. package/dist/index.d.ts +39 -0
  99. package/dist/index.js +25 -0
  100. package/dist/npcs/agent-common/activity.d.ts +42 -0
  101. package/dist/npcs/agent-common/activity.js +280 -0
  102. package/dist/npcs/agent-common/git-stats.d.ts +11 -0
  103. package/dist/npcs/agent-common/git-stats.js +67 -0
  104. package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
  105. package/dist/npcs/agent-common/hook-runner.js +245 -0
  106. package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
  107. package/dist/npcs/agent-common/hook-settings.js +265 -0
  108. package/dist/npcs/agent-common/liveness.d.ts +12 -0
  109. package/dist/npcs/agent-common/liveness.js +105 -0
  110. package/dist/npcs/agent-common/lock.d.ts +6 -0
  111. package/dist/npcs/agent-common/lock.js +95 -0
  112. package/dist/npcs/agent-common/runtime.d.ts +16 -0
  113. package/dist/npcs/agent-common/runtime.js +149 -0
  114. package/dist/npcs/agent-common/session.d.ts +184 -0
  115. package/dist/npcs/agent-common/session.js +368 -0
  116. package/dist/npcs/agent-common/shim.d.ts +4 -0
  117. package/dist/npcs/agent-common/shim.js +90 -0
  118. package/dist/npcs/agent-common/watchdog.d.ts +16 -0
  119. package/dist/npcs/agent-common/watchdog.js +144 -0
  120. package/dist/npcs/claude-code/defaults.d.ts +15 -0
  121. package/dist/npcs/claude-code/defaults.js +17 -0
  122. package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
  123. package/dist/npcs/claude-code/hook-handler.js +131 -0
  124. package/dist/npcs/claude-code/index.d.ts +36 -0
  125. package/dist/npcs/claude-code/index.js +92 -0
  126. package/dist/npcs/claude-code/setup.d.ts +58 -0
  127. package/dist/npcs/claude-code/setup.js +121 -0
  128. package/dist/npcs/claude-code/state-machine.d.ts +28 -0
  129. package/dist/npcs/claude-code/state-machine.js +125 -0
  130. package/dist/npcs/codex/defaults.d.ts +24 -0
  131. package/dist/npcs/codex/defaults.js +28 -0
  132. package/dist/npcs/codex/hook-handler.d.ts +24 -0
  133. package/dist/npcs/codex/hook-handler.js +110 -0
  134. package/dist/npcs/codex/index.d.ts +21 -0
  135. package/dist/npcs/codex/index.js +76 -0
  136. package/dist/npcs/codex/rollout.d.ts +39 -0
  137. package/dist/npcs/codex/rollout.js +242 -0
  138. package/dist/npcs/codex/setup.d.ts +37 -0
  139. package/dist/npcs/codex/setup.js +96 -0
  140. package/dist/npcs/codex/watcher.d.ts +62 -0
  141. package/dist/npcs/codex/watcher.js +341 -0
  142. package/dist/npcs/cursor/defaults.d.ts +9 -0
  143. package/dist/npcs/cursor/defaults.js +11 -0
  144. package/dist/npcs/cursor/hook-handler.d.ts +8 -0
  145. package/dist/npcs/cursor/hook-handler.js +60 -0
  146. package/dist/npcs/cursor/index.d.ts +18 -0
  147. package/dist/npcs/cursor/index.js +57 -0
  148. package/dist/npcs/cursor/normalize.d.ts +6 -0
  149. package/dist/npcs/cursor/normalize.js +49 -0
  150. package/dist/npcs/cursor/setup.d.ts +29 -0
  151. package/dist/npcs/cursor/setup.js +90 -0
  152. package/dist/npcs/gemini/defaults.d.ts +9 -0
  153. package/dist/npcs/gemini/defaults.js +11 -0
  154. package/dist/npcs/gemini/hook-handler.d.ts +10 -0
  155. package/dist/npcs/gemini/hook-handler.js +87 -0
  156. package/dist/npcs/gemini/index.d.ts +17 -0
  157. package/dist/npcs/gemini/index.js +56 -0
  158. package/dist/npcs/gemini/setup.d.ts +29 -0
  159. package/dist/npcs/gemini/setup.js +90 -0
  160. package/dist/npcs/github-watcher/defaults.d.ts +8 -0
  161. package/dist/npcs/github-watcher/defaults.js +8 -0
  162. package/dist/npcs/github-watcher/format.d.ts +3 -0
  163. package/dist/npcs/github-watcher/format.js +18 -0
  164. package/dist/npcs/github-watcher/github-api.d.ts +55 -0
  165. package/dist/npcs/github-watcher/github-api.js +120 -0
  166. package/dist/npcs/github-watcher/index.d.ts +35 -0
  167. package/dist/npcs/github-watcher/index.js +144 -0
  168. package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
  169. package/dist/npcs/internet-connectivity/checks.js +84 -0
  170. package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
  171. package/dist/npcs/internet-connectivity/defaults.js +19 -0
  172. package/dist/npcs/internet-connectivity/index.d.ts +55 -0
  173. package/dist/npcs/internet-connectivity/index.js +246 -0
  174. package/dist/npcs/log-watcher/buffer.d.ts +18 -0
  175. package/dist/npcs/log-watcher/buffer.js +34 -0
  176. package/dist/npcs/log-watcher/defaults.d.ts +9 -0
  177. package/dist/npcs/log-watcher/defaults.js +9 -0
  178. package/dist/npcs/log-watcher/index.d.ts +45 -0
  179. package/dist/npcs/log-watcher/index.js +157 -0
  180. package/dist/npcs/log-watcher/tail.d.ts +9 -0
  181. package/dist/npcs/log-watcher/tail.js +78 -0
  182. package/dist/npcs/process-watcher/defaults.d.ts +6 -0
  183. package/dist/npcs/process-watcher/defaults.js +6 -0
  184. package/dist/npcs/process-watcher/index.d.ts +26 -0
  185. package/dist/npcs/process-watcher/index.js +136 -0
  186. package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
  187. package/dist/npcs/process-watcher/process-scanner.js +110 -0
  188. package/dist/npcs/receptionist/defaults.d.ts +6 -0
  189. package/dist/npcs/receptionist/defaults.js +6 -0
  190. package/dist/npcs/receptionist/index.d.ts +29 -0
  191. package/dist/npcs/receptionist/index.js +116 -0
  192. package/dist/npcs/receptionist/weather.d.ts +37 -0
  193. package/dist/npcs/receptionist/weather.js +121 -0
  194. package/dist/npcs/system-monitor/collectors.d.ts +45 -0
  195. package/dist/npcs/system-monitor/collectors.js +60 -0
  196. package/dist/npcs/system-monitor/defaults.d.ts +7 -0
  197. package/dist/npcs/system-monitor/defaults.js +10 -0
  198. package/dist/npcs/system-monitor/index.d.ts +27 -0
  199. package/dist/npcs/system-monitor/index.js +101 -0
  200. package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
  201. package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
  202. package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
  203. package/dist/npcs/web-page-watcher/capture.js +75 -0
  204. package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
  205. package/dist/npcs/web-page-watcher/defaults.js +27 -0
  206. package/dist/npcs/web-page-watcher/index.d.ts +42 -0
  207. package/dist/npcs/web-page-watcher/index.js +230 -0
  208. package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
  209. package/dist/npcs/web-page-watcher/pixelate.js +172 -0
  210. package/package.json +73 -3
@@ -0,0 +1,45 @@
1
+ import { BaseNPC } from '../../core/base-npc.js';
2
+ import type { NpcConfig, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
3
+ export interface LogWatcherConfig extends NpcConfig {
4
+ files: string[];
5
+ errorPattern?: string;
6
+ warningPattern?: string;
7
+ windowSeconds?: number;
8
+ errorWarn?: number;
9
+ errorCrit?: number;
10
+ reportInterval?: number;
11
+ }
12
+ export declare class LogWatcherNPC extends BaseNPC {
13
+ private tailers;
14
+ private timer;
15
+ private entries;
16
+ private recentLines;
17
+ private lastBurstPush;
18
+ private errorRegex;
19
+ private warningRegex;
20
+ private windowMs;
21
+ private errorWarn;
22
+ private errorCrit;
23
+ private reportInterval;
24
+ private files;
25
+ constructor(config: LogWatcherConfig);
26
+ get name(): string;
27
+ protected get defaultDisplayName(): string;
28
+ get archetype(): Archetype;
29
+ get heartbeatSeconds(): number;
30
+ get behavior(): BehaviorHints;
31
+ get companion(): CompanionDecl;
32
+ protected onStart(): Promise<void>;
33
+ protected onStop(): Promise<void>;
34
+ private handleLine;
35
+ /** A fresh error triggers an immediate push, debounced to at most once / 5s. */
36
+ private onErrorBurst;
37
+ private pruneWindow;
38
+ private report;
39
+ /**
40
+ * Build and push the current status. If `overrideMessage` is set (burst push),
41
+ * it drives the bubble and the status floors at warning.
42
+ */
43
+ private pushReport;
44
+ }
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { basename } from 'node:path';
2
+ import { BaseNPC } from '../../core/base-npc.js';
3
+ import { evaluateThreshold, worstStatus } from '../../core/thresholds.js';
4
+ import { truncate } from '../../core/text.js';
5
+ import { createTailer } from './tail.js';
6
+ import { classifyLine, pushRecent, buildLogScreen, shouldEmitBurst } from './buffer.js';
7
+ import { DEFAULTS } from './defaults.js';
8
+ export class LogWatcherNPC extends BaseNPC {
9
+ tailers = [];
10
+ timer = null;
11
+ entries = [];
12
+ recentLines = [];
13
+ lastBurstPush = 0;
14
+ errorRegex;
15
+ warningRegex;
16
+ windowMs;
17
+ errorWarn;
18
+ errorCrit;
19
+ reportInterval;
20
+ files;
21
+ constructor(config) {
22
+ super(config);
23
+ this.files = config.files;
24
+ this.errorRegex = new RegExp(config.errorPattern ?? DEFAULTS.errorPattern);
25
+ this.warningRegex = new RegExp(config.warningPattern ?? DEFAULTS.warningPattern);
26
+ this.windowMs = (config.windowSeconds ?? DEFAULTS.windowSeconds) * 1000;
27
+ this.errorWarn = config.errorWarn ?? DEFAULTS.errorWarn;
28
+ this.errorCrit = config.errorCrit ?? DEFAULTS.errorCrit;
29
+ this.reportInterval = config.reportInterval ?? DEFAULTS.reportInterval;
30
+ }
31
+ get name() {
32
+ return 'log-watcher';
33
+ }
34
+ get defaultDisplayName() {
35
+ const first = this.files[0];
36
+ return first ? `Logs: ${basename(first)}` : 'Logs';
37
+ }
38
+ get archetype() {
39
+ return 'service';
40
+ }
41
+ get heartbeatSeconds() {
42
+ return 30;
43
+ }
44
+ get behavior() {
45
+ return { idle: 'wander' };
46
+ }
47
+ get companion() {
48
+ return {
49
+ screenType: 'log-terminal',
50
+ size: { w: 2, h: 2 },
51
+ placement: 'adjacent-wall',
52
+ label: this.files[0] ? basename(this.files[0]) : 'log',
53
+ };
54
+ }
55
+ async onStart() {
56
+ for (const filePath of this.files) {
57
+ this.logger.info(`Tailing ${filePath}`);
58
+ const tailer = createTailer(filePath, (line) => {
59
+ this.handleLine(line);
60
+ });
61
+ this.tailers.push(tailer);
62
+ }
63
+ // Report immediately, then on interval
64
+ await this.report();
65
+ this.timer = setInterval(() => {
66
+ void this.report();
67
+ }, this.reportInterval);
68
+ }
69
+ async onStop() {
70
+ if (this.timer) {
71
+ clearInterval(this.timer);
72
+ this.timer = null;
73
+ }
74
+ for (const tailer of this.tailers) {
75
+ tailer.close();
76
+ }
77
+ this.tailers = [];
78
+ }
79
+ handleLine(line) {
80
+ const now = Date.now();
81
+ const level = classifyLine(line, this.errorRegex, this.warningRegex);
82
+ // Ring buffer holds ALL lines (matched ones flagged) for the log TV.
83
+ pushRecent(this.recentLines, line, level);
84
+ if (level === 'error') {
85
+ this.entries.push({ timestamp: now, type: 'error', message: line });
86
+ this.onErrorBurst(line);
87
+ }
88
+ else if (level === 'warning') {
89
+ this.entries.push({ timestamp: now, type: 'warning', message: line });
90
+ }
91
+ }
92
+ /** A fresh error triggers an immediate push, debounced to at most once / 5s. */
93
+ onErrorBurst(line) {
94
+ const now = Date.now();
95
+ if (!shouldEmitBurst(this.lastBurstPush, now))
96
+ return;
97
+ this.lastBurstPush = now;
98
+ void this.pushReport(truncate(line, 200), 'error');
99
+ }
100
+ pruneWindow() {
101
+ const cutoff = Date.now() - this.windowMs;
102
+ this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
103
+ }
104
+ async report() {
105
+ if (this.state !== 'running')
106
+ return;
107
+ await this.pushReport();
108
+ }
109
+ /**
110
+ * Build and push the current status. If `overrideMessage` is set (burst push),
111
+ * it drives the bubble and the status floors at warning.
112
+ */
113
+ async pushReport(overrideMessage, subStatus) {
114
+ if (this.state !== 'running')
115
+ return;
116
+ this.pruneWindow();
117
+ const errorCount = this.entries.filter((e) => e.type === 'error').length;
118
+ const warningCount = this.entries.filter((e) => e.type === 'warning').length;
119
+ const windowSeconds = this.windowMs / 1000;
120
+ let status = evaluateThreshold(errorCount, {
121
+ warning: this.errorWarn,
122
+ critical: this.errorCrit,
123
+ direction: 'above',
124
+ });
125
+ if (overrideMessage)
126
+ status = worstStatus([status, 'warning']);
127
+ const mood = status === 'critical' ? 'stressed' : status === 'warning' ? 'concerned' : 'happy';
128
+ const lastError = this.entries.filter((e) => e.type === 'error').at(-1);
129
+ const message = overrideMessage ??
130
+ (errorCount > 0
131
+ ? `${errorCount} error${errorCount !== 1 ? 's' : ''} in last ${windowSeconds / 60}min across ${this.files.length} file${this.files.length !== 1 ? 's' : ''}`
132
+ : `No errors in last ${windowSeconds / 60}min`);
133
+ const metrics = {
134
+ status,
135
+ mood,
136
+ message,
137
+ subStatus,
138
+ screenData: buildLogScreen(this.recentLines),
139
+ metrics: {
140
+ error_count: errorCount,
141
+ warning_count: warningCount,
142
+ window_seconds: windowSeconds,
143
+ files_watched: this.files.length,
144
+ last_error_message: lastError?.message ?? '',
145
+ },
146
+ };
147
+ try {
148
+ await this.pushMetrics(metrics);
149
+ }
150
+ catch (err) {
151
+ this.logger.error('Failed to push metrics', {
152
+ error: err instanceof Error ? err.message : String(err),
153
+ });
154
+ }
155
+ }
156
+ }
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ export interface FileTailer {
2
+ close(): void;
3
+ }
4
+ /**
5
+ * Tail a file by watching for changes and reading new lines from the last
6
+ * known byte offset. Handles file rotation (size shrinks -> reset to 0).
7
+ */
8
+ export declare function createTailer(filePath: string, onLine: (line: string) => void): FileTailer;
9
+ //# sourceMappingURL=tail.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { watch, createReadStream, promises as fsp } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ /**
4
+ * Tail a file by watching for changes and reading new lines from the last
5
+ * known byte offset. Handles file rotation (size shrinks -> reset to 0).
6
+ */
7
+ export function createTailer(filePath, onLine) {
8
+ let offset = 0;
9
+ let reading = false;
10
+ let watcher = null;
11
+ let closed = false;
12
+ async function init() {
13
+ // Start at the end of the file so we only read new lines
14
+ try {
15
+ const stats = await fsp.stat(filePath);
16
+ offset = stats.size;
17
+ }
18
+ catch {
19
+ // File may not exist yet; start at 0
20
+ offset = 0;
21
+ }
22
+ watcher = watch(filePath, () => {
23
+ if (!closed) {
24
+ void readNewLines();
25
+ }
26
+ });
27
+ // Ignore watcher errors (e.g. file deleted then recreated)
28
+ watcher.on('error', () => { });
29
+ }
30
+ async function readNewLines() {
31
+ if (reading || closed)
32
+ return;
33
+ reading = true;
34
+ try {
35
+ const stats = await fsp.stat(filePath);
36
+ // File rotation detected: size smaller than our offset
37
+ if (stats.size < offset) {
38
+ offset = 0;
39
+ }
40
+ // Nothing new to read
41
+ if (stats.size === offset) {
42
+ reading = false;
43
+ return;
44
+ }
45
+ const stream = createReadStream(filePath, {
46
+ start: offset,
47
+ encoding: 'utf-8',
48
+ });
49
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
50
+ let bytesRead = 0;
51
+ for await (const line of rl) {
52
+ if (closed)
53
+ break;
54
+ // Account for line content + newline character
55
+ bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
56
+ onLine(line);
57
+ }
58
+ offset += bytesRead;
59
+ }
60
+ catch {
61
+ // File may have been removed; ignore and wait for next change
62
+ }
63
+ finally {
64
+ reading = false;
65
+ }
66
+ }
67
+ void init();
68
+ return {
69
+ close() {
70
+ closed = true;
71
+ if (watcher) {
72
+ watcher.close();
73
+ watcher = null;
74
+ }
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=tail.js.map
@@ -0,0 +1,6 @@
1
+ export declare const DEFAULTS: {
2
+ pollInterval: number;
3
+ cpuWarn: number;
4
+ memWarn: number;
5
+ };
6
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const DEFAULTS = {
2
+ pollInterval: 15000, // 15 seconds
3
+ cpuWarn: 80, // percent
4
+ memWarn: 500, // MB
5
+ };
6
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,26 @@
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
+ export interface ProcessWatcherConfig extends PollingNpcConfig {
5
+ /** List of process names to watch */
6
+ processes: string[];
7
+ /** CPU usage percent threshold for warning status (default: 80) */
8
+ cpuWarn?: number;
9
+ /** Memory usage MB threshold for warning status (default: 500) */
10
+ memWarn?: number;
11
+ }
12
+ export declare class ProcessWatcherNPC extends PollingNPC {
13
+ private processes;
14
+ private cpuWarn;
15
+ private memWarn;
16
+ private cpuHistories;
17
+ private previousRunning;
18
+ constructor(config: ProcessWatcherConfig);
19
+ get name(): string;
20
+ protected get defaultDisplayName(): string;
21
+ get archetype(): Archetype;
22
+ get heartbeatSeconds(): number;
23
+ get companion(): CompanionDecl;
24
+ protected collect(): Promise<CollectedMetrics>;
25
+ }
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,136 @@
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import { worstStatus } from '../../core/thresholds.js';
3
+ import { pushHistory } from '../../core/screen.js';
4
+ import { scanProcesses, detectTransitions } from './process-scanner.js';
5
+ import { DEFAULTS } from './defaults.js';
6
+ /** Max per-process panels on the companion dashboard. */
7
+ const MAX_PANELS = 8;
8
+ export class ProcessWatcherNPC extends PollingNPC {
9
+ processes;
10
+ cpuWarn;
11
+ memWarn;
12
+ cpuHistories = new Map();
13
+ previousRunning = null;
14
+ constructor(config) {
15
+ if (process.platform === 'win32') {
16
+ throw new Error('process-watcher is not supported on Windows (relies on the POSIX `ps` command). ' +
17
+ 'Run it under WSL/Linux, or use system-monitor instead.');
18
+ }
19
+ super(config);
20
+ this.processes = config.processes;
21
+ this.cpuWarn = config.cpuWarn ?? DEFAULTS.cpuWarn;
22
+ this.memWarn = config.memWarn ?? DEFAULTS.memWarn;
23
+ }
24
+ get name() {
25
+ return 'process-watcher';
26
+ }
27
+ get defaultDisplayName() {
28
+ return 'Processes';
29
+ }
30
+ get archetype() {
31
+ return 'service';
32
+ }
33
+ get heartbeatSeconds() {
34
+ return 30;
35
+ }
36
+ get companion() {
37
+ return {
38
+ screenType: 'dashboard-wall',
39
+ size: { w: 2, h: 2 },
40
+ placement: 'on-desk',
41
+ label: 'Processes',
42
+ };
43
+ }
44
+ async collect() {
45
+ const infos = await scanProcesses(this.processes);
46
+ const statuses = [];
47
+ const messageParts = [];
48
+ const metrics = {};
49
+ const panels = [];
50
+ let totalRunning = 0;
51
+ let totalMissing = 0;
52
+ for (const info of infos) {
53
+ const safeKey = info.name.replace(/[^a-zA-Z0-9_]/g, '_');
54
+ if (!info.running) {
55
+ statuses.push('critical');
56
+ messageParts.push(`${info.name}: NOT FOUND`);
57
+ metrics[`${safeKey}_running`] = 0;
58
+ metrics[`${safeKey}_cpu`] = 0;
59
+ metrics[`${safeKey}_mem_mb`] = 0;
60
+ totalMissing++;
61
+ if (panels.length < MAX_PANELS) {
62
+ panels.push({ label: info.name, value: 'DOWN', color: 'red' });
63
+ }
64
+ continue;
65
+ }
66
+ totalRunning++;
67
+ const cpu = info.cpuPercent ?? 0;
68
+ metrics[`${safeKey}_running`] = 1;
69
+ metrics[`${safeKey}_cpu`] = cpu;
70
+ metrics[`${safeKey}_mem_mb`] = info.memMb ?? 0;
71
+ const history = this.cpuHistories.get(info.name) ?? [];
72
+ pushHistory(history, cpu);
73
+ this.cpuHistories.set(info.name, history);
74
+ const cpuHigh = cpu >= this.cpuWarn;
75
+ const memHigh = (info.memMb ?? 0) >= this.memWarn;
76
+ if (cpuHigh || memHigh) {
77
+ statuses.push('warning');
78
+ const warnings = [];
79
+ if (cpuHigh)
80
+ warnings.push(`cpu=${cpu}%`);
81
+ if (memHigh)
82
+ warnings.push(`mem=${info.memMb}MB`);
83
+ messageParts.push(`${info.name}: WARN (${warnings.join(', ')})`);
84
+ }
85
+ else {
86
+ statuses.push('healthy');
87
+ messageParts.push(`${info.name}: OK`);
88
+ }
89
+ if (panels.length < MAX_PANELS) {
90
+ panels.push({
91
+ label: info.name,
92
+ value: `${cpu}%`,
93
+ color: cpuHigh || memHigh ? 'amber' : 'green',
94
+ sparkline: [...history],
95
+ });
96
+ }
97
+ }
98
+ metrics['total_watched'] = this.processes.length;
99
+ metrics['total_running'] = totalRunning;
100
+ metrics['total_missing'] = totalMissing;
101
+ // Transition speech: compare the running set to last poll's. On the first
102
+ // poll we only seed the set (no bogus "is back" for already-running procs).
103
+ const currentRunning = new Set(infos.filter((i) => i.running).map((i) => i.name));
104
+ let transitions = [];
105
+ if (this.previousRunning !== null) {
106
+ transitions = detectTransitions(this.previousRunning, currentRunning);
107
+ }
108
+ this.previousRunning = currentRunning;
109
+ const status = worstStatus(statuses);
110
+ const mood = deriveMood(status);
111
+ // A death message takes over the bubble this cycle so it reads as an alert.
112
+ const message = transitions.length > 0 ? transitions.join(' · ') : messageParts.join(' | ');
113
+ const screenData = { layout: 'multi-metric', values: panels };
114
+ return {
115
+ status,
116
+ mood,
117
+ message,
118
+ subStatus: totalMissing > 0 ? 'process-down' : undefined,
119
+ screenData,
120
+ metrics,
121
+ };
122
+ }
123
+ }
124
+ function deriveMood(status) {
125
+ switch (status) {
126
+ case 'healthy':
127
+ return 'happy';
128
+ case 'warning':
129
+ return 'concerned';
130
+ case 'critical':
131
+ return 'stressed';
132
+ default:
133
+ return 'idle';
134
+ }
135
+ }
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ export interface ProcessInfo {
2
+ name: string;
3
+ running: boolean;
4
+ pid?: number;
5
+ cpuPercent?: number;
6
+ memMb?: number;
7
+ }
8
+ /**
9
+ * Compare the previous and current running-process sets and return speech
10
+ * strings for any transitions: "<name> died!" when a process disappears and
11
+ * "<name> is back" when one returns.
12
+ */
13
+ export declare function detectTransitions(prev: Set<string>, curr: Set<string>): string[];
14
+ /**
15
+ * Scan for processes by name and return their status and resource usage.
16
+ * Works on macOS and Linux by using `ps` to list all processes.
17
+ */
18
+ export declare function scanProcesses(names: string[]): Promise<ProcessInfo[]>;
19
+ //# sourceMappingURL=process-scanner.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ /**
5
+ * Compare the previous and current running-process sets and return speech
6
+ * strings for any transitions: "<name> died!" when a process disappears and
7
+ * "<name> is back" when one returns.
8
+ */
9
+ export function detectTransitions(prev, curr) {
10
+ const messages = [];
11
+ for (const name of prev) {
12
+ if (!curr.has(name))
13
+ messages.push(`${name} died!`);
14
+ }
15
+ for (const name of curr) {
16
+ if (!prev.has(name))
17
+ messages.push(`${name} is back`);
18
+ }
19
+ return messages;
20
+ }
21
+ /**
22
+ * Parse the output of `ps` into structured entries.
23
+ * Expects columns: PID, %CPU, RSS (KB), COMMAND
24
+ */
25
+ function parsePsOutput(stdout) {
26
+ const lines = stdout.trim().split('\n');
27
+ // Skip the header line
28
+ const entries = [];
29
+ for (let i = 1; i < lines.length; i++) {
30
+ const line = lines[i].trim();
31
+ if (!line)
32
+ continue;
33
+ // Fields are whitespace-separated: PID, %CPU, RSS, COMMAND (may contain spaces)
34
+ const parts = line.split(/\s+/);
35
+ if (parts.length < 4)
36
+ continue;
37
+ const pid = parseInt(parts[0], 10);
38
+ const cpu = parseFloat(parts[1]);
39
+ const memKb = parseInt(parts[2], 10);
40
+ // COMMAND is everything from the 4th column onward
41
+ const comm = parts.slice(3).join(' ');
42
+ if (Number.isNaN(pid) || Number.isNaN(cpu) || Number.isNaN(memKb))
43
+ continue;
44
+ entries.push({ pid, cpu, memKb, comm });
45
+ }
46
+ return entries;
47
+ }
48
+ /**
49
+ * Match a process name against a ps COMMAND column.
50
+ * Matches if the basename of the command path equals the search name,
51
+ * or if the command contains the search name.
52
+ */
53
+ function matchesName(comm, searchName) {
54
+ // Get the basename of the command (strip path and arguments)
55
+ const basename = comm.split('/').pop()?.split(/\s/)[0] ?? '';
56
+ if (basename === searchName)
57
+ return true;
58
+ // Also check if the full command string contains the name
59
+ // (handles cases like "python my-script.py" when searching for "python")
60
+ return comm.toLowerCase().includes(searchName.toLowerCase());
61
+ }
62
+ /**
63
+ * Scan for processes by name and return their status and resource usage.
64
+ * Works on macOS and Linux by using `ps` to list all processes.
65
+ */
66
+ export async function scanProcesses(names) {
67
+ let psArgs;
68
+ if (process.platform === 'darwin') {
69
+ // macOS: -e selects all processes, -o specifies output columns
70
+ psArgs = ['-eo', 'pid,pcpu,rss,comm'];
71
+ }
72
+ else {
73
+ // Linux: -eo with similar columns
74
+ psArgs = ['-eo', 'pid,pcpu,rss,comm'];
75
+ }
76
+ let entries;
77
+ try {
78
+ const { stdout } = await execFileAsync('ps', psArgs);
79
+ entries = parsePsOutput(stdout);
80
+ }
81
+ catch {
82
+ // If ps fails, report all processes as unknown / not running
83
+ return names.map((name) => ({ name, running: false }));
84
+ }
85
+ const results = [];
86
+ for (const name of names) {
87
+ // Find all matching entries for this process name
88
+ const matching = entries.filter((e) => matchesName(e.comm, name));
89
+ if (matching.length === 0) {
90
+ results.push({ name, running: false });
91
+ continue;
92
+ }
93
+ // Aggregate: take the first matching PID, sum CPU and memory across all instances
94
+ let totalCpu = 0;
95
+ let totalMemKb = 0;
96
+ for (const entry of matching) {
97
+ totalCpu += entry.cpu;
98
+ totalMemKb += entry.memKb;
99
+ }
100
+ results.push({
101
+ name,
102
+ running: true,
103
+ pid: matching[0].pid,
104
+ cpuPercent: Math.round(totalCpu * 100) / 100,
105
+ memMb: Math.round((totalMemKb / 1024) * 100) / 100,
106
+ });
107
+ }
108
+ return results;
109
+ }
110
+ //# sourceMappingURL=process-scanner.js.map
@@ -0,0 +1,6 @@
1
+ export declare const DEFAULTS: {
2
+ pollInterval: number;
3
+ weatherRefreshMs: number;
4
+ fetchTimeoutMs: number;
5
+ };
6
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const DEFAULTS = {
2
+ pollInterval: 60000, // 60s clock tick
3
+ weatherRefreshMs: 15 * 60 * 1000, // refresh weather every 15 minutes
4
+ fetchTimeoutMs: 5000,
5
+ };
6
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,29 @@
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import type { PollingNpcConfig } from '../../core/polling-npc.js';
3
+ import type { CollectedMetrics, Archetype, BehaviorHints, CompanionDecl } from '../../core/types.js';
4
+ export interface ReceptionistConfig extends PollingNpcConfig {
5
+ lat?: number;
6
+ lon?: number;
7
+ location?: string;
8
+ /** Display temperature in Fahrenheit instead of Celsius. */
9
+ fahrenheit?: boolean;
10
+ }
11
+ export declare class ReceptionistNPC extends PollingNPC {
12
+ private explicitCoords;
13
+ private location?;
14
+ private fahrenheit;
15
+ private coords;
16
+ private geoAttempted;
17
+ private weather;
18
+ private lastWeatherFetch;
19
+ constructor(config: ReceptionistConfig);
20
+ get name(): string;
21
+ protected get defaultDisplayName(): string;
22
+ get archetype(): Archetype;
23
+ get behavior(): BehaviorHints;
24
+ get companion(): CompanionDecl;
25
+ protected collect(): Promise<CollectedMetrics>;
26
+ private refreshWeather;
27
+ private resolveCoords;
28
+ }
29
+ //# sourceMappingURL=index.d.ts.map