@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,341 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, readSync, readdirSync, statSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { NocvilleClient } from '@nocville/node';
4
+ import { Logger } from '../../core/logger.js';
5
+ import { buildAgentPayload, pushHistory, stateFilePath } from '../agent-common/session.js';
6
+ import { CODEX_DEFAULTS } from './defaults.js';
7
+ import { CODEX_ADAPTER, codexServiceId } from './hook-handler.js';
8
+ import { parseRolloutLine } from './rollout.js';
9
+ const pad = (n) => String(n).padStart(2, '0');
10
+ /** Day directories to scan: today and yesterday (local time, like Codex). */
11
+ export function recentDayDirs(root, now) {
12
+ const out = [];
13
+ for (const offset of [0, 1]) {
14
+ const d = new Date(now - offset * 24 * 60 * 60 * 1000);
15
+ out.push(join(root, String(d.getFullYear()), pad(d.getMonth() + 1), pad(d.getDate())));
16
+ }
17
+ return out;
18
+ }
19
+ /** Read the first line of a file (session_meta can be tens of KB). */
20
+ function readFirstLine(file, maxBytes = 4 * 1024 * 1024) {
21
+ const fd = openSync(file, 'r');
22
+ try {
23
+ const chunk = Buffer.alloc(64 * 1024);
24
+ let acc = '';
25
+ let pos = 0;
26
+ while (pos < maxBytes) {
27
+ const n = readSync(fd, chunk, 0, chunk.length, pos);
28
+ if (n <= 0)
29
+ break;
30
+ const text = chunk.subarray(0, n).toString('utf-8');
31
+ const nl = text.indexOf('\n');
32
+ if (nl >= 0)
33
+ return acc + text.slice(0, nl);
34
+ acc += text;
35
+ pos += n;
36
+ }
37
+ return acc;
38
+ }
39
+ finally {
40
+ closeSync(fd);
41
+ }
42
+ }
43
+ function readRange(file, start, end) {
44
+ if (end <= start)
45
+ return '';
46
+ const fd = openSync(file, 'r');
47
+ try {
48
+ const buf = Buffer.alloc(end - start);
49
+ const n = readSync(fd, buf, 0, buf.length, start);
50
+ return buf.subarray(0, n).toString('utf-8');
51
+ }
52
+ finally {
53
+ closeSync(fd);
54
+ }
55
+ }
56
+ export class CodexWatcher {
57
+ config;
58
+ opts;
59
+ sessionsDir;
60
+ pollMs;
61
+ idleTimeoutMs;
62
+ includeExec;
63
+ minPushGapMs;
64
+ hookStateDir;
65
+ now;
66
+ logger;
67
+ sessions = new Map();
68
+ interval;
69
+ clients = new Map();
70
+ constructor(config, opts = {}) {
71
+ this.config = config;
72
+ this.opts = opts;
73
+ this.sessionsDir = opts.sessionsDir ?? CODEX_DEFAULTS.sessionsDir;
74
+ this.pollMs = opts.pollMs ?? CODEX_DEFAULTS.pollMs;
75
+ this.idleTimeoutMs = opts.idleTimeoutMs ?? CODEX_DEFAULTS.idleTimeoutMs;
76
+ this.includeExec = opts.includeExec ?? false;
77
+ this.minPushGapMs = opts.minPushGapMs ?? CODEX_DEFAULTS.minPushGapMs;
78
+ this.hookStateDir = opts.hookStateDir ?? CODEX_ADAPTER.stateDir;
79
+ this.now = opts.now ?? Date.now;
80
+ this.logger = new Logger('codex-watch', config.debug);
81
+ }
82
+ /** Active (non-ignored) sessions — exposed for tests/diagnostics. */
83
+ activeSessions() {
84
+ return [...this.sessions.values()].filter((s) => !s.ignored).map((s) => s.state);
85
+ }
86
+ async start() {
87
+ mkdirSync(this.hookStateDir, { recursive: true, mode: 0o700 });
88
+ writeFileSync(join(this.hookStateDir, `watcher-${process.pid}.pid`), String(process.pid), {
89
+ mode: 0o600,
90
+ });
91
+ if (!existsSync(this.sessionsDir)) {
92
+ this.logger.warn(`${this.sessionsDir} not found yet — waiting for Codex sessions`);
93
+ }
94
+ this.logger.info(`Watching Codex sessions in ${this.sessionsDir}`);
95
+ await this.tick();
96
+ this.interval = setInterval(() => {
97
+ void this.tick().catch((err) => this.logger.warn('Watch tick failed', { error: err?.message }));
98
+ }, this.pollMs);
99
+ const onSignal = () => {
100
+ void this.stop().then(() => process.exit(0));
101
+ };
102
+ process.once('SIGINT', onSignal);
103
+ process.once('SIGTERM', onSignal);
104
+ }
105
+ /** Stop polling and despawn every live session. */
106
+ async stop() {
107
+ rmSync(join(this.hookStateDir, `watcher-${process.pid}.pid`), { force: true });
108
+ if (this.interval)
109
+ clearInterval(this.interval);
110
+ this.interval = undefined;
111
+ const live = [...this.sessions.values()].filter((s) => !s.ignored && s.spawned);
112
+ await Promise.all(live.map((s) => this.despawn(s)));
113
+ this.sessions.clear();
114
+ }
115
+ /** One poll: discover files, read appended lines, push, expire quiet sessions. */
116
+ async tick() {
117
+ const now = this.now();
118
+ for (const dir of recentDayDirs(this.sessionsDir, now)) {
119
+ let files;
120
+ try {
121
+ files = readdirSync(dir);
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ for (const name of files) {
127
+ if (!name.startsWith('rollout-') || !name.endsWith('.jsonl'))
128
+ continue;
129
+ const file = join(dir, name);
130
+ let st;
131
+ try {
132
+ st = statSync(file);
133
+ }
134
+ catch {
135
+ continue;
136
+ }
137
+ const tracked = this.sessions.get(file);
138
+ if (!tracked) {
139
+ if (now - st.mtimeMs > this.idleTimeoutMs)
140
+ continue;
141
+ this.discover(file, st.size, st.mtimeMs);
142
+ }
143
+ else if (st.size > tracked.offset) {
144
+ if (!tracked.ignored) {
145
+ if (st.size - tracked.offset > CODEX_DEFAULTS.maxReadBytes) {
146
+ // Huge burst (rollouts can reach hundreds of MB): jump to the tail.
147
+ tracked.offset = st.size - CODEX_DEFAULTS.initialTailBytes;
148
+ tracked.partial = '';
149
+ this.readAppended(tracked, st.size, true);
150
+ }
151
+ else {
152
+ this.readAppended(tracked, st.size);
153
+ }
154
+ }
155
+ else {
156
+ tracked.offset = st.size;
157
+ }
158
+ tracked.lastGrowthAt = now;
159
+ }
160
+ else if (st.size < tracked.offset) {
161
+ // Truncated/rewritten: restart from the top.
162
+ tracked.offset = 0;
163
+ tracked.partial = '';
164
+ }
165
+ }
166
+ }
167
+ const pushes = [];
168
+ for (const [file, s] of this.sessions) {
169
+ if (s.ignored) {
170
+ if (now - s.lastGrowthAt > this.idleTimeoutMs * 2)
171
+ this.sessions.delete(file);
172
+ continue;
173
+ }
174
+ if (now - s.lastGrowthAt > this.idleTimeoutMs) {
175
+ if (s.spawned)
176
+ pushes.push(this.despawn(s));
177
+ this.sessions.delete(file);
178
+ continue;
179
+ }
180
+ pushes.push(this.maybePush(s));
181
+ }
182
+ await Promise.all(pushes);
183
+ }
184
+ discover(file, size, mtimeMs) {
185
+ let meta = null;
186
+ try {
187
+ const sig = parseRolloutLine(readFirstLine(file));
188
+ if (sig?.kind === 'meta')
189
+ meta = sig;
190
+ }
191
+ catch {
192
+ meta = null;
193
+ }
194
+ const sessionId = meta?.sessionId ?? file;
195
+ const cwd = meta?.cwd ?? '';
196
+ const ignored = !meta || meta.subagent || (meta.exec && !this.includeExec);
197
+ const tracked = {
198
+ file,
199
+ offset: 0,
200
+ partial: '',
201
+ lastGrowthAt: Math.min(this.now(), mtimeMs),
202
+ ignored,
203
+ spawned: false,
204
+ lastPushAt: 0,
205
+ pushedRev: 0,
206
+ state: {
207
+ sessionId,
208
+ serviceId: codexServiceId(sessionId, cwd || 'codex'),
209
+ state: 'IDLE',
210
+ projectDir: cwd,
211
+ lastEvent: 'discover',
212
+ lastUpdate: this.now(),
213
+ activity: ['Ready for a task'],
214
+ status: 'healthy',
215
+ mood: 'idle',
216
+ rev: 1,
217
+ },
218
+ };
219
+ this.sessions.set(file, tracked);
220
+ if (ignored) {
221
+ tracked.offset = size;
222
+ return;
223
+ }
224
+ // Replay only the tail of a long-running session to establish its state.
225
+ const start = Math.max(0, size - CODEX_DEFAULTS.initialTailBytes);
226
+ tracked.offset = start;
227
+ tracked.partial = '';
228
+ const skipFirst = start > 0;
229
+ this.readAppended(tracked, size, skipFirst);
230
+ this.logger.info(`Tracking Codex session ${sessionId} (${tracked.state.projectDir || '?'})`);
231
+ }
232
+ readAppended(s, size, skipFirstLine = false) {
233
+ let text;
234
+ try {
235
+ text = s.partial + readRange(s.file, s.offset, size);
236
+ }
237
+ catch {
238
+ return;
239
+ }
240
+ s.offset = size;
241
+ const lines = text.split('\n');
242
+ s.partial = lines.pop() ?? '';
243
+ if (skipFirstLine)
244
+ lines.shift();
245
+ for (const line of lines) {
246
+ const sig = parseRolloutLine(line, s.state.projectDir || undefined);
247
+ if (sig)
248
+ this.apply(s, sig);
249
+ }
250
+ }
251
+ /** Reduce one signal into the session state. Exposed via tick for tests. */
252
+ apply(s, sig) {
253
+ const st = s.state;
254
+ const set = (phase, activity, mood) => {
255
+ const history = pushHistory(st.activity, activity);
256
+ if (st.state === phase && history[0] === st.activity?.[0])
257
+ return;
258
+ st.state = phase;
259
+ st.activity = history;
260
+ st.status = phase === 'WAITING' ? 'warning' : 'healthy';
261
+ st.mood = mood;
262
+ st.lastUpdate = this.now();
263
+ st.rev = (st.rev ?? 0) + 1;
264
+ };
265
+ switch (sig.kind) {
266
+ case 'meta':
267
+ return;
268
+ case 'cwd':
269
+ if (sig.cwd && sig.cwd !== st.projectDir) {
270
+ st.projectDir = sig.cwd;
271
+ if (!s.spawned)
272
+ st.serviceId = codexServiceId(st.sessionId, sig.cwd);
273
+ st.rev = (st.rev ?? 0) + 1;
274
+ }
275
+ return;
276
+ case 'working':
277
+ return set('WORKING', sig.activity, 'happy');
278
+ case 'waiting':
279
+ return set('WAITING', sig.activity, 'concerned');
280
+ case 'idle':
281
+ return set('IDLE', sig.activity, 'idle');
282
+ }
283
+ }
284
+ hooksOwn(sessionId) {
285
+ return existsSync(stateFilePath(this.hookStateDir, sessionId));
286
+ }
287
+ async maybePush(s) {
288
+ const rev = s.state.rev ?? 0;
289
+ if (rev <= s.pushedRev)
290
+ return;
291
+ if (this.hooksOwn(s.state.sessionId)) {
292
+ s.pushedRev = rev;
293
+ return;
294
+ }
295
+ const now = this.now();
296
+ if (s.spawned && now - s.lastPushAt < this.minPushGapMs) {
297
+ // Coalesce; the next tick after the gap pushes the latest state.
298
+ return;
299
+ }
300
+ s.pushedRev = rev;
301
+ s.lastPushAt = now;
302
+ s.spawned = true;
303
+ await this.send(s.state.serviceId, buildAgentPayload(CODEX_ADAPTER, s.state, this.config));
304
+ }
305
+ async despawn(s) {
306
+ if (this.hooksOwn(s.state.sessionId))
307
+ return;
308
+ const offline = { ...s.state, offline: true };
309
+ await this.send(s.state.serviceId, buildAgentPayload(CODEX_ADAPTER, offline, this.config));
310
+ this.logger.info(`Codex session ${s.state.sessionId} ended`);
311
+ }
312
+ async send(serviceId, payload) {
313
+ try {
314
+ if (this.opts.push) {
315
+ await this.opts.push(serviceId, payload);
316
+ return;
317
+ }
318
+ if (!this.config.apiKey)
319
+ return;
320
+ let client = this.clients.get(serviceId);
321
+ if (!client) {
322
+ client = new NocvilleClient({
323
+ apiUrl: this.config.apiUrl,
324
+ apiKey: this.config.apiKey,
325
+ serviceId,
326
+ timeout: 5000,
327
+ retries: 1,
328
+ debug: this.config.debug,
329
+ });
330
+ this.clients.set(serviceId, client);
331
+ }
332
+ const result = await client.pushStatus(payload);
333
+ if (!result.success)
334
+ this.logger.warn('Push failed', { serviceId, error: result.error });
335
+ }
336
+ catch (err) {
337
+ this.logger.warn('Push error', { serviceId, error: err?.message });
338
+ }
339
+ }
340
+ }
341
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1,9 @@
1
+ export declare const CURSOR_DEFAULTS: {
2
+ stateDir: string;
3
+ stateFileMaxAge: number;
4
+ heartbeatSeconds: number;
5
+ minPushGapMs: number;
6
+ hookTimeoutSeconds: number;
7
+ };
8
+ export declare const cursorHome: () => string;
9
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { homedir, tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const CURSOR_DEFAULTS = {
4
+ stateDir: join(tmpdir(), 'nocville-cursor'),
5
+ stateFileMaxAge: 24 * 60 * 60 * 1000,
6
+ heartbeatSeconds: 90,
7
+ minPushGapMs: 1500,
8
+ hookTimeoutSeconds: 10,
9
+ };
10
+ export const cursorHome = () => join(homedir(), '.cursor');
11
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,8 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ import type { AgentHookEvent, AgentHookSpec, HookRunOptions } from '../agent-common/hook-runner.js';
3
+ import type { AgentAdapter } from '../agent-common/session.js';
4
+ export declare const CURSOR_ADAPTER: AgentAdapter;
5
+ export declare function cursorServiceId(sessionId: string, cwd: string): string;
6
+ export declare const CURSOR_HOOK_SPEC: AgentHookSpec;
7
+ export declare function handleCursorHookEvent(config: NpcConfig, rawInput?: string | AgentHookEvent, opts?: HookRunOptions, source?: 'native' | 'third-party'): Promise<void>;
8
+ //# sourceMappingURL=hook-handler.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { describeToolUse } from '../agent-common/activity.js';
3
+ import { readStdin, runAgentHook } from '../agent-common/hook-runner.js';
4
+ import { projectNameOf, sanitizeServiceId } from '../agent-common/session.js';
5
+ import { claudeTransition } from '../claude-code/state-machine.js';
6
+ import { CURSOR_DEFAULTS } from './defaults.js';
7
+ import { normalizeCursorEvent } from './normalize.js';
8
+ export const CURSOR_ADAPTER = {
9
+ command: 'cursor',
10
+ label: 'Cursor',
11
+ family: 'cursor',
12
+ stateDir: CURSOR_DEFAULTS.stateDir,
13
+ heartbeatSeconds: 90,
14
+ processNames: [],
15
+ };
16
+ export function cursorServiceId(sessionId, cwd) {
17
+ return sanitizeServiceId(`cursor-${projectNameOf(cwd)}-${sessionId.slice(0, 8)}`);
18
+ }
19
+ export const CURSOR_HOOK_SPEC = {
20
+ adapter: CURSOR_ADAPTER,
21
+ serviceIdFor: cursorServiceId,
22
+ transition: (event, previous) => event.hook_event_name === 'PostToolUse'
23
+ ? {
24
+ state: 'WORKING',
25
+ status: 'healthy',
26
+ mood: 'happy',
27
+ activity: describeToolUse(event.tool_name, event.tool_input, event.cwd),
28
+ }
29
+ : claudeTransition(event, previous),
30
+ deliveryIdentity: (event) => ({
31
+ source: event['nocvilleHookSource'] === 'third-party' ? 'third-party' : 'native',
32
+ key: createHash('sha256')
33
+ .update(JSON.stringify([
34
+ event.hook_event_name,
35
+ event['generation_id'],
36
+ describeToolUse(event.tool_name, event.tool_input, event.cwd),
37
+ ]))
38
+ .digest('hex'),
39
+ }),
40
+ };
41
+ export async function handleCursorHookEvent(config, rawInput, opts = {}, source = 'native') {
42
+ try {
43
+ const input = rawInput ?? readStdin();
44
+ const event = typeof input === 'string' ? JSON.parse(input) : input;
45
+ if (!event || typeof event !== 'object' || Array.isArray(event))
46
+ return;
47
+ await runAgentHook(CURSOR_HOOK_SPEC, config, {
48
+ ...normalizeCursorEvent(event),
49
+ nocvilleHookSource: source,
50
+ }, { minPushGapMs: CURSOR_DEFAULTS.minPushGapMs, ...opts });
51
+ }
52
+ catch {
53
+ /* Hook failures never affect Cursor. */
54
+ }
55
+ finally {
56
+ // A schema-neutral response, including malformed/failed direct invocations.
57
+ process.stdout.write('{}\n');
58
+ }
59
+ }
60
+ //# sourceMappingURL=hook-handler.js.map
@@ -0,0 +1,18 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ export { CURSOR_DEFAULTS } from './defaults.js';
3
+ export { CURSOR_ADAPTER, cursorServiceId, handleCursorHookEvent } from './hook-handler.js';
4
+ export { setupCursorHooks, uninstallCursorHooks, installCursorHooksInto, CURSOR_HOOK_EVENTS, } from './setup.js';
5
+ export type { CursorSetupOptions } from './setup.js';
6
+ export interface CursorSubcommandOptions {
7
+ subcommand: string;
8
+ config: NpcConfig;
9
+ flags: Record<string, string | boolean>;
10
+ args?: string[];
11
+ }
12
+ /**
13
+ * cursor subcommands: setup | uninstall | hook | flush | watchdog | cleanup.
14
+ * All hooks and detached helpers stay silent.
15
+ */
16
+ export declare function handleCursorCommand(opts: CursorSubcommandOptions): Promise<void>;
17
+ export { isCursorInvocation, normalizeCursorEvent, CURSOR_TO_CANON } from './normalize.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { numberFlag, stringFlag } from '../../core/config.js';
2
+ import { readStdin, runFlushCommand } from '../agent-common/hook-runner.js';
3
+ import { runWatchdog } from '../agent-common/watchdog.js';
4
+ import { cleanupSessionStates } from '../agent-common/session.js';
5
+ import { CURSOR_DEFAULTS } from './defaults.js';
6
+ import { CURSOR_ADAPTER, handleCursorHookEvent } from './hook-handler.js';
7
+ import { setupCursorHooks, uninstallCursorHooks } from './setup.js';
8
+ export { CURSOR_DEFAULTS } from './defaults.js';
9
+ export { CURSOR_ADAPTER, cursorServiceId, handleCursorHookEvent } from './hook-handler.js';
10
+ export { setupCursorHooks, uninstallCursorHooks, installCursorHooksInto, CURSOR_HOOK_EVENTS, } from './setup.js';
11
+ /**
12
+ * cursor subcommands: setup | uninstall | hook | flush | watchdog | cleanup.
13
+ * All hooks and detached helpers stay silent.
14
+ */
15
+ export async function handleCursorCommand(opts) {
16
+ const { subcommand, config, flags } = opts;
17
+ switch (subcommand) {
18
+ case 'setup': {
19
+ if (flags['uninstall']) {
20
+ uninstallCursorHooks();
21
+ break;
22
+ }
23
+ const ok = setupCursorHooks({
24
+ apiUrl: stringFlag(flags, 'api-url') ?? process.env['NOCVILLE_API_URL'],
25
+ apiKey: stringFlag(flags, 'api-key') ?? process.env['NOCVILLE_API_KEY'],
26
+ map: stringFlag(flags, 'map'),
27
+ cliPath: process.argv[1],
28
+ });
29
+ if (!ok)
30
+ process.exitCode = 1;
31
+ break;
32
+ }
33
+ case 'uninstall':
34
+ uninstallCursorHooks();
35
+ break;
36
+ case 'hook':
37
+ await handleCursorHookEvent(config, readStdin(), { mode: 'detach' });
38
+ break;
39
+ case 'watchdog':
40
+ await runWatchdog(CURSOR_ADAPTER, config, opts.args?.[0]);
41
+ break;
42
+ case 'flush':
43
+ await runFlushCommand(CURSOR_ADAPTER, config, opts.args?.[0], numberFlag(flags, 'delay'), flags['stats'] === true);
44
+ break;
45
+ case 'cleanup': {
46
+ const removed = await cleanupSessionStates(CURSOR_DEFAULTS.stateDir, CURSOR_DEFAULTS.stateFileMaxAge);
47
+ console.log(`Cleanup complete: ${removed} stale file(s) removed`);
48
+ break;
49
+ }
50
+ default:
51
+ console.error(`Unknown cursor subcommand: ${subcommand}`);
52
+ console.error('Available subcommands: setup, uninstall, hook, cleanup');
53
+ process.exitCode = 1;
54
+ }
55
+ }
56
+ export { isCursorInvocation, normalizeCursorEvent, CURSOR_TO_CANON } from './normalize.js';
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ import type { AgentHookEvent } from '../agent-common/hook-runner.js';
2
+ export declare const CURSOR_TO_CANON: Record<string, string>;
3
+ export declare function isCursorInvocation(event: AgentHookEvent): boolean;
4
+ /** Copy only telemetry fields; never retain prompts, model responses or tool output. */
5
+ export declare function normalizeCursorEvent(event: AgentHookEvent): AgentHookEvent;
6
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1,49 @@
1
+ export const CURSOR_TO_CANON = {
2
+ sessionStart: 'SessionStart',
3
+ sessionEnd: 'SessionEnd',
4
+ beforeSubmitPrompt: 'UserPromptSubmit',
5
+ preToolUse: 'PreToolUse',
6
+ postToolUse: 'PostToolUse',
7
+ postToolUseFailure: 'PostToolUseFailure',
8
+ beforeShellExecution: 'PreToolUse',
9
+ afterFileEdit: 'PostToolUse',
10
+ stop: 'Stop',
11
+ subagentStart: 'SubagentStart',
12
+ subagentStop: 'SubagentStop',
13
+ preCompact: 'PreCompact',
14
+ };
15
+ export function isCursorInvocation(event) {
16
+ const transcript = typeof event['transcript_path'] === 'string'
17
+ ? event['transcript_path'].replace(/\\/g, '/')
18
+ : '';
19
+ return (typeof event['cursor_version'] === 'string' ||
20
+ (typeof event['conversation_id'] === 'string' && !/(?:^|\/)\.claude(?:\/|$)/.test(transcript)));
21
+ }
22
+ /** Copy only telemetry fields; never retain prompts, model responses or tool output. */
23
+ export function normalizeCursorEvent(event) {
24
+ const name = event.hook_event_name ?? event.event ?? '';
25
+ const roots = event['workspace_roots'];
26
+ const shell = name === 'beforeShellExecution';
27
+ const tools = {
28
+ Shell: 'Bash',
29
+ ReadFile: 'Read',
30
+ Write: 'Write',
31
+ StrReplace: 'Edit',
32
+ Grep: 'Grep',
33
+ Delete: 'Delete',
34
+ Task: 'Task',
35
+ };
36
+ return {
37
+ session_id: typeof event['conversation_id'] === 'string' ? event['conversation_id'] : event.session_id,
38
+ cwd: Array.isArray(roots) && typeof roots[0] === 'string' ? roots[0] : event.cwd,
39
+ hook_event_name: CURSOR_TO_CANON[name] ?? name,
40
+ tool_name: shell
41
+ ? 'Bash'
42
+ : event.tool_name?.startsWith('MCP:')
43
+ ? `mcp__cursor__${event.tool_name.slice(4)}`
44
+ : (tools[event.tool_name ?? ''] ?? event.tool_name),
45
+ tool_input: shell ? { command: event['command'] } : event.tool_input,
46
+ generation_id: typeof event['generation_id'] === 'string' ? event['generation_id'] : undefined,
47
+ };
48
+ }
49
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1,29 @@
1
+ import type { HooksFile } from '../agent-common/hook-settings.js';
2
+ /** Telemetry hooks only; model output and prompt text never enter Nocville. */
3
+ export declare const CURSOR_HOOK_EVENTS: string[];
4
+ export declare function cursorSettingsPath(): string;
5
+ export interface CursorSetupOptions {
6
+ apiUrl?: string;
7
+ apiKey?: string;
8
+ map?: string;
9
+ cliPath?: string;
10
+ /** Override ~/.cursor/hooks.json (tests). */
11
+ settingsPath?: string;
12
+ quiet?: boolean;
13
+ }
14
+ /** Install the nocville group into every Cursor event. Mutates `file`. */
15
+ export declare function installCursorHooksInto(file: HooksFile, command: string, opts?: {
16
+ timeout?: number;
17
+ }): {
18
+ migrated: number;
19
+ installed: number;
20
+ };
21
+ export declare function buildCursorHookCommand(_cliPath: string): string;
22
+ /** Merge into Cursor settings, preserving user hooks and a one-time backup. */
23
+ export declare function setupCursorHooks(opts?: CursorSetupOptions): boolean;
24
+ /** Remove all Nocville hooks from `~/.cursor/hooks.json`. */
25
+ export declare function uninstallCursorHooks(opts?: {
26
+ settingsPath?: string;
27
+ quiet?: boolean;
28
+ }): number;
29
+ //# sourceMappingURL=setup.d.ts.map