@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,105 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
+ function startTime(pid, timeout = 1000) {
6
+ if (process.platform === 'win32')
7
+ return undefined;
8
+ const out = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
9
+ encoding: 'utf8',
10
+ timeout,
11
+ });
12
+ const value = Date.parse(out.stdout?.trim() ?? '');
13
+ return Number.isFinite(value) ? value : undefined;
14
+ }
15
+ export function isAgentAlive(proc) {
16
+ if (!Number.isSafeInteger(proc.pid) || proc.pid <= 0)
17
+ return false;
18
+ try {
19
+ process.kill(proc.pid, 0);
20
+ }
21
+ catch (error) {
22
+ return error.code === 'EPERM';
23
+ }
24
+ if (!proc.startedAt || process.platform === 'win32')
25
+ return true;
26
+ const actual = startTime(proc.pid);
27
+ return actual === undefined || Math.abs(actual - proc.startedAt) < 120_000;
28
+ }
29
+ export function claudePidFromStore(sessionId, dir = join(homedir(), '.claude', 'sessions')) {
30
+ try {
31
+ for (const file of readdirSync(dir)) {
32
+ if (!/^\d+\.json$/.test(file))
33
+ continue;
34
+ try {
35
+ const data = JSON.parse(readFileSync(join(dir, file), 'utf8'));
36
+ if (data.sessionId !== sessionId || !Number.isSafeInteger(data.pid) || data.pid <= 0)
37
+ continue;
38
+ const proc = {
39
+ pid: data.pid,
40
+ startedAt: typeof data.startedAt === 'number' ? data.startedAt : undefined,
41
+ };
42
+ if (isAgentAlive(proc))
43
+ return proc;
44
+ }
45
+ catch {
46
+ /* incomplete or old sidecar */
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ /* no session store */
52
+ }
53
+ return null;
54
+ }
55
+ export function ancestorPid(names, start = process.ppid, nodeCommandPattern) {
56
+ if (process.platform === 'win32')
57
+ return null;
58
+ const deadline = Date.now() + 750;
59
+ let pid = start;
60
+ for (let hop = 0; hop < 5 && pid > 1 && Date.now() < deadline; hop++) {
61
+ const out = spawnSync('ps', ['-o', 'ppid=,comm=', '-p', String(pid)], {
62
+ encoding: 'utf8',
63
+ timeout: Math.max(1, deadline - Date.now()),
64
+ });
65
+ const match = out.stdout?.trim().match(/^(\d+)\s+(.+)$/);
66
+ if (!match)
67
+ return null;
68
+ const command = basename(match[2]);
69
+ if (names.includes(command)) {
70
+ if (command !== 'node')
71
+ return { pid, startedAt: startTime(pid, Math.max(1, deadline - Date.now())) };
72
+ if (nodeCommandPattern) {
73
+ const argv = spawnSync('ps', ['-o', 'command=', '-p', String(pid)], {
74
+ encoding: 'utf8',
75
+ timeout: Math.max(1, deadline - Date.now()),
76
+ }).stdout?.trim();
77
+ if (argv && nodeCommandPattern.test(argv))
78
+ return { pid, startedAt: startTime(pid, Math.max(1, deadline - Date.now())) };
79
+ }
80
+ }
81
+ const parent = Number(match[1]);
82
+ if (parent === pid)
83
+ return null;
84
+ pid = parent;
85
+ }
86
+ return null;
87
+ }
88
+ export function resolveAgentPid(adapter, sessionId) {
89
+ if (adapter.processNames?.length === 0)
90
+ return null;
91
+ return ((adapter.family === 'claude' ? claudePidFromStore(sessionId) : null) ??
92
+ ancestorPid(adapter.processNames ?? [adapter.command], process.ppid, adapter.nodeCommandPattern));
93
+ }
94
+ /** Reserve under the state lock so simultaneous hooks cannot duplicate PID probes. */
95
+ export function reserveAgentPidAttempt(state, now) {
96
+ if (state.agentPid ||
97
+ state.offline ||
98
+ (state.agentPidResolveAttempts ?? 0) >= 5 ||
99
+ (state.agentPidLastResolveAt !== undefined && now - state.agentPidLastResolveAt < 60_000))
100
+ return false;
101
+ state.agentPidResolveAttempts = (state.agentPidResolveAttempts ?? 0) + 1;
102
+ state.agentPidLastResolveAt = now;
103
+ return true;
104
+ }
105
+ //# sourceMappingURL=liveness.js.map
@@ -0,0 +1,6 @@
1
+ export declare const LOCK_STALE_MS = 5000;
2
+ export declare const LOCK_WAIT_MS = 2000;
3
+ export declare const LOCK_HARD_STALE_MS = 30000;
4
+ /** Exclusive cross-process lock. A timeout skips the hook; it never permits an unlocked write. */
5
+ export declare function withStateLock<T>(path: string, fn: () => T | Promise<T>, waitMs?: number): Promise<T>;
6
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync, } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ export const LOCK_STALE_MS = 5000;
5
+ export const LOCK_WAIT_MS = 2000;
6
+ export const LOCK_HARD_STALE_MS = 30_000;
7
+ function alive(pid) {
8
+ if (!Number.isSafeInteger(pid) || pid <= 0)
9
+ return false;
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ }
14
+ catch (error) {
15
+ return error.code === 'EPERM';
16
+ }
17
+ }
18
+ /** Exclusive cross-process lock. A timeout skips the hook; it never permits an unlocked write. */
19
+ export async function withStateLock(path, fn, waitMs = LOCK_WAIT_MS) {
20
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
21
+ const lock = `${path}.lock`;
22
+ const token = `${process.pid}:${randomUUID()}`;
23
+ const deadline = Date.now() + waitMs;
24
+ for (;;) {
25
+ try {
26
+ const fd = openSync(lock, 'wx', 0o600);
27
+ try {
28
+ writeSync(fd, token);
29
+ }
30
+ finally {
31
+ closeSync(fd);
32
+ }
33
+ break;
34
+ }
35
+ catch (error) {
36
+ if (error.code !== 'EEXIST')
37
+ throw error;
38
+ // Serialize stale reclamation so two waiters cannot unlink each other's new lock.
39
+ let reaper;
40
+ try {
41
+ const reapPath = `${lock}.reap`;
42
+ try {
43
+ const oldToken = readFileSync(reapPath, 'utf8');
44
+ if (Date.now() - statSync(reapPath).mtimeMs > LOCK_STALE_MS &&
45
+ readFileSync(reapPath, 'utf8') === oldToken)
46
+ unlinkSync(reapPath);
47
+ }
48
+ catch {
49
+ /* no stale reaper */
50
+ }
51
+ reaper = openSync(reapPath, 'wx', 0o600);
52
+ writeSync(reaper, token);
53
+ const owner = readFileSync(lock, 'utf8');
54
+ const age = Date.now() - statSync(lock).mtimeMs;
55
+ // PID reuse cannot block forever. State mutations are synchronous; the separate
56
+ // delivery lock bounds its HTTP request well below this hard ceiling.
57
+ if (age > LOCK_HARD_STALE_MS ||
58
+ (age > LOCK_STALE_MS && !alive(Number(owner.split(':')[0]))))
59
+ unlinkSync(lock);
60
+ }
61
+ catch {
62
+ /* owner released, or another waiter is reclaiming it */
63
+ }
64
+ finally {
65
+ if (reaper !== undefined) {
66
+ closeSync(reaper);
67
+ try {
68
+ if (readFileSync(`${lock}.reap`, 'utf8') === token)
69
+ unlinkSync(`${lock}.reap`);
70
+ }
71
+ catch {
72
+ /* stale reaper already removed */
73
+ }
74
+ }
75
+ }
76
+ if (Date.now() >= deadline)
77
+ throw new Error('Session state lock timed out');
78
+ await new Promise((resolve) => setTimeout(resolve, 10 + Math.random() * 15));
79
+ }
80
+ }
81
+ try {
82
+ return await fn();
83
+ }
84
+ finally {
85
+ // Never unlink a replacement lock owned by another writer.
86
+ try {
87
+ if (readFileSync(lock, 'utf8') === token)
88
+ unlinkSync(lock);
89
+ }
90
+ catch {
91
+ /* already gone */
92
+ }
93
+ }
94
+ }
95
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hooks must point at a cli.js that will still exist tomorrow. When setup runs
3
+ * from an ephemeral location — `npx @nocville/npc …` executes out of the npm
4
+ * cache (`~/.npm/_npx/<hash>/…`), which npm may prune at any time — we copy
5
+ * the package (dist + its `@nocville/node` dependency) into
6
+ * `~/.config/nocville/runtime/<version>/` and point hooks there instead.
7
+ */
8
+ /** True when the CLI is running from npm's npx cache (or another temp dir). */
9
+ export declare function isEphemeralCliPath(cliPath: string): boolean;
10
+ /**
11
+ * Always copy a valid installed runtime, including global installs removed by nvm uninstall.
12
+ */
13
+ export declare function ensureStableCliPath(cliPath: string, _force?: boolean): string;
14
+ /** Prune only old snapshots, never the active pointer target or unknown directories. */
15
+ export declare function pruneOldRuntimes(runtimeDir: string, now?: number): number;
16
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,149 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, sep, resolve } from 'node:path';
5
+ import { userConfigPath } from '../../core/user-config.js';
6
+ /**
7
+ * Hooks must point at a cli.js that will still exist tomorrow. When setup runs
8
+ * from an ephemeral location — `npx @nocville/npc …` executes out of the npm
9
+ * cache (`~/.npm/_npx/<hash>/…`), which npm may prune at any time — we copy
10
+ * the package (dist + its `@nocville/node` dependency) into
11
+ * `~/.config/nocville/runtime/<version>/` and point hooks there instead.
12
+ */
13
+ /** True when the CLI is running from npm's npx cache (or another temp dir). */
14
+ export function isEphemeralCliPath(cliPath) {
15
+ const p = cliPath.split(sep).join('/');
16
+ return p.includes('/_npx/') || p.includes('/.pnpm-store/') || p.includes('/dlx-');
17
+ }
18
+ function findPackageRoot(start, name) {
19
+ let dir = start;
20
+ for (let i = 0; i < 8; i++) {
21
+ const pkg = join(dir, 'package.json');
22
+ if (existsSync(pkg)) {
23
+ try {
24
+ const parsed = JSON.parse(readFileSync(pkg, 'utf-8'));
25
+ if (parsed.name === name)
26
+ return dir;
27
+ }
28
+ catch {
29
+ // keep walking
30
+ }
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir)
34
+ break;
35
+ dir = parent;
36
+ }
37
+ return null;
38
+ }
39
+ function readVersion(root) {
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'));
42
+ return parsed.version ?? '0.0.0';
43
+ }
44
+ catch {
45
+ return '0.0.0';
46
+ }
47
+ }
48
+ /**
49
+ * Always copy a valid installed runtime, including global installs removed by nvm uninstall.
50
+ */
51
+ export function ensureStableCliPath(cliPath, _force = true) {
52
+ const pkgRoot = findPackageRoot(dirname(cliPath), '@nocville/npc');
53
+ if (!pkgRoot)
54
+ return cliPath;
55
+ let sdkRoot = null;
56
+ try {
57
+ const req = createRequire(cliPath);
58
+ sdkRoot = findPackageRoot(dirname(req.resolve('@nocville/node')), '@nocville/node');
59
+ }
60
+ catch {
61
+ sdkRoot = null;
62
+ }
63
+ if (!sdkRoot)
64
+ return cliPath;
65
+ const req = createRequire(cliPath);
66
+ const pngRoot = findPackageRoot(dirname(req.resolve('pngjs')), 'pngjs');
67
+ if (!pngRoot)
68
+ throw new Error('Cannot install hook runtime: pngjs is missing');
69
+ const version = readVersion(pkgRoot);
70
+ const hash = createHash('sha256');
71
+ const hashTree = (root) => {
72
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
73
+ hash.update(entry.name);
74
+ const path = join(root, entry.name);
75
+ if (entry.isDirectory())
76
+ hashTree(path);
77
+ else if (entry.isFile())
78
+ hash.update(readFileSync(path));
79
+ }
80
+ };
81
+ for (const root of [pkgRoot, sdkRoot]) {
82
+ hash.update(readFileSync(join(root, 'package.json')));
83
+ hashTree(join(root, 'dist'));
84
+ }
85
+ hash.update(readFileSync(join(pngRoot, 'package.json')));
86
+ hashTree(join(pngRoot, 'lib'));
87
+ // Immutable snapshots also support multiple development builds of one version.
88
+ // Never remove files a running watchdog or the current pointer may still use.
89
+ const dest = join(dirname(userConfigPath()), 'runtime', `${version}-${hash.digest('hex').slice(0, 16)}`);
90
+ if (existsSync(join(dest, 'dist', 'cli.js')))
91
+ return join(dest, 'dist', 'cli.js');
92
+ const stage = `${dest}.${randomUUID()}.tmp`;
93
+ try {
94
+ mkdirSync(stage, { recursive: true, mode: 0o700 });
95
+ cpSync(join(pkgRoot, 'package.json'), join(stage, 'package.json'));
96
+ cpSync(join(pkgRoot, 'dist'), join(stage, 'dist'), { recursive: true });
97
+ const sdkDest = join(stage, 'node_modules', '@nocville', 'node');
98
+ mkdirSync(sdkDest, { recursive: true });
99
+ cpSync(join(sdkRoot, 'package.json'), join(sdkDest, 'package.json'));
100
+ cpSync(join(sdkRoot, 'dist'), join(sdkDest, 'dist'), { recursive: true });
101
+ const pngDest = join(stage, 'node_modules', 'pngjs');
102
+ mkdirSync(pngDest, { recursive: true });
103
+ cpSync(join(pngRoot, 'package.json'), join(pngDest, 'package.json'));
104
+ cpSync(join(pngRoot, 'lib'), join(pngDest, 'lib'), { recursive: true });
105
+ try {
106
+ renameSync(stage, dest);
107
+ }
108
+ catch (error) {
109
+ if (!existsSync(join(dest, 'dist', 'cli.js')))
110
+ throw error;
111
+ }
112
+ return join(dest, 'dist', 'cli.js');
113
+ }
114
+ finally {
115
+ rmSync(stage, { recursive: true, force: true });
116
+ }
117
+ }
118
+ /** Prune only old snapshots, never the active pointer target or unknown directories. */
119
+ export function pruneOldRuntimes(runtimeDir, now = Date.now()) {
120
+ let active;
121
+ try {
122
+ const pointer = JSON.parse(readFileSync(join(runtimeDir, 'current.json'), 'utf8'));
123
+ if (typeof pointer.cli !== 'string')
124
+ return 0;
125
+ active = resolve(pointer.cli);
126
+ }
127
+ catch {
128
+ return 0;
129
+ }
130
+ let removed = 0;
131
+ for (const entry of readdirSync(runtimeDir, { withFileTypes: true })) {
132
+ if (!entry.isDirectory() || !/^\d[^/]*-[a-f0-9]{16}$/.test(entry.name))
133
+ continue;
134
+ const path = resolve(runtimeDir, entry.name);
135
+ if (active === path || active.startsWith(path + sep))
136
+ continue;
137
+ try {
138
+ if (now - statSync(path).mtimeMs <= 7 * 86_400_000)
139
+ continue;
140
+ rmSync(path, { recursive: true, force: true });
141
+ removed++;
142
+ }
143
+ catch {
144
+ /* best-effort maintenance must not break setup */
145
+ }
146
+ }
147
+ return removed;
148
+ }
149
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,192 @@
1
+ import type { StatusUpdate, AgentStats } from '@nocville/node';
2
+ import { type SessionFilter } from '../../core/session-filter.js';
3
+ import type { NpcConfig, Status, Mood, BehaviorHints } from '../../core/types.js';
4
+ /**
5
+ * Shared session plumbing for hook-driven AI-agent NPCs (Claude Code, Codex).
6
+ *
7
+ * Each agent session is one NPC. A hook invocation reads the session's state
8
+ * file, applies the event, writes the state back, and decides whether to push:
9
+ *
10
+ * - phase changes (working ↔ waiting ↔ idle) and the first push go out at once;
11
+ * - rapid same-phase activity changes (PreToolUse bursts) are coalesced so the
12
+ * service stays well under the API rate limit, while the *latest* activity
13
+ * still lands — a deferred flush pushes whatever the state is when it fires.
14
+ *
15
+ * Pushes run in a detached background process (`<agent> flush <session>`) so a
16
+ * hook returns in tens of milliseconds and never blocks or fails the agent.
17
+ */
18
+ export type AgentPhase = 'IDLE' | 'WORKING' | 'WAITING';
19
+ export interface AgentSessionState {
20
+ hookDeliveries?: {
21
+ key: string;
22
+ source: string;
23
+ at: number;
24
+ }[];
25
+ agentStats?: AgentStats;
26
+ /** Stop revision awaiting collection by a detached flush (including coalesced flushes). */
27
+ statsPendingRev?: number;
28
+ sessionId: string;
29
+ serviceId: string;
30
+ startedAt?: number;
31
+ agentPid?: number;
32
+ agentStartedAt?: number;
33
+ agentPidResolveAttempts?: number;
34
+ agentPidLastResolveAt?: number;
35
+ watchdogPid?: number;
36
+ /** First terminal delivery attempt, persisted across watchdog restarts. */
37
+ offlineRetryStartedAt?: number;
38
+ /** Terminal delivery exhausted its budget or received a non-retryable response. */
39
+ offlineDeliveryStopped?: boolean;
40
+ subagents?: Record<string, {
41
+ type: string;
42
+ activity: string;
43
+ at: number;
44
+ }>;
45
+ /** Machine phase. */
46
+ state: AgentPhase;
47
+ projectDir: string;
48
+ lastEvent: string;
49
+ lastUpdate: number;
50
+ /** Recent activity lines, most recent FIRST (desk-sign shows values[0]). */
51
+ activity?: string[];
52
+ /** Cached user-facing session name (Claude Code session title), if any. */
53
+ name?: string;
54
+ status?: Status;
55
+ mood?: Mood;
56
+ /** Offline sentinel pending push (session ended). */
57
+ offline?: boolean;
58
+ /**
59
+ * The office currently shows this session (last delivered push was not offline). Lets a
60
+ * session that becomes filtered out (sessions.json) despawn once instead of lingering.
61
+ */
62
+ published?: boolean;
63
+ /** Monotonic revision bumped on every applied change. */
64
+ rev?: number;
65
+ /** Revision last pushed successfully. */
66
+ pushedRev?: number;
67
+ /** Epoch ms of the last push. */
68
+ lastPushAt?: number;
69
+ /** Epoch ms a deferred flush is scheduled for. */
70
+ flushAt?: number;
71
+ }
72
+ /** Result of applying one agent event. */
73
+ export interface AgentTransition {
74
+ state: AgentPhase;
75
+ /** Current activity line; `undefined` keeps the previous one. */
76
+ activity?: string;
77
+ status: Status;
78
+ mood: Mood;
79
+ offline?: boolean;
80
+ /** Event carried nothing displayable — record it but never push for it. */
81
+ quiet?: boolean;
82
+ }
83
+ export interface StatusOutput {
84
+ status: Status;
85
+ mood: Mood;
86
+ message: string;
87
+ /** 'working' | 'waiting' | 'idle' | 'offline'. */
88
+ subStatus?: string;
89
+ behavior?: BehaviorHints;
90
+ offline?: boolean;
91
+ }
92
+ /** Behavior hints + subStatus for a phase (shared by every agent NPC). */
93
+ export declare function phasePresentation(state: AgentPhase, offline?: boolean): {
94
+ subStatus: string;
95
+ behavior?: BehaviorHints;
96
+ };
97
+ /** Static identity of an agent integration. */
98
+ export interface AgentAdapter {
99
+ /** CLI command name: 'claude-code' | 'codex'. */
100
+ command: string;
101
+ /** Banner prefix: 'Claude' | 'Codex'. */
102
+ label: string;
103
+ /** Appearance family sent as `appearance.family` (branded NPC tee + headgear). */
104
+ family: 'claude' | 'codex' | 'gemini' | 'cursor';
105
+ stateDir: string;
106
+ /** Heartbeat sent with each push — the server marks the NPC unknown at 2x. */
107
+ heartbeatSeconds: number;
108
+ processNames?: readonly string[];
109
+ /** A node ancestor must match this argv pattern; unrelated node processes never qualify. */
110
+ nodeCommandPattern?: RegExp;
111
+ }
112
+ export declare const ACTIVITY_HISTORY = 8;
113
+ export declare const DEFAULT_MIN_PUSH_GAP_MS = 1500;
114
+ export declare function projectNameOf(cwd: string): string;
115
+ /** "Claude · nocville", clamped to the API's 32-char banner limit. */
116
+ export declare function agentDisplayName(label: string, subject: string): string;
117
+ /** Service ids are ≤100 chars of [A-Za-z0-9_.-]. */
118
+ export declare function sanitizeServiceId(raw: string): string;
119
+ export declare function stateFilePath(stateDir: string, sessionId: string): string;
120
+ export declare function readSessionState(stateDir: string, sessionId: string): AgentSessionState | null;
121
+ /** Atomic write (temp file + rename) so concurrent readers never see half a file. */
122
+ export declare function writeSessionState(stateDir: string, state: AgentSessionState): void;
123
+ export declare function deleteSessionState(stateDir: string, sessionId: string): void;
124
+ /** Remove state files older than `maxAgeMs`. Returns the number removed. */
125
+ export declare function cleanupSessionStates(stateDir: string, maxAgeMs: number, now?: number): Promise<number>;
126
+ /** Cross-process hourly maintenance, invoked only on SessionStart. */
127
+ export declare function maybeCleanupSessionStates(stateDir: string, now?: number): Promise<number>;
128
+ /** Prepend `line` to the history (deduping an identical head), capped. */
129
+ export declare function pushHistory(prev: string[] | undefined, line: string | undefined): string[];
130
+ export interface ApplyInput {
131
+ sessionId: string;
132
+ serviceId: string;
133
+ projectDir: string;
134
+ eventName: string;
135
+ name?: string;
136
+ transition: AgentTransition;
137
+ now: number;
138
+ }
139
+ export type PushDecision = {
140
+ kind: 'none';
141
+ } | {
142
+ kind: 'now';
143
+ } | {
144
+ kind: 'defer';
145
+ delayMs: number;
146
+ };
147
+ /**
148
+ * Apply a transition to the previous state and decide when to push.
149
+ * Pure — exposed for testing.
150
+ */
151
+ export declare function applyTransition(prev: AgentSessionState | null, input: ApplyInput, minGapMs?: number): {
152
+ next: AgentSessionState;
153
+ decision: PushDecision;
154
+ };
155
+ /** Build the status push for a session (offline sentinel when the session ended). */
156
+ export declare function buildAgentPayload(adapter: AgentAdapter, state: AgentSessionState, config: Pick<NpcConfig, 'name' | 'map'>): StatusUpdate;
157
+ export interface PushDeps {
158
+ /** Override the HTTP push (tests). */
159
+ push?: (serviceId: string, payload: StatusUpdate) => Promise<{
160
+ success: boolean;
161
+ error?: string;
162
+ statusCode?: number;
163
+ }>;
164
+ onResult?: (result: {
165
+ success: boolean;
166
+ error?: string;
167
+ statusCode?: number;
168
+ }) => void;
169
+ timeoutMs?: number;
170
+ force?: boolean;
171
+ lockWaitMs?: number;
172
+ /** Override sessions.json (tests). */
173
+ sessionFilter?: SessionFilter;
174
+ }
175
+ /**
176
+ * Push the session's current state if it has unpushed changes (or is offline),
177
+ * then record the push. Re-reads the state right before writing so a hook that
178
+ * ran concurrently isn't clobbered. Never throws.
179
+ */
180
+ export declare function flushSession(adapter: AgentAdapter, config: NpcConfig, sessionId: string, deps?: PushDeps): Promise<boolean>;
181
+ /** Current parent activity, up to three subagents, then recent parent history. */
182
+ export declare function tickerLines(state: AgentSessionState): string[];
183
+ export declare function applySubagentEvent(prev: AgentSessionState, agentId: string, type: string, event: string, activity: string, now: number): AgentSessionState;
184
+ /**
185
+ * Hand a flush to a detached background process so the hook can exit at once.
186
+ * The child inherits the resolved config via env (not argv, so the key never
187
+ * shows up in `ps`).
188
+ */
189
+ export declare function spawnDetachedFlush(adapter: AgentAdapter, config: NpcConfig, sessionId: string, delayMs: number, cliPath?: string | undefined, stats?: boolean): void;
190
+ export declare function spawnDetachedAgent(adapter: AgentAdapter, config: NpcConfig, args: string[], cliPath?: string | undefined): number | undefined;
191
+ export declare function sleep(ms: number): Promise<void>;
192
+ //# sourceMappingURL=session.d.ts.map