@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.
Files changed (213) hide show
  1. package/README.md +268 -2
  2. package/dist/cjs/cli.js +584 -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/session-filter.js +150 -0
  11. package/dist/cjs/core/text.js +15 -0
  12. package/dist/cjs/core/thresholds.js +48 -0
  13. package/dist/cjs/core/types.js +10 -0
  14. package/dist/cjs/core/user-config.js +92 -0
  15. package/dist/cjs/doctor.js +130 -0
  16. package/dist/cjs/index.js +95 -0
  17. package/dist/cjs/npcs/agent-common/activity.js +291 -0
  18. package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
  19. package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
  20. package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
  21. package/dist/cjs/npcs/agent-common/liveness.js +112 -0
  22. package/dist/cjs/npcs/agent-common/lock.js +99 -0
  23. package/dist/cjs/npcs/agent-common/runtime.js +154 -0
  24. package/dist/cjs/npcs/agent-common/session.js +412 -0
  25. package/dist/cjs/npcs/agent-common/shim.js +96 -0
  26. package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
  27. package/dist/cjs/npcs/claude-code/defaults.js +20 -0
  28. package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
  29. package/dist/cjs/npcs/claude-code/index.js +104 -0
  30. package/dist/cjs/npcs/claude-code/setup.js +133 -0
  31. package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
  32. package/dist/cjs/npcs/codex/defaults.js +32 -0
  33. package/dist/cjs/npcs/codex/hook-handler.js +116 -0
  34. package/dist/cjs/npcs/codex/index.js +92 -0
  35. package/dist/cjs/npcs/codex/rollout.js +246 -0
  36. package/dist/cjs/npcs/codex/setup.js +104 -0
  37. package/dist/cjs/npcs/codex/watcher.js +361 -0
  38. package/dist/cjs/npcs/cursor/defaults.js +15 -0
  39. package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
  40. package/dist/cjs/npcs/cursor/index.js +72 -0
  41. package/dist/cjs/npcs/cursor/normalize.js +54 -0
  42. package/dist/cjs/npcs/cursor/setup.js +98 -0
  43. package/dist/cjs/npcs/gemini/defaults.js +15 -0
  44. package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
  45. package/dist/cjs/npcs/gemini/index.js +69 -0
  46. package/dist/cjs/npcs/gemini/setup.js +98 -0
  47. package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
  48. package/dist/cjs/npcs/github-watcher/format.js +21 -0
  49. package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
  50. package/dist/cjs/npcs/github-watcher/index.js +148 -0
  51. package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
  52. package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
  53. package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
  54. package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
  55. package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
  56. package/dist/cjs/npcs/log-watcher/index.js +161 -0
  57. package/dist/cjs/npcs/log-watcher/tail.js +81 -0
  58. package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
  59. package/dist/cjs/npcs/process-watcher/index.js +140 -0
  60. package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
  61. package/dist/cjs/npcs/receptionist/defaults.js +9 -0
  62. package/dist/cjs/npcs/receptionist/index.js +120 -0
  63. package/dist/cjs/npcs/receptionist/weather.js +129 -0
  64. package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
  65. package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
  66. package/dist/cjs/npcs/system-monitor/index.js +105 -0
  67. package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
  68. package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
  69. package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
  70. package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
  71. package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
  72. package/dist/cjs/package.json +1 -0
  73. package/dist/cli.d.ts +3 -0
  74. package/dist/cli.js +549 -0
  75. package/dist/core/api-key.d.ts +34 -0
  76. package/dist/core/api-key.js +64 -0
  77. package/dist/core/base-npc.d.ts +41 -0
  78. package/dist/core/base-npc.js +156 -0
  79. package/dist/core/config.d.ts +22 -0
  80. package/dist/core/config.js +101 -0
  81. package/dist/core/event-npc.d.ts +8 -0
  82. package/dist/core/event-npc.js +10 -0
  83. package/dist/core/logger.d.ts +11 -0
  84. package/dist/core/logger.js +35 -0
  85. package/dist/core/polling-npc.d.ts +18 -0
  86. package/dist/core/polling-npc.js +61 -0
  87. package/dist/core/screen.d.ts +11 -0
  88. package/dist/core/screen.js +24 -0
  89. package/dist/core/session-filter.d.ts +59 -0
  90. package/dist/core/session-filter.js +143 -0
  91. package/dist/core/text.d.ts +6 -0
  92. package/dist/core/text.js +12 -0
  93. package/dist/core/thresholds.d.ts +10 -0
  94. package/dist/core/thresholds.js +44 -0
  95. package/dist/core/types.d.ts +60 -0
  96. package/dist/core/types.js +7 -0
  97. package/dist/core/user-config.d.ts +43 -0
  98. package/dist/core/user-config.js +85 -0
  99. package/dist/doctor.d.ts +8 -0
  100. package/dist/doctor.js +126 -0
  101. package/dist/index.d.ts +41 -0
  102. package/dist/index.js +26 -0
  103. package/dist/npcs/agent-common/activity.d.ts +42 -0
  104. package/dist/npcs/agent-common/activity.js +280 -0
  105. package/dist/npcs/agent-common/git-stats.d.ts +11 -0
  106. package/dist/npcs/agent-common/git-stats.js +67 -0
  107. package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
  108. package/dist/npcs/agent-common/hook-runner.js +245 -0
  109. package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
  110. package/dist/npcs/agent-common/hook-settings.js +265 -0
  111. package/dist/npcs/agent-common/liveness.d.ts +12 -0
  112. package/dist/npcs/agent-common/liveness.js +105 -0
  113. package/dist/npcs/agent-common/lock.d.ts +6 -0
  114. package/dist/npcs/agent-common/lock.js +95 -0
  115. package/dist/npcs/agent-common/runtime.d.ts +16 -0
  116. package/dist/npcs/agent-common/runtime.js +149 -0
  117. package/dist/npcs/agent-common/session.d.ts +192 -0
  118. package/dist/npcs/agent-common/session.js +390 -0
  119. package/dist/npcs/agent-common/shim.d.ts +4 -0
  120. package/dist/npcs/agent-common/shim.js +90 -0
  121. package/dist/npcs/agent-common/watchdog.d.ts +16 -0
  122. package/dist/npcs/agent-common/watchdog.js +144 -0
  123. package/dist/npcs/claude-code/defaults.d.ts +15 -0
  124. package/dist/npcs/claude-code/defaults.js +17 -0
  125. package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
  126. package/dist/npcs/claude-code/hook-handler.js +131 -0
  127. package/dist/npcs/claude-code/index.d.ts +36 -0
  128. package/dist/npcs/claude-code/index.js +92 -0
  129. package/dist/npcs/claude-code/setup.d.ts +58 -0
  130. package/dist/npcs/claude-code/setup.js +121 -0
  131. package/dist/npcs/claude-code/state-machine.d.ts +28 -0
  132. package/dist/npcs/claude-code/state-machine.js +125 -0
  133. package/dist/npcs/codex/defaults.d.ts +24 -0
  134. package/dist/npcs/codex/defaults.js +28 -0
  135. package/dist/npcs/codex/hook-handler.d.ts +24 -0
  136. package/dist/npcs/codex/hook-handler.js +110 -0
  137. package/dist/npcs/codex/index.d.ts +21 -0
  138. package/dist/npcs/codex/index.js +76 -0
  139. package/dist/npcs/codex/rollout.d.ts +39 -0
  140. package/dist/npcs/codex/rollout.js +242 -0
  141. package/dist/npcs/codex/setup.d.ts +37 -0
  142. package/dist/npcs/codex/setup.js +96 -0
  143. package/dist/npcs/codex/watcher.d.ts +65 -0
  144. package/dist/npcs/codex/watcher.js +356 -0
  145. package/dist/npcs/cursor/defaults.d.ts +9 -0
  146. package/dist/npcs/cursor/defaults.js +11 -0
  147. package/dist/npcs/cursor/hook-handler.d.ts +8 -0
  148. package/dist/npcs/cursor/hook-handler.js +60 -0
  149. package/dist/npcs/cursor/index.d.ts +18 -0
  150. package/dist/npcs/cursor/index.js +57 -0
  151. package/dist/npcs/cursor/normalize.d.ts +6 -0
  152. package/dist/npcs/cursor/normalize.js +49 -0
  153. package/dist/npcs/cursor/setup.d.ts +29 -0
  154. package/dist/npcs/cursor/setup.js +90 -0
  155. package/dist/npcs/gemini/defaults.d.ts +9 -0
  156. package/dist/npcs/gemini/defaults.js +11 -0
  157. package/dist/npcs/gemini/hook-handler.d.ts +10 -0
  158. package/dist/npcs/gemini/hook-handler.js +87 -0
  159. package/dist/npcs/gemini/index.d.ts +17 -0
  160. package/dist/npcs/gemini/index.js +56 -0
  161. package/dist/npcs/gemini/setup.d.ts +29 -0
  162. package/dist/npcs/gemini/setup.js +90 -0
  163. package/dist/npcs/github-watcher/defaults.d.ts +8 -0
  164. package/dist/npcs/github-watcher/defaults.js +8 -0
  165. package/dist/npcs/github-watcher/format.d.ts +3 -0
  166. package/dist/npcs/github-watcher/format.js +18 -0
  167. package/dist/npcs/github-watcher/github-api.d.ts +55 -0
  168. package/dist/npcs/github-watcher/github-api.js +120 -0
  169. package/dist/npcs/github-watcher/index.d.ts +35 -0
  170. package/dist/npcs/github-watcher/index.js +144 -0
  171. package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
  172. package/dist/npcs/internet-connectivity/checks.js +84 -0
  173. package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
  174. package/dist/npcs/internet-connectivity/defaults.js +19 -0
  175. package/dist/npcs/internet-connectivity/index.d.ts +55 -0
  176. package/dist/npcs/internet-connectivity/index.js +246 -0
  177. package/dist/npcs/log-watcher/buffer.d.ts +18 -0
  178. package/dist/npcs/log-watcher/buffer.js +34 -0
  179. package/dist/npcs/log-watcher/defaults.d.ts +9 -0
  180. package/dist/npcs/log-watcher/defaults.js +9 -0
  181. package/dist/npcs/log-watcher/index.d.ts +45 -0
  182. package/dist/npcs/log-watcher/index.js +157 -0
  183. package/dist/npcs/log-watcher/tail.d.ts +9 -0
  184. package/dist/npcs/log-watcher/tail.js +78 -0
  185. package/dist/npcs/process-watcher/defaults.d.ts +6 -0
  186. package/dist/npcs/process-watcher/defaults.js +6 -0
  187. package/dist/npcs/process-watcher/index.d.ts +26 -0
  188. package/dist/npcs/process-watcher/index.js +136 -0
  189. package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
  190. package/dist/npcs/process-watcher/process-scanner.js +110 -0
  191. package/dist/npcs/receptionist/defaults.d.ts +6 -0
  192. package/dist/npcs/receptionist/defaults.js +6 -0
  193. package/dist/npcs/receptionist/index.d.ts +29 -0
  194. package/dist/npcs/receptionist/index.js +116 -0
  195. package/dist/npcs/receptionist/weather.d.ts +37 -0
  196. package/dist/npcs/receptionist/weather.js +121 -0
  197. package/dist/npcs/system-monitor/collectors.d.ts +45 -0
  198. package/dist/npcs/system-monitor/collectors.js +60 -0
  199. package/dist/npcs/system-monitor/defaults.d.ts +7 -0
  200. package/dist/npcs/system-monitor/defaults.js +10 -0
  201. package/dist/npcs/system-monitor/index.d.ts +27 -0
  202. package/dist/npcs/system-monitor/index.js +101 -0
  203. package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
  204. package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
  205. package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
  206. package/dist/npcs/web-page-watcher/capture.js +75 -0
  207. package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
  208. package/dist/npcs/web-page-watcher/defaults.js +27 -0
  209. package/dist/npcs/web-page-watcher/index.d.ts +42 -0
  210. package/dist/npcs/web-page-watcher/index.js +230 -0
  211. package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
  212. package/dist/npcs/web-page-watcher/pixelate.js +172 -0
  213. package/package.json +73 -3
@@ -0,0 +1,96 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { maskKey, persistCredentials } from '../../core/user-config.js';
4
+ import { buildShimHookCommand, stableNodePath, installHookGroups, readJsonFile, removeHookGroups, resolveCliPath, writeJsonFile, } from '../agent-common/hook-settings.js';
5
+ import { installHookShim } from '../agent-common/shim.js';
6
+ import { ensureStableCliPath } from '../agent-common/runtime.js';
7
+ import { CODEX_DEFAULTS, codexHome } from './defaults.js';
8
+ /**
9
+ * Codex hook events we install (developers.openai.com/codex/hooks). PostCompact
10
+ * is omitted — it carries nothing displayable.
11
+ */
12
+ export const CODEX_HOOK_EVENTS = [
13
+ 'SessionStart',
14
+ 'SessionEnd',
15
+ 'UserPromptSubmit',
16
+ 'PreToolUse',
17
+ 'PostToolUse',
18
+ 'PermissionRequest',
19
+ 'Stop',
20
+ 'Interrupt',
21
+ 'PreCompact',
22
+ 'SubagentStart',
23
+ 'SubagentStop',
24
+ ];
25
+ export function codexHooksPath() {
26
+ return join(codexHome(), 'hooks.json');
27
+ }
28
+ /** Install the nocville group into every Codex event. Mutates `file`. */
29
+ export function installCodexHooksInto(file, command, opts = {}) {
30
+ return installHookGroups(file, command, CODEX_HOOK_EVENTS, {
31
+ perEvent: Object.fromEntries(CODEX_HOOK_EVENTS.map((event) => [
32
+ event,
33
+ event === 'SessionEnd' || event === 'Interrupt'
34
+ ? { timeout: 3 }
35
+ : { async: true, timeout: opts.timeout ?? 10 },
36
+ ])),
37
+ });
38
+ }
39
+ export function buildCodexHookCommand(_cliPath) {
40
+ return buildShimHookCommand('codex');
41
+ }
42
+ function out(opts, msg) {
43
+ if (!opts.quiet)
44
+ console.log(msg);
45
+ }
46
+ /**
47
+ * Install Nocville hooks into `~/.codex/hooks.json` (same matcher-group JSON
48
+ * format as Claude Code). Merges with existing hooks, idempotent, keeps a
49
+ * one-time `.nocville-backup`. Codex requires new hooks to be reviewed and
50
+ * trusted once via `/hooks` before they run — we print that instruction.
51
+ */
52
+ export function setupCodexHooks(opts = {}) {
53
+ const hooksPath = opts.hooksPath ?? codexHooksPath();
54
+ const dir = join(hooksPath, '..');
55
+ if (!existsSync(dir)) {
56
+ console.error(`${dir} does not exist — is Codex installed? Skipping Codex setup.`);
57
+ return false;
58
+ }
59
+ const file = readJsonFile(hooksPath);
60
+ if (file === null) {
61
+ console.error(`Could not parse ${hooksPath}; fix it (or move it aside) and re-run setup.`);
62
+ return false;
63
+ }
64
+ const creds = persistCredentials(opts);
65
+ const resolved = resolveCliPath(opts.cliPath);
66
+ const cliPath = ensureStableCliPath(resolved);
67
+ installHookShim(stableNodePath(), cliPath);
68
+ const { installed } = installCodexHooksInto(file, buildCodexHookCommand(cliPath), {
69
+ timeout: CODEX_DEFAULTS.hookTimeoutSeconds,
70
+ });
71
+ writeJsonFile(hooksPath, file);
72
+ out(opts, `✓ Codex: installed Nocville hooks for ${installed} events in ${hooksPath}`);
73
+ out(opts, ` Hook runs: ${process.execPath} ${cliPath}`);
74
+ out(opts, ` API: ${creds.apiUrl ?? 'https://nocville.com'} key: ${maskKey(creds.apiKey)}`);
75
+ if (!creds.apiKey) {
76
+ out(opts, ' ⚠ No API key saved yet — run: nocville-npc init --api-key=<your team key>');
77
+ }
78
+ out(opts, ' ➜ One-time step: start Codex and run /hooks to review and trust the Nocville');
79
+ out(opts, ' hooks (Codex skips new hooks until trusted).');
80
+ return true;
81
+ }
82
+ /** Remove all Nocville hooks from `~/.codex/hooks.json`. */
83
+ export function uninstallCodexHooks(opts = {}) {
84
+ const hooksPath = opts.hooksPath ?? codexHooksPath();
85
+ const file = readJsonFile(hooksPath);
86
+ if (!file || !existsSync(hooksPath)) {
87
+ out(opts, 'Codex: no hooks.json — nothing to remove.');
88
+ return 0;
89
+ }
90
+ const removed = removeHookGroups(file);
91
+ if (removed > 0)
92
+ writeJsonFile(hooksPath, file);
93
+ out(opts, `✓ Codex: removed ${removed} Nocville hook(s) from ${hooksPath}`);
94
+ return removed;
95
+ }
96
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,65 @@
1
+ import type { StatusUpdate } from '@nocville/node';
2
+ import { type SessionFilter } from '../../core/session-filter.js';
3
+ import type { NpcConfig } from '../../core/types.js';
4
+ import type { AgentSessionState } from '../agent-common/session.js';
5
+ /**
6
+ * `nocville-npc codex watch` — zero-config Codex integration that tails the
7
+ * session rollout logs Codex writes under ~/.codex/sessions/YYYY/MM/DD/.
8
+ *
9
+ * One NPC per live interactive session (subagent and guardian threads are
10
+ * skipped; `codex exec` runs only with --include-exec). Phase + activity are
11
+ * derived from the log; a session whose log stops growing for `idleTimeoutMs`
12
+ * despawns. Sessions whose hooks are active (any hook state file exists)
13
+ * are left to the hooks so nothing is pushed twice. Ctrl-C despawns all.
14
+ */
15
+ export interface CodexWatcherOptions {
16
+ sessionsDir?: string;
17
+ pollMs?: number;
18
+ idleTimeoutMs?: number;
19
+ includeExec?: boolean;
20
+ minPushGapMs?: number;
21
+ /** Push override (tests). */
22
+ push?: (serviceId: string, payload: StatusUpdate) => Promise<{
23
+ success: boolean;
24
+ error?: string;
25
+ }>;
26
+ now?: () => number;
27
+ /** Hook state dir consulted for hook ownership (tests). */
28
+ hookStateDir?: string;
29
+ /** Override sessions.json (tests). */
30
+ sessionFilter?: SessionFilter;
31
+ }
32
+ /** Day directories to scan: today and yesterday (local time, like Codex). */
33
+ export declare function recentDayDirs(root: string, now: number): string[];
34
+ export declare class CodexWatcher {
35
+ private readonly config;
36
+ private readonly opts;
37
+ private readonly sessionsDir;
38
+ private readonly pollMs;
39
+ private readonly idleTimeoutMs;
40
+ private readonly includeExec;
41
+ private readonly minPushGapMs;
42
+ private readonly hookStateDir;
43
+ private readonly now;
44
+ private readonly logger;
45
+ private readonly sessions;
46
+ private interval?;
47
+ private clients;
48
+ constructor(config: NpcConfig, opts?: CodexWatcherOptions);
49
+ /** Active (non-ignored) sessions — exposed for tests/diagnostics. */
50
+ activeSessions(): AgentSessionState[];
51
+ start(): Promise<void>;
52
+ /** Stop polling and despawn every live session. */
53
+ stop(): Promise<void>;
54
+ /** One poll: discover files, read appended lines, push, expire quiet sessions. */
55
+ tick(): Promise<void>;
56
+ private discover;
57
+ private readAppended;
58
+ /** Reduce one signal into the session state. Exposed via tick for tests. */
59
+ private apply;
60
+ private hooksOwn;
61
+ private maybePush;
62
+ private despawn;
63
+ private send;
64
+ }
65
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1,356 @@
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 { loadSessionFilter, sessionPublishDecision, } from '../../core/session-filter.js';
6
+ import { buildAgentPayload, pushHistory, stateFilePath } from '../agent-common/session.js';
7
+ import { CODEX_DEFAULTS } from './defaults.js';
8
+ import { CODEX_ADAPTER, codexServiceId } from './hook-handler.js';
9
+ import { parseRolloutLine } from './rollout.js';
10
+ const pad = (n) => String(n).padStart(2, '0');
11
+ /** Day directories to scan: today and yesterday (local time, like Codex). */
12
+ export function recentDayDirs(root, now) {
13
+ const out = [];
14
+ for (const offset of [0, 1]) {
15
+ const d = new Date(now - offset * 24 * 60 * 60 * 1000);
16
+ out.push(join(root, String(d.getFullYear()), pad(d.getMonth() + 1), pad(d.getDate())));
17
+ }
18
+ return out;
19
+ }
20
+ /** Read the first line of a file (session_meta can be tens of KB). */
21
+ function readFirstLine(file, maxBytes = 4 * 1024 * 1024) {
22
+ const fd = openSync(file, 'r');
23
+ try {
24
+ const chunk = Buffer.alloc(64 * 1024);
25
+ let acc = '';
26
+ let pos = 0;
27
+ while (pos < maxBytes) {
28
+ const n = readSync(fd, chunk, 0, chunk.length, pos);
29
+ if (n <= 0)
30
+ break;
31
+ const text = chunk.subarray(0, n).toString('utf-8');
32
+ const nl = text.indexOf('\n');
33
+ if (nl >= 0)
34
+ return acc + text.slice(0, nl);
35
+ acc += text;
36
+ pos += n;
37
+ }
38
+ return acc;
39
+ }
40
+ finally {
41
+ closeSync(fd);
42
+ }
43
+ }
44
+ function readRange(file, start, end) {
45
+ if (end <= start)
46
+ return '';
47
+ const fd = openSync(file, 'r');
48
+ try {
49
+ const buf = Buffer.alloc(end - start);
50
+ const n = readSync(fd, buf, 0, buf.length, start);
51
+ return buf.subarray(0, n).toString('utf-8');
52
+ }
53
+ finally {
54
+ closeSync(fd);
55
+ }
56
+ }
57
+ export class CodexWatcher {
58
+ config;
59
+ opts;
60
+ sessionsDir;
61
+ pollMs;
62
+ idleTimeoutMs;
63
+ includeExec;
64
+ minPushGapMs;
65
+ hookStateDir;
66
+ now;
67
+ logger;
68
+ sessions = new Map();
69
+ interval;
70
+ clients = new Map();
71
+ constructor(config, opts = {}) {
72
+ this.config = config;
73
+ this.opts = opts;
74
+ this.sessionsDir = opts.sessionsDir ?? CODEX_DEFAULTS.sessionsDir;
75
+ this.pollMs = opts.pollMs ?? CODEX_DEFAULTS.pollMs;
76
+ this.idleTimeoutMs = opts.idleTimeoutMs ?? CODEX_DEFAULTS.idleTimeoutMs;
77
+ this.includeExec = opts.includeExec ?? false;
78
+ this.minPushGapMs = opts.minPushGapMs ?? CODEX_DEFAULTS.minPushGapMs;
79
+ this.hookStateDir = opts.hookStateDir ?? CODEX_ADAPTER.stateDir;
80
+ this.now = opts.now ?? Date.now;
81
+ this.logger = new Logger('codex-watch', config.debug);
82
+ }
83
+ /** Active (non-ignored) sessions — exposed for tests/diagnostics. */
84
+ activeSessions() {
85
+ return [...this.sessions.values()].filter((s) => !s.ignored).map((s) => s.state);
86
+ }
87
+ async start() {
88
+ mkdirSync(this.hookStateDir, { recursive: true, mode: 0o700 });
89
+ writeFileSync(join(this.hookStateDir, `watcher-${process.pid}.pid`), String(process.pid), {
90
+ mode: 0o600,
91
+ });
92
+ if (!existsSync(this.sessionsDir)) {
93
+ this.logger.warn(`${this.sessionsDir} not found yet — waiting for Codex sessions`);
94
+ }
95
+ this.logger.info(`Watching Codex sessions in ${this.sessionsDir}`);
96
+ await this.tick();
97
+ this.interval = setInterval(() => {
98
+ void this.tick().catch((err) => this.logger.warn('Watch tick failed', { error: err?.message }));
99
+ }, this.pollMs);
100
+ const onSignal = () => {
101
+ void this.stop().then(() => process.exit(0));
102
+ };
103
+ process.once('SIGINT', onSignal);
104
+ process.once('SIGTERM', onSignal);
105
+ }
106
+ /** Stop polling and despawn every live session. */
107
+ async stop() {
108
+ rmSync(join(this.hookStateDir, `watcher-${process.pid}.pid`), { force: true });
109
+ if (this.interval)
110
+ clearInterval(this.interval);
111
+ this.interval = undefined;
112
+ const live = [...this.sessions.values()].filter((s) => !s.ignored && s.spawned);
113
+ await Promise.all(live.map((s) => this.despawn(s)));
114
+ this.sessions.clear();
115
+ }
116
+ /** One poll: discover files, read appended lines, push, expire quiet sessions. */
117
+ async tick() {
118
+ const now = this.now();
119
+ for (const dir of recentDayDirs(this.sessionsDir, now)) {
120
+ let files;
121
+ try {
122
+ files = readdirSync(dir);
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ for (const name of files) {
128
+ if (!name.startsWith('rollout-') || !name.endsWith('.jsonl'))
129
+ continue;
130
+ const file = join(dir, name);
131
+ let st;
132
+ try {
133
+ st = statSync(file);
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ const tracked = this.sessions.get(file);
139
+ if (!tracked) {
140
+ if (now - st.mtimeMs > this.idleTimeoutMs)
141
+ continue;
142
+ this.discover(file, st.size, st.mtimeMs);
143
+ }
144
+ else if (st.size > tracked.offset) {
145
+ if (!tracked.ignored) {
146
+ if (st.size - tracked.offset > CODEX_DEFAULTS.maxReadBytes) {
147
+ // Huge burst (rollouts can reach hundreds of MB): jump to the tail.
148
+ tracked.offset = st.size - CODEX_DEFAULTS.initialTailBytes;
149
+ tracked.partial = '';
150
+ this.readAppended(tracked, st.size, true);
151
+ }
152
+ else {
153
+ this.readAppended(tracked, st.size);
154
+ }
155
+ }
156
+ else {
157
+ tracked.offset = st.size;
158
+ }
159
+ tracked.lastGrowthAt = now;
160
+ }
161
+ else if (st.size < tracked.offset) {
162
+ // Truncated/rewritten: restart from the top.
163
+ tracked.offset = 0;
164
+ tracked.partial = '';
165
+ }
166
+ }
167
+ }
168
+ const pushes = [];
169
+ for (const [file, s] of this.sessions) {
170
+ if (s.ignored) {
171
+ if (now - s.lastGrowthAt > this.idleTimeoutMs * 2)
172
+ this.sessions.delete(file);
173
+ continue;
174
+ }
175
+ if (now - s.lastGrowthAt > this.idleTimeoutMs) {
176
+ if (s.spawned)
177
+ pushes.push(this.despawn(s));
178
+ this.sessions.delete(file);
179
+ continue;
180
+ }
181
+ pushes.push(this.maybePush(s));
182
+ }
183
+ await Promise.all(pushes);
184
+ }
185
+ discover(file, size, mtimeMs) {
186
+ let meta = null;
187
+ try {
188
+ const sig = parseRolloutLine(readFirstLine(file));
189
+ if (sig?.kind === 'meta')
190
+ meta = sig;
191
+ }
192
+ catch {
193
+ meta = null;
194
+ }
195
+ const sessionId = meta?.sessionId ?? file;
196
+ const cwd = meta?.cwd ?? '';
197
+ const ignored = !meta || meta.subagent || (meta.exec && !this.includeExec);
198
+ const tracked = {
199
+ file,
200
+ offset: 0,
201
+ partial: '',
202
+ lastGrowthAt: Math.min(this.now(), mtimeMs),
203
+ ignored,
204
+ spawned: false,
205
+ lastPushAt: 0,
206
+ pushedRev: 0,
207
+ state: {
208
+ sessionId,
209
+ serviceId: codexServiceId(sessionId, cwd || 'codex'),
210
+ state: 'IDLE',
211
+ projectDir: cwd,
212
+ lastEvent: 'discover',
213
+ lastUpdate: this.now(),
214
+ activity: ['Ready for a task'],
215
+ status: 'healthy',
216
+ mood: 'idle',
217
+ rev: 1,
218
+ },
219
+ };
220
+ this.sessions.set(file, tracked);
221
+ if (ignored) {
222
+ tracked.offset = size;
223
+ return;
224
+ }
225
+ // Replay only the tail of a long-running session to establish its state.
226
+ const start = Math.max(0, size - CODEX_DEFAULTS.initialTailBytes);
227
+ tracked.offset = start;
228
+ tracked.partial = '';
229
+ const skipFirst = start > 0;
230
+ this.readAppended(tracked, size, skipFirst);
231
+ this.logger.info(`Tracking Codex session ${sessionId} (${tracked.state.projectDir || '?'})`);
232
+ }
233
+ readAppended(s, size, skipFirstLine = false) {
234
+ let text;
235
+ try {
236
+ text = s.partial + readRange(s.file, s.offset, size);
237
+ }
238
+ catch {
239
+ return;
240
+ }
241
+ s.offset = size;
242
+ const lines = text.split('\n');
243
+ s.partial = lines.pop() ?? '';
244
+ if (skipFirstLine)
245
+ lines.shift();
246
+ for (const line of lines) {
247
+ const sig = parseRolloutLine(line, s.state.projectDir || undefined);
248
+ if (sig)
249
+ this.apply(s, sig);
250
+ }
251
+ }
252
+ /** Reduce one signal into the session state. Exposed via tick for tests. */
253
+ apply(s, sig) {
254
+ const st = s.state;
255
+ const set = (phase, activity, mood) => {
256
+ const history = pushHistory(st.activity, activity);
257
+ if (st.state === phase && history[0] === st.activity?.[0])
258
+ return;
259
+ st.state = phase;
260
+ st.activity = history;
261
+ st.status = phase === 'WAITING' ? 'warning' : 'healthy';
262
+ st.mood = mood;
263
+ st.lastUpdate = this.now();
264
+ st.rev = (st.rev ?? 0) + 1;
265
+ };
266
+ switch (sig.kind) {
267
+ case 'meta':
268
+ return;
269
+ case 'cwd':
270
+ if (sig.cwd && sig.cwd !== st.projectDir) {
271
+ st.projectDir = sig.cwd;
272
+ if (!s.spawned)
273
+ st.serviceId = codexServiceId(st.sessionId, sig.cwd);
274
+ st.rev = (st.rev ?? 0) + 1;
275
+ }
276
+ return;
277
+ case 'working':
278
+ return set('WORKING', sig.activity, 'happy');
279
+ case 'waiting':
280
+ return set('WAITING', sig.activity, 'concerned');
281
+ case 'idle':
282
+ return set('IDLE', sig.activity, 'idle');
283
+ }
284
+ }
285
+ hooksOwn(sessionId) {
286
+ return existsSync(stateFilePath(this.hookStateDir, sessionId));
287
+ }
288
+ async maybePush(s) {
289
+ const rev = s.state.rev ?? 0;
290
+ if (rev <= s.pushedRev)
291
+ return;
292
+ if (this.hooksOwn(s.state.sessionId)) {
293
+ s.pushedRev = rev;
294
+ return;
295
+ }
296
+ const verdict = sessionPublishDecision(this.opts.sessionFilter ?? loadSessionFilter(), {
297
+ projectDir: s.state.projectDir,
298
+ name: s.state.name,
299
+ });
300
+ if (!verdict.publish) {
301
+ // Filtered out by sessions.json: never show it, and remove it if it was showing.
302
+ s.pushedRev = rev;
303
+ if (s.spawned) {
304
+ s.spawned = false;
305
+ const offline = { ...s.state, offline: true };
306
+ await this.send(s.state.serviceId, buildAgentPayload(CODEX_ADAPTER, offline, this.config));
307
+ }
308
+ return;
309
+ }
310
+ const now = this.now();
311
+ if (s.spawned && now - s.lastPushAt < this.minPushGapMs) {
312
+ // Coalesce; the next tick after the gap pushes the latest state.
313
+ return;
314
+ }
315
+ s.pushedRev = rev;
316
+ s.lastPushAt = now;
317
+ s.spawned = true;
318
+ await this.send(s.state.serviceId, buildAgentPayload(CODEX_ADAPTER, s.state, this.config));
319
+ }
320
+ async despawn(s) {
321
+ if (this.hooksOwn(s.state.sessionId) || !s.spawned)
322
+ return;
323
+ const offline = { ...s.state, offline: true };
324
+ await this.send(s.state.serviceId, buildAgentPayload(CODEX_ADAPTER, offline, this.config));
325
+ this.logger.info(`Codex session ${s.state.sessionId} ended`);
326
+ }
327
+ async send(serviceId, payload) {
328
+ try {
329
+ if (this.opts.push) {
330
+ await this.opts.push(serviceId, payload);
331
+ return;
332
+ }
333
+ if (!this.config.apiKey)
334
+ return;
335
+ let client = this.clients.get(serviceId);
336
+ if (!client) {
337
+ client = new NocvilleClient({
338
+ apiUrl: this.config.apiUrl,
339
+ apiKey: this.config.apiKey,
340
+ serviceId,
341
+ timeout: 5000,
342
+ retries: 1,
343
+ debug: this.config.debug,
344
+ });
345
+ this.clients.set(serviceId, client);
346
+ }
347
+ const result = await client.pushStatus(payload);
348
+ if (!result.success)
349
+ this.logger.warn('Push failed', { serviceId, error: result.error });
350
+ }
351
+ catch (err) {
352
+ this.logger.warn('Push error', { serviceId, error: err?.message });
353
+ }
354
+ }
355
+ }
356
+ //# 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