@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,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hookEventName = hookEventName;
4
+ exports.readStdin = readStdin;
5
+ exports.runAgentHook = runAgentHook;
6
+ exports.runFlushCommand = runFlushCommand;
7
+ exports.collectSessionGitStats = collectSessionGitStats;
8
+ const node_fs_1 = require("node:fs");
9
+ const activity_js_1 = require("./activity.js");
10
+ const git_stats_js_1 = require("./git-stats.js");
11
+ const liveness_js_1 = require("./liveness.js");
12
+ const lock_js_1 = require("./lock.js");
13
+ const session_js_1 = require("./session.js");
14
+ const watchdog_js_1 = require("./watchdog.js");
15
+ function hookEventName(event) {
16
+ return event.hook_event_name ?? event.event ?? '';
17
+ }
18
+ /** Read all of stdin synchronously; '' on error. */
19
+ function readStdin() {
20
+ try {
21
+ return (0, node_fs_1.readFileSync)(0, 'utf-8');
22
+ }
23
+ catch {
24
+ return '';
25
+ }
26
+ }
27
+ /**
28
+ * Process one hook invocation. Never throws. Only a configured neutral JSON response
29
+ * reaches stdout; other hosts receive no output.
30
+ */
31
+ async function runAgentHook(spec, config, rawInput, opts = {}) {
32
+ try {
33
+ await runAgentHookUnsafe(spec, config, rawInput, opts);
34
+ }
35
+ catch (err) {
36
+ if (config.debug) {
37
+ console.error(`[nocville ${spec.adapter.command} hook] ${err?.message ?? err}`);
38
+ }
39
+ }
40
+ finally {
41
+ if (spec.stdoutOnSuccess !== undefined)
42
+ process.stdout.write(spec.stdoutOnSuccess + '\n');
43
+ }
44
+ }
45
+ async function runAgentHookUnsafe(spec, config, rawInput, opts) {
46
+ if (typeof rawInput === 'string' && !rawInput.trim())
47
+ return;
48
+ let event;
49
+ try {
50
+ event = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ if (!event || typeof event !== 'object')
56
+ return;
57
+ const sessionId = typeof event.session_id === 'string' ? event.session_id : '';
58
+ if (!sessionId)
59
+ return;
60
+ const { adapter } = spec;
61
+ const eventName = hookEventName(event);
62
+ // Leave headroom for launcher startup and the 1.5s HTTP attempt within Codex's 3s cap.
63
+ const shutdown = eventName === 'SessionEnd' || eventName === 'Interrupt';
64
+ const lockWaitMs = shutdown ? 150 : adapter.family === 'gemini' ? 500 : undefined;
65
+ const canResolvePid = adapter.processNames?.length !== 0 &&
66
+ !shutdown &&
67
+ opts.mode === 'detach' &&
68
+ process.env['NOCVILLE_NO_WATCHDOG'] !== '1';
69
+ const now = (opts.now ?? Date.now)();
70
+ if (eventName === 'SessionStart')
71
+ await (0, session_js_1.maybeCleanupSessionStates)(adapter.stateDir, now);
72
+ const { decision, offline, resolvePid, collectStats } = await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
73
+ const prev = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
74
+ if (prev && adapter.processNames?.length === 0 && (prev.agentPid || prev.watchdogPid)) {
75
+ // Retire observers installed before an adapter opted out of process liveness.
76
+ prev.agentPid = undefined;
77
+ prev.agentStartedAt = undefined;
78
+ prev.watchdogPid = undefined;
79
+ (0, session_js_1.writeSessionState)(adapter.stateDir, prev);
80
+ }
81
+ const identity = spec.deliveryIdentity?.(event);
82
+ const deliveries = (prev?.hookDeliveries ?? []).filter((d) => now - d.at < 5000);
83
+ if (identity) {
84
+ const duplicate = deliveries.findIndex((d) => d.key === identity.key && d.source !== identity.source);
85
+ if (duplicate >= 0 && prev) {
86
+ deliveries.splice(duplicate, 1);
87
+ prev.hookDeliveries = deliveries;
88
+ (0, session_js_1.writeSessionState)(adapter.stateDir, prev);
89
+ return {
90
+ decision: { kind: 'none' },
91
+ offline: !!prev.offline,
92
+ resolvePid: false,
93
+ collectStats: false,
94
+ };
95
+ }
96
+ deliveries.push({ ...identity, at: now });
97
+ }
98
+ const cwd = (typeof event.cwd === 'string' && event.cwd) || prev?.projectDir || process.cwd();
99
+ // An offline sentinel awaiting retry cannot be revived by late async tool hooks.
100
+ if (prev?.offline && eventName !== 'SessionStart')
101
+ return {
102
+ decision: { kind: 'now' },
103
+ offline: true,
104
+ resolvePid: false,
105
+ collectStats: false,
106
+ };
107
+ const agentId = typeof event['agent_id'] === 'string' ? event['agent_id'] : '';
108
+ if (adapter.family === 'claude' &&
109
+ agentId &&
110
+ prev &&
111
+ eventName !== 'PermissionRequest' &&
112
+ eventName !== 'SubagentStart') {
113
+ const next = (0, session_js_1.applySubagentEvent)(prev, agentId, typeof event['agent_type'] === 'string' ? event['agent_type'] : 'subagent', eventName, (0, activity_js_1.describeToolUse)(event.tool_name, event.tool_input, cwd), now);
114
+ let decision = { kind: 'none' };
115
+ if (next.rev !== prev.rev) {
116
+ const gap = opts.minPushGapMs ?? session_js_1.DEFAULT_MIN_PUSH_GAP_MS;
117
+ const elapsed = now - (prev.lastPushAt ?? 0);
118
+ if (!prev.lastPushAt || elapsed >= gap) {
119
+ next.lastPushAt = now;
120
+ decision = { kind: 'now' };
121
+ }
122
+ else if (!next.flushAt || next.flushAt <= now - 2000) {
123
+ const delayMs = Math.max(50, gap - elapsed);
124
+ next.flushAt = now + delayMs;
125
+ decision = { kind: 'defer', delayMs };
126
+ }
127
+ }
128
+ const resolvePid = canResolvePid && (0, liveness_js_1.reserveAgentPidAttempt)(next, now);
129
+ (0, session_js_1.writeSessionState)(adapter.stateDir, next);
130
+ return { decision, offline: false, resolvePid, collectStats: false };
131
+ }
132
+ // Codex has no documented agent_id in hook payloads: keep its parent transitions.
133
+ // https://developers.openai.com/codex/hooks
134
+ const { next, decision } = (0, session_js_1.applyTransition)(prev, {
135
+ sessionId,
136
+ serviceId: spec.serviceIdFor(sessionId, cwd),
137
+ projectDir: cwd,
138
+ eventName,
139
+ name: spec.resolveName?.(event, prev),
140
+ transition: spec.transition(event, prev),
141
+ now,
142
+ }, opts.minPushGapMs ?? session_js_1.DEFAULT_MIN_PUSH_GAP_MS);
143
+ if (!prev || prev.offline) {
144
+ next.agentPid = undefined;
145
+ next.agentStartedAt = undefined;
146
+ next.agentPidResolveAttempts = 0;
147
+ next.agentPidLastResolveAt = undefined;
148
+ next.watchdogPid = undefined;
149
+ next.offlineRetryStartedAt = undefined;
150
+ next.offlineDeliveryStopped = undefined;
151
+ next.subagents = {};
152
+ next.agentStats = undefined;
153
+ next.statsPendingRev = undefined;
154
+ }
155
+ if (identity)
156
+ next.hookDeliveries = deliveries.slice(-64);
157
+ const resolvePid = canResolvePid && (0, liveness_js_1.reserveAgentPidAttempt)(next, now);
158
+ const collectStats = (spec.statsEvents ?? ['Stop']).includes(eventName) && !next.offline;
159
+ if (collectStats)
160
+ next.statsPendingRev = next.rev ?? 0;
161
+ else if (next.state === 'WORKING' || next.offline)
162
+ next.statsPendingRev = undefined;
163
+ (0, session_js_1.writeSessionState)(adapter.stateDir, next);
164
+ return { decision, offline: !!next.offline, resolvePid, collectStats };
165
+ }, lockWaitMs);
166
+ if (resolvePid && !offline) {
167
+ const proc = (0, liveness_js_1.resolveAgentPid)(adapter, sessionId);
168
+ await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
169
+ const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
170
+ if (!fresh || fresh.offline)
171
+ return;
172
+ fresh.agentPid = proc?.pid;
173
+ fresh.agentStartedAt = proc?.startedAt;
174
+ (0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
175
+ }, lockWaitMs);
176
+ }
177
+ if (opts.mode === 'detach' && !offline)
178
+ await (0, watchdog_js_1.ensureWatchdog)(adapter, config, sessionId, opts.cliPath, lockWaitMs);
179
+ // SessionEnd is synchronous and bounded by the agent's short shutdown timeout.
180
+ if (offline) {
181
+ await (0, session_js_1.flushSession)(adapter, config, sessionId, {
182
+ push: opts.push,
183
+ timeoutMs: 1500,
184
+ lockWaitMs: 150,
185
+ });
186
+ // If delivery failed, the watchdog retries; unsupported hosts get one detached retry.
187
+ const pending = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
188
+ if (opts.mode === 'detach' && pending && (pending.rev ?? 0) > (pending.pushedRev ?? 0))
189
+ (0, session_js_1.spawnDetachedFlush)(adapter, config, sessionId, 0, opts.cliPath);
190
+ return;
191
+ }
192
+ if (decision.kind === 'none' && !collectStats)
193
+ return;
194
+ const mode = opts.mode ?? 'inline';
195
+ const delayMs = decision.kind === 'defer' ? decision.delayMs : 0;
196
+ if (opts.scheduleFlush && decision.kind === 'defer') {
197
+ opts.scheduleFlush(sessionId, delayMs);
198
+ return;
199
+ }
200
+ if (mode === 'detach') {
201
+ (0, session_js_1.spawnDetachedFlush)(adapter, config, sessionId, delayMs, opts.cliPath, collectStats);
202
+ return;
203
+ }
204
+ if (delayMs > 0) {
205
+ const timer = setTimeout(() => {
206
+ void (0, session_js_1.flushSession)(adapter, config, sessionId, { push: opts.push });
207
+ }, delayMs);
208
+ timer.unref?.();
209
+ return;
210
+ }
211
+ await (0, session_js_1.flushSession)(adapter, config, sessionId, { push: opts.push });
212
+ }
213
+ /** `<agent> flush <session> --delay=ms`: the detached half of a hook push. */
214
+ async function runFlushCommand(adapter, config, sessionId, delayMs, stats = false) {
215
+ if (!sessionId)
216
+ return;
217
+ try {
218
+ if (delayMs && delayMs > 0)
219
+ await (0, session_js_1.sleep)(Math.min(delayMs, 10_000));
220
+ if (stats || (0, session_js_1.readSessionState)(adapter.stateDir, sessionId)?.statsPendingRev !== undefined)
221
+ await collectSessionGitStats(adapter, sessionId);
222
+ await (0, session_js_1.flushSession)(adapter, config, sessionId);
223
+ }
224
+ catch {
225
+ // never surface errors from a background flush
226
+ }
227
+ }
228
+ /** Git runs outside the state lock, and a newer turn can invalidate its result. */
229
+ async function collectSessionGitStats(adapter, sessionId, compute = git_stats_js_1.computeGitStats) {
230
+ const snapshot = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
231
+ if (!snapshot || snapshot.offline || snapshot.state === 'WORKING')
232
+ return;
233
+ // Another scheduled flush may already have consumed an explicit --stats request.
234
+ if (snapshot.statsPendingRev === undefined)
235
+ return;
236
+ const revision = snapshot.statsPendingRev;
237
+ const stats = compute(snapshot.projectDir);
238
+ await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
239
+ const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
240
+ if (!fresh || fresh.offline || fresh.state === 'WORKING' || fresh.statsPendingRev !== revision)
241
+ return;
242
+ fresh.statsPendingRev = undefined;
243
+ fresh.agentStats = stats ?? undefined;
244
+ if (stats && stats.filesChanged > 0) {
245
+ const line = `Uncommitted changes — ${stats.filesChanged} file${stats.filesChanged === 1 ? '' : 's'}, +${stats.insertions}/−${stats.deletions}`;
246
+ fresh.activity = (0, session_js_1.pushHistory)(fresh.activity, line);
247
+ }
248
+ fresh.rev = (fresh.rev ?? 0) + 1;
249
+ (0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
250
+ });
251
+ }
252
+ //# sourceMappingURL=hook-runner.js.map
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HOOK_MARKER_COMMENT = exports.HOOK_MARKER = void 0;
4
+ exports.installHookGroups = installHookGroups;
5
+ exports.removeHookGroups = removeHookGroups;
6
+ exports.findNocvilleCommand = findNocvilleCommand;
7
+ exports.extractFlag = extractFlag;
8
+ exports.shellQuote = shellQuote;
9
+ exports.buildAgentHookCommand = buildAgentHookCommand;
10
+ exports.stableNodePath = stableNodePath;
11
+ exports.resolveCliPath = resolveCliPath;
12
+ exports.readJsonFile = readJsonFile;
13
+ exports.writeJsonFile = writeJsonFile;
14
+ exports.buildShimHookCommand = buildShimHookCommand;
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const shim_js_1 = require("./shim.js");
18
+ /**
19
+ * Matcher-group hook config shared by Claude Code (`~/.claude/settings.json`)
20
+ * and Codex (`~/.codex/hooks.json`) — both use:
21
+ *
22
+ * { "hooks": { "<Event>": [ { "matcher": "", "hooks": [ { "type": "command", "command": "…" } ] } ] } }
23
+ *
24
+ * Install/uninstall only ever touch entries whose command carries the
25
+ * `nocville-npc` marker; the user's own hooks are preserved verbatim.
26
+ */
27
+ /** Marker used to identify nocville-npc hook commands (flat or nested). */
28
+ exports.HOOK_MARKER = 'nocville-npc';
29
+ /**
30
+ * Trailing shell comment appended to hook commands. `/bin/sh` ignores it, but it
31
+ * guarantees the marker is present for idempotency regardless of install path.
32
+ */
33
+ exports.HOOK_MARKER_COMMENT = '# nocville-npc-hook';
34
+ function isCommandHookWithMarker(hook) {
35
+ if (!hook ||
36
+ typeof hook !== 'object' ||
37
+ typeof hook.command !== 'string')
38
+ return false;
39
+ const command = hook.command;
40
+ if (command.includes(exports.HOOK_MARKER) ||
41
+ /nocville-hook\.cmd"?\s+(?:claude-code|codex|gemini|cursor)\s+hook\b/.test(command))
42
+ return true;
43
+ // Recognize the legacy encoded Windows form so setup replaces it and uninstall removes it.
44
+ const encoded = /^powershell\.exe -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/=]+)$/.exec(command);
45
+ if (!encoded)
46
+ return false;
47
+ const decoded = Buffer.from(encoded[1], 'base64').toString('utf16le');
48
+ return (decoded.startsWith(`& cmd.exe /d /s /c '`) &&
49
+ decoded.endsWith(` # ${exports.HOOK_MARKER}`) &&
50
+ /nocville-hook\.cmd" (?:claude-code|codex|gemini|cursor) hook/.test(decoded));
51
+ }
52
+ function isGroup(entry) {
53
+ return (typeof entry === 'object' &&
54
+ entry !== null &&
55
+ Array.isArray(entry.hooks));
56
+ }
57
+ /** Old flat format: a bare command hook (no nested `hooks` array). */
58
+ function isFlatNocvilleEntry(entry) {
59
+ return !isGroup(entry) && isCommandHookWithMarker(entry);
60
+ }
61
+ /** Strip nocville hooks from one event's entry list. Returns kept entries + removed count. */
62
+ function stripNocville(entries) {
63
+ const kept = [];
64
+ let removed = 0;
65
+ let flat = 0;
66
+ for (const entry of entries) {
67
+ if (isFlatNocvilleEntry(entry)) {
68
+ removed++;
69
+ flat++;
70
+ continue;
71
+ }
72
+ if (isGroup(entry)) {
73
+ const keptHooks = entry.hooks.filter((h) => !isCommandHookWithMarker(h));
74
+ const diff = entry.hooks.length - keptHooks.length;
75
+ if (diff === 0)
76
+ kept.push(entry);
77
+ else {
78
+ removed += diff;
79
+ if (keptHooks.length > 0)
80
+ kept.push({ ...entry, hooks: keptHooks });
81
+ }
82
+ continue;
83
+ }
84
+ kept.push(entry);
85
+ }
86
+ return { kept, removed, flat };
87
+ }
88
+ /**
89
+ * Install one nocville hook group into each of `events`. Idempotent: existing
90
+ * nocville entries (flat legacy or nested) are removed first, preserving the
91
+ * user's own hooks, then a single group per event is appended. Mutates `file`.
92
+ */
93
+ function installHookGroups(file, command, events, extra = {}) {
94
+ if (!file.hooks || typeof file.hooks !== 'object' || Array.isArray(file.hooks))
95
+ file.hooks = {};
96
+ const root = file.hooks;
97
+ let migrated = 0;
98
+ let installed = 0;
99
+ // Drop nocville entries from events we no longer install, too.
100
+ for (const ev of Object.keys(root)) {
101
+ if (events.includes(ev) || !Array.isArray(root[ev]))
102
+ continue;
103
+ const { kept } = stripNocville(root[ev]);
104
+ if (kept.length === 0)
105
+ delete root[ev];
106
+ else
107
+ root[ev] = kept;
108
+ }
109
+ for (const ev of events) {
110
+ const existing = Array.isArray(root[ev]) ? root[ev] : [];
111
+ const { kept, flat } = stripNocville(existing);
112
+ migrated += flat;
113
+ const hook = { type: 'command', command };
114
+ if (extra.timeout !== undefined)
115
+ hook.timeout = extra.timeout;
116
+ Object.assign(hook, extra.perEvent?.[ev]);
117
+ kept.push({ matcher: '', hooks: [hook] });
118
+ installed++;
119
+ root[ev] = kept;
120
+ }
121
+ return { migrated, installed };
122
+ }
123
+ /** Remove every nocville hook entry. Mutates `file`; returns the count removed. */
124
+ function removeHookGroups(file) {
125
+ if (!file.hooks || typeof file.hooks !== 'object')
126
+ return 0;
127
+ const root = file.hooks;
128
+ let removed = 0;
129
+ for (const ev of Object.keys(root)) {
130
+ const arr = root[ev];
131
+ if (!Array.isArray(arr))
132
+ continue;
133
+ const res = stripNocville(arr);
134
+ removed += res.removed;
135
+ if (res.kept.length === 0)
136
+ delete root[ev];
137
+ else
138
+ root[ev] = res.kept;
139
+ }
140
+ if (Object.keys(root).length === 0)
141
+ delete file.hooks;
142
+ return removed;
143
+ }
144
+ /** First nocville hook command found in any event (any format). */
145
+ function findNocvilleCommand(file) {
146
+ const root = file.hooks;
147
+ if (!root || typeof root !== 'object')
148
+ return undefined;
149
+ for (const ev of Object.keys(root)) {
150
+ const arr = root[ev];
151
+ if (!Array.isArray(arr))
152
+ continue;
153
+ for (const entry of arr) {
154
+ if (isFlatNocvilleEntry(entry))
155
+ return entry.command;
156
+ if (isGroup(entry)) {
157
+ const hook = entry.hooks.find(isCommandHookWithMarker);
158
+ if (hook)
159
+ return hook.command;
160
+ }
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ /** Extract a `--flag` value from a command string; supports `=`, quotes, or the space form. */
166
+ function extractFlag(command, flag) {
167
+ const re = new RegExp(`--${flag}(?:=|\\s+)('([^']*)'|"([^"]*)"|([^\\s]+))`);
168
+ const m = command.match(re);
169
+ if (!m)
170
+ return undefined;
171
+ return m[2] ?? m[3] ?? m[4];
172
+ }
173
+ /** POSIX single-quote a string so /bin/sh treats it as one literal token. */
174
+ function shellQuote(s) {
175
+ return `'${s.replace(/'/g, `'\\''`)}'`;
176
+ }
177
+ /**
178
+ * Absolute hook command: `'<node>' '<cli.js>' <agent> hook # nocville-npc-hook`.
179
+ *
180
+ * Agents run hooks via `/bin/sh` without the user's interactive PATH (nvm /
181
+ * homebrew shims are absent), so a bare `nocville-npc` would fail; absolute
182
+ * paths fix that. No API key on the command line — the hook reads the saved
183
+ * user config (~/.config/nocville/config.json).
184
+ */
185
+ function buildAgentHookCommand(cliPath, agent, extraArgs = [], nodePath = stableNodePath()) {
186
+ const args = extraArgs.length ? ' ' + extraArgs.join(' ') : '';
187
+ return `${shellQuote(nodePath)} ${shellQuote(cliPath)} ${agent} hook${args} ${exports.HOOK_MARKER_COMMENT}`;
188
+ }
189
+ /**
190
+ * The node binary to bake into hook commands. `process.execPath` is often a
191
+ * versioned path (e.g. Homebrew's /opt/homebrew/Cellar/node/24.4.1/bin/node)
192
+ * that disappears on upgrade, so prefer an unversioned PATH entry (e.g.
193
+ * /opt/homebrew/bin/node) that resolves to the same binary.
194
+ */
195
+ function stableNodePath(execPath = process.execPath, pathEnv = process.env['PATH'] ?? '') {
196
+ let target;
197
+ try {
198
+ target = (0, node_fs_1.realpathSync)(execPath);
199
+ }
200
+ catch {
201
+ return execPath;
202
+ }
203
+ const exe = process.platform === 'win32' ? 'node.exe' : 'node';
204
+ for (const dir of pathEnv.split(node_path_1.delimiter)) {
205
+ if (!dir || /[\\/]Cellar[\\/]/.test(dir))
206
+ continue;
207
+ const candidate = (0, node_path_1.join)(dir, exe);
208
+ try {
209
+ if (candidate !== execPath && (0, node_fs_1.realpathSync)(candidate) === target)
210
+ return candidate;
211
+ }
212
+ catch {
213
+ // not here
214
+ }
215
+ }
216
+ return execPath;
217
+ }
218
+ /** Realpath the running CLI (follows npm bin symlinks). */
219
+ function resolveCliPath(explicit) {
220
+ const candidate = explicit ?? process.argv[1];
221
+ if (candidate) {
222
+ try {
223
+ return (0, node_fs_1.realpathSync)(candidate);
224
+ }
225
+ catch {
226
+ return candidate;
227
+ }
228
+ }
229
+ return 'nocville-npc';
230
+ }
231
+ /** Read a JSON hooks/settings file; missing → {}; unparsable → null (refuse to overwrite). */
232
+ function readJsonFile(path) {
233
+ if (!(0, node_fs_1.existsSync)(path))
234
+ return {};
235
+ try {
236
+ const raw = (0, node_fs_1.readFileSync)(path, 'utf-8');
237
+ if (!raw.trim())
238
+ return {};
239
+ const parsed = JSON.parse(raw);
240
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
241
+ return null;
242
+ return parsed;
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ /** Write JSON, keeping a one-time `.nocville-backup` of the original file. */
249
+ function writeJsonFile(path, data) {
250
+ const backup = `${path}.nocville-backup`;
251
+ if ((0, node_fs_1.existsSync)(path) && !(0, node_fs_1.existsSync)(backup)) {
252
+ try {
253
+ (0, node_fs_1.copyFileSync)(path, backup);
254
+ }
255
+ catch {
256
+ // best effort
257
+ }
258
+ }
259
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
260
+ }
261
+ function buildShimHookCommand(agent, platform = process.platform) {
262
+ if (platform === 'win32') {
263
+ const shim = `${(0, shim_js_1.SHIM_PATH)()}.cmd`;
264
+ if (agent === 'claude-code') {
265
+ // installHooksInto sets shell: powershell, bypassing Git Bash detection.
266
+ return `& '${shim.replace(/['‘’‚‛]/g, '$&$&')}' ${agent} hook ${exports.HOOK_MARKER_COMMENT}`;
267
+ }
268
+ const payload = `""${shim}" ${agent} hook"`;
269
+ if (agent === 'gemini') {
270
+ // Gemini uses PowerShell (7 or 5.1). Stop native-argument parsing so cmd
271
+ // receives its quotes intact. The newline ends --% before Gemini appends
272
+ // its own PowerShell exit-code check. No encoded command or policy bypass.
273
+ return `cmd.exe /d /s /c --% ${payload}\n`;
274
+ }
275
+ return `cmd.exe /d /s /c ${payload}`;
276
+ }
277
+ return `${shellQuote((0, shim_js_1.SHIM_PATH)())} ${agent} hook ${exports.HOOK_MARKER_COMMENT}`;
278
+ }
279
+ //# sourceMappingURL=hook-settings.js.map
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAgentAlive = isAgentAlive;
4
+ exports.claudePidFromStore = claudePidFromStore;
5
+ exports.ancestorPid = ancestorPid;
6
+ exports.resolveAgentPid = resolveAgentPid;
7
+ exports.reserveAgentPidAttempt = reserveAgentPidAttempt;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ function startTime(pid, timeout = 1000) {
13
+ if (process.platform === 'win32')
14
+ return undefined;
15
+ const out = (0, node_child_process_1.spawnSync)('ps', ['-o', 'lstart=', '-p', String(pid)], {
16
+ encoding: 'utf8',
17
+ timeout,
18
+ });
19
+ const value = Date.parse(out.stdout?.trim() ?? '');
20
+ return Number.isFinite(value) ? value : undefined;
21
+ }
22
+ function isAgentAlive(proc) {
23
+ if (!Number.isSafeInteger(proc.pid) || proc.pid <= 0)
24
+ return false;
25
+ try {
26
+ process.kill(proc.pid, 0);
27
+ }
28
+ catch (error) {
29
+ return error.code === 'EPERM';
30
+ }
31
+ if (!proc.startedAt || process.platform === 'win32')
32
+ return true;
33
+ const actual = startTime(proc.pid);
34
+ return actual === undefined || Math.abs(actual - proc.startedAt) < 120_000;
35
+ }
36
+ function claudePidFromStore(sessionId, dir = (0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'sessions')) {
37
+ try {
38
+ for (const file of (0, node_fs_1.readdirSync)(dir)) {
39
+ if (!/^\d+\.json$/.test(file))
40
+ continue;
41
+ try {
42
+ const data = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, file), 'utf8'));
43
+ if (data.sessionId !== sessionId || !Number.isSafeInteger(data.pid) || data.pid <= 0)
44
+ continue;
45
+ const proc = {
46
+ pid: data.pid,
47
+ startedAt: typeof data.startedAt === 'number' ? data.startedAt : undefined,
48
+ };
49
+ if (isAgentAlive(proc))
50
+ return proc;
51
+ }
52
+ catch {
53
+ /* incomplete or old sidecar */
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ /* no session store */
59
+ }
60
+ return null;
61
+ }
62
+ function ancestorPid(names, start = process.ppid, nodeCommandPattern) {
63
+ if (process.platform === 'win32')
64
+ return null;
65
+ const deadline = Date.now() + 750;
66
+ let pid = start;
67
+ for (let hop = 0; hop < 5 && pid > 1 && Date.now() < deadline; hop++) {
68
+ const out = (0, node_child_process_1.spawnSync)('ps', ['-o', 'ppid=,comm=', '-p', String(pid)], {
69
+ encoding: 'utf8',
70
+ timeout: Math.max(1, deadline - Date.now()),
71
+ });
72
+ const match = out.stdout?.trim().match(/^(\d+)\s+(.+)$/);
73
+ if (!match)
74
+ return null;
75
+ const command = (0, node_path_1.basename)(match[2]);
76
+ if (names.includes(command)) {
77
+ if (command !== 'node')
78
+ return { pid, startedAt: startTime(pid, Math.max(1, deadline - Date.now())) };
79
+ if (nodeCommandPattern) {
80
+ const argv = (0, node_child_process_1.spawnSync)('ps', ['-o', 'command=', '-p', String(pid)], {
81
+ encoding: 'utf8',
82
+ timeout: Math.max(1, deadline - Date.now()),
83
+ }).stdout?.trim();
84
+ if (argv && nodeCommandPattern.test(argv))
85
+ return { pid, startedAt: startTime(pid, Math.max(1, deadline - Date.now())) };
86
+ }
87
+ }
88
+ const parent = Number(match[1]);
89
+ if (parent === pid)
90
+ return null;
91
+ pid = parent;
92
+ }
93
+ return null;
94
+ }
95
+ function resolveAgentPid(adapter, sessionId) {
96
+ if (adapter.processNames?.length === 0)
97
+ return null;
98
+ return ((adapter.family === 'claude' ? claudePidFromStore(sessionId) : null) ??
99
+ ancestorPid(adapter.processNames ?? [adapter.command], process.ppid, adapter.nodeCommandPattern));
100
+ }
101
+ /** Reserve under the state lock so simultaneous hooks cannot duplicate PID probes. */
102
+ function reserveAgentPidAttempt(state, now) {
103
+ if (state.agentPid ||
104
+ state.offline ||
105
+ (state.agentPidResolveAttempts ?? 0) >= 5 ||
106
+ (state.agentPidLastResolveAt !== undefined && now - state.agentPidLastResolveAt < 60_000))
107
+ return false;
108
+ state.agentPidResolveAttempts = (state.agentPidResolveAttempts ?? 0) + 1;
109
+ state.agentPidLastResolveAt = now;
110
+ return true;
111
+ }
112
+ //# sourceMappingURL=liveness.js.map