@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,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOCK_HARD_STALE_MS = exports.LOCK_WAIT_MS = exports.LOCK_STALE_MS = void 0;
4
+ exports.withStateLock = withStateLock;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ exports.LOCK_STALE_MS = 5000;
9
+ exports.LOCK_WAIT_MS = 2000;
10
+ exports.LOCK_HARD_STALE_MS = 30_000;
11
+ function alive(pid) {
12
+ if (!Number.isSafeInteger(pid) || pid <= 0)
13
+ return false;
14
+ try {
15
+ process.kill(pid, 0);
16
+ return true;
17
+ }
18
+ catch (error) {
19
+ return error.code === 'EPERM';
20
+ }
21
+ }
22
+ /** Exclusive cross-process lock. A timeout skips the hook; it never permits an unlocked write. */
23
+ async function withStateLock(path, fn, waitMs = exports.LOCK_WAIT_MS) {
24
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
25
+ const lock = `${path}.lock`;
26
+ const token = `${process.pid}:${(0, node_crypto_1.randomUUID)()}`;
27
+ const deadline = Date.now() + waitMs;
28
+ for (;;) {
29
+ try {
30
+ const fd = (0, node_fs_1.openSync)(lock, 'wx', 0o600);
31
+ try {
32
+ (0, node_fs_1.writeSync)(fd, token);
33
+ }
34
+ finally {
35
+ (0, node_fs_1.closeSync)(fd);
36
+ }
37
+ break;
38
+ }
39
+ catch (error) {
40
+ if (error.code !== 'EEXIST')
41
+ throw error;
42
+ // Serialize stale reclamation so two waiters cannot unlink each other's new lock.
43
+ let reaper;
44
+ try {
45
+ const reapPath = `${lock}.reap`;
46
+ try {
47
+ const oldToken = (0, node_fs_1.readFileSync)(reapPath, 'utf8');
48
+ if (Date.now() - (0, node_fs_1.statSync)(reapPath).mtimeMs > exports.LOCK_STALE_MS &&
49
+ (0, node_fs_1.readFileSync)(reapPath, 'utf8') === oldToken)
50
+ (0, node_fs_1.unlinkSync)(reapPath);
51
+ }
52
+ catch {
53
+ /* no stale reaper */
54
+ }
55
+ reaper = (0, node_fs_1.openSync)(reapPath, 'wx', 0o600);
56
+ (0, node_fs_1.writeSync)(reaper, token);
57
+ const owner = (0, node_fs_1.readFileSync)(lock, 'utf8');
58
+ const age = Date.now() - (0, node_fs_1.statSync)(lock).mtimeMs;
59
+ // PID reuse cannot block forever. State mutations are synchronous; the separate
60
+ // delivery lock bounds its HTTP request well below this hard ceiling.
61
+ if (age > exports.LOCK_HARD_STALE_MS ||
62
+ (age > exports.LOCK_STALE_MS && !alive(Number(owner.split(':')[0]))))
63
+ (0, node_fs_1.unlinkSync)(lock);
64
+ }
65
+ catch {
66
+ /* owner released, or another waiter is reclaiming it */
67
+ }
68
+ finally {
69
+ if (reaper !== undefined) {
70
+ (0, node_fs_1.closeSync)(reaper);
71
+ try {
72
+ if ((0, node_fs_1.readFileSync)(`${lock}.reap`, 'utf8') === token)
73
+ (0, node_fs_1.unlinkSync)(`${lock}.reap`);
74
+ }
75
+ catch {
76
+ /* stale reaper already removed */
77
+ }
78
+ }
79
+ }
80
+ if (Date.now() >= deadline)
81
+ throw new Error('Session state lock timed out');
82
+ await new Promise((resolve) => setTimeout(resolve, 10 + Math.random() * 15));
83
+ }
84
+ }
85
+ try {
86
+ return await fn();
87
+ }
88
+ finally {
89
+ // Never unlink a replacement lock owned by another writer.
90
+ try {
91
+ if ((0, node_fs_1.readFileSync)(lock, 'utf8') === token)
92
+ (0, node_fs_1.unlinkSync)(lock);
93
+ }
94
+ catch {
95
+ /* already gone */
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isEphemeralCliPath = isEphemeralCliPath;
4
+ exports.ensureStableCliPath = ensureStableCliPath;
5
+ exports.pruneOldRuntimes = pruneOldRuntimes;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_module_1 = require("node:module");
9
+ const node_path_1 = require("node:path");
10
+ const user_config_js_1 = require("../../core/user-config.js");
11
+ /**
12
+ * Hooks must point at a cli.js that will still exist tomorrow. When setup runs
13
+ * from an ephemeral location — `npx @nocville/npc …` executes out of the npm
14
+ * cache (`~/.npm/_npx/<hash>/…`), which npm may prune at any time — we copy
15
+ * the package (dist + its `@nocville/node` dependency) into
16
+ * `~/.config/nocville/runtime/<version>/` and point hooks there instead.
17
+ */
18
+ /** True when the CLI is running from npm's npx cache (or another temp dir). */
19
+ function isEphemeralCliPath(cliPath) {
20
+ const p = cliPath.split(node_path_1.sep).join('/');
21
+ return p.includes('/_npx/') || p.includes('/.pnpm-store/') || p.includes('/dlx-');
22
+ }
23
+ function findPackageRoot(start, name) {
24
+ let dir = start;
25
+ for (let i = 0; i < 8; i++) {
26
+ const pkg = (0, node_path_1.join)(dir, 'package.json');
27
+ if ((0, node_fs_1.existsSync)(pkg)) {
28
+ try {
29
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(pkg, 'utf-8'));
30
+ if (parsed.name === name)
31
+ return dir;
32
+ }
33
+ catch {
34
+ // keep walking
35
+ }
36
+ }
37
+ const parent = (0, node_path_1.dirname)(dir);
38
+ if (parent === dir)
39
+ break;
40
+ dir = parent;
41
+ }
42
+ return null;
43
+ }
44
+ function readVersion(root) {
45
+ try {
46
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'package.json'), 'utf-8'));
47
+ return parsed.version ?? '0.0.0';
48
+ }
49
+ catch {
50
+ return '0.0.0';
51
+ }
52
+ }
53
+ /**
54
+ * Always copy a valid installed runtime, including global installs removed by nvm uninstall.
55
+ */
56
+ function ensureStableCliPath(cliPath, _force = true) {
57
+ const pkgRoot = findPackageRoot((0, node_path_1.dirname)(cliPath), '@nocville/npc');
58
+ if (!pkgRoot)
59
+ return cliPath;
60
+ let sdkRoot = null;
61
+ try {
62
+ const req = (0, node_module_1.createRequire)(cliPath);
63
+ sdkRoot = findPackageRoot((0, node_path_1.dirname)(req.resolve('@nocville/node')), '@nocville/node');
64
+ }
65
+ catch {
66
+ sdkRoot = null;
67
+ }
68
+ if (!sdkRoot)
69
+ return cliPath;
70
+ const req = (0, node_module_1.createRequire)(cliPath);
71
+ const pngRoot = findPackageRoot((0, node_path_1.dirname)(req.resolve('pngjs')), 'pngjs');
72
+ if (!pngRoot)
73
+ throw new Error('Cannot install hook runtime: pngjs is missing');
74
+ const version = readVersion(pkgRoot);
75
+ const hash = (0, node_crypto_1.createHash)('sha256');
76
+ const hashTree = (root) => {
77
+ for (const entry of (0, node_fs_1.readdirSync)(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
78
+ hash.update(entry.name);
79
+ const path = (0, node_path_1.join)(root, entry.name);
80
+ if (entry.isDirectory())
81
+ hashTree(path);
82
+ else if (entry.isFile())
83
+ hash.update((0, node_fs_1.readFileSync)(path));
84
+ }
85
+ };
86
+ for (const root of [pkgRoot, sdkRoot]) {
87
+ hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'package.json')));
88
+ hashTree((0, node_path_1.join)(root, 'dist'));
89
+ }
90
+ hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(pngRoot, 'package.json')));
91
+ hashTree((0, node_path_1.join)(pngRoot, 'lib'));
92
+ // Immutable snapshots also support multiple development builds of one version.
93
+ // Never remove files a running watchdog or the current pointer may still use.
94
+ const dest = (0, node_path_1.join)((0, node_path_1.dirname)((0, user_config_js_1.userConfigPath)()), 'runtime', `${version}-${hash.digest('hex').slice(0, 16)}`);
95
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dest, 'dist', 'cli.js')))
96
+ return (0, node_path_1.join)(dest, 'dist', 'cli.js');
97
+ const stage = `${dest}.${(0, node_crypto_1.randomUUID)()}.tmp`;
98
+ try {
99
+ (0, node_fs_1.mkdirSync)(stage, { recursive: true, mode: 0o700 });
100
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(pkgRoot, 'package.json'), (0, node_path_1.join)(stage, 'package.json'));
101
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(pkgRoot, 'dist'), (0, node_path_1.join)(stage, 'dist'), { recursive: true });
102
+ const sdkDest = (0, node_path_1.join)(stage, 'node_modules', '@nocville', 'node');
103
+ (0, node_fs_1.mkdirSync)(sdkDest, { recursive: true });
104
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(sdkRoot, 'package.json'), (0, node_path_1.join)(sdkDest, 'package.json'));
105
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(sdkRoot, 'dist'), (0, node_path_1.join)(sdkDest, 'dist'), { recursive: true });
106
+ const pngDest = (0, node_path_1.join)(stage, 'node_modules', 'pngjs');
107
+ (0, node_fs_1.mkdirSync)(pngDest, { recursive: true });
108
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(pngRoot, 'package.json'), (0, node_path_1.join)(pngDest, 'package.json'));
109
+ (0, node_fs_1.cpSync)((0, node_path_1.join)(pngRoot, 'lib'), (0, node_path_1.join)(pngDest, 'lib'), { recursive: true });
110
+ try {
111
+ (0, node_fs_1.renameSync)(stage, dest);
112
+ }
113
+ catch (error) {
114
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dest, 'dist', 'cli.js')))
115
+ throw error;
116
+ }
117
+ return (0, node_path_1.join)(dest, 'dist', 'cli.js');
118
+ }
119
+ finally {
120
+ (0, node_fs_1.rmSync)(stage, { recursive: true, force: true });
121
+ }
122
+ }
123
+ /** Prune only old snapshots, never the active pointer target or unknown directories. */
124
+ function pruneOldRuntimes(runtimeDir, now = Date.now()) {
125
+ let active;
126
+ try {
127
+ const pointer = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(runtimeDir, 'current.json'), 'utf8'));
128
+ if (typeof pointer.cli !== 'string')
129
+ return 0;
130
+ active = (0, node_path_1.resolve)(pointer.cli);
131
+ }
132
+ catch {
133
+ return 0;
134
+ }
135
+ let removed = 0;
136
+ for (const entry of (0, node_fs_1.readdirSync)(runtimeDir, { withFileTypes: true })) {
137
+ if (!entry.isDirectory() || !/^\d[^/]*-[a-f0-9]{16}$/.test(entry.name))
138
+ continue;
139
+ const path = (0, node_path_1.resolve)(runtimeDir, entry.name);
140
+ if (active === path || active.startsWith(path + node_path_1.sep))
141
+ continue;
142
+ try {
143
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs <= 7 * 86_400_000)
144
+ continue;
145
+ (0, node_fs_1.rmSync)(path, { recursive: true, force: true });
146
+ removed++;
147
+ }
148
+ catch {
149
+ /* best-effort maintenance must not break setup */
150
+ }
151
+ }
152
+ return removed;
153
+ }
154
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,390 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_MIN_PUSH_GAP_MS = exports.ACTIVITY_HISTORY = void 0;
4
+ exports.phasePresentation = phasePresentation;
5
+ exports.projectNameOf = projectNameOf;
6
+ exports.agentDisplayName = agentDisplayName;
7
+ exports.sanitizeServiceId = sanitizeServiceId;
8
+ exports.stateFilePath = stateFilePath;
9
+ exports.readSessionState = readSessionState;
10
+ exports.writeSessionState = writeSessionState;
11
+ exports.deleteSessionState = deleteSessionState;
12
+ exports.cleanupSessionStates = cleanupSessionStates;
13
+ exports.maybeCleanupSessionStates = maybeCleanupSessionStates;
14
+ exports.pushHistory = pushHistory;
15
+ exports.applyTransition = applyTransition;
16
+ exports.buildAgentPayload = buildAgentPayload;
17
+ exports.flushSession = flushSession;
18
+ exports.tickerLines = tickerLines;
19
+ exports.applySubagentEvent = applySubagentEvent;
20
+ exports.spawnDetachedFlush = spawnDetachedFlush;
21
+ exports.spawnDetachedAgent = spawnDetachedAgent;
22
+ exports.sleep = sleep;
23
+ const node_child_process_1 = require("node:child_process");
24
+ const node_fs_1 = require("node:fs");
25
+ const node_path_1 = require("node:path");
26
+ const node_1 = require("@nocville/node");
27
+ const text_js_1 = require("../../core/text.js");
28
+ const lock_js_1 = require("./lock.js");
29
+ /** Behavior hints + subStatus for a phase (shared by every agent NPC). */
30
+ function phasePresentation(state, offline) {
31
+ if (offline)
32
+ return { subStatus: 'offline' };
33
+ switch (state) {
34
+ case 'WORKING':
35
+ // Head to a desk and sit while actively working.
36
+ return { subStatus: 'working', behavior: { activity: 'busy', busy: 'seek-desk' } };
37
+ case 'WAITING':
38
+ // Freeze and raise a hand until the human responds.
39
+ return {
40
+ subStatus: 'waiting',
41
+ behavior: { activity: 'waiting', waitingEmote: 'raised-hand' },
42
+ };
43
+ case 'IDLE':
44
+ default:
45
+ // Settle at the desk and doze off.
46
+ return {
47
+ subStatus: 'idle',
48
+ behavior: { activity: 'idle', idle: 'stay', idlePose: 'sleep' },
49
+ };
50
+ }
51
+ }
52
+ exports.ACTIVITY_HISTORY = 8;
53
+ exports.DEFAULT_MIN_PUSH_GAP_MS = 1500;
54
+ const STATE_PREFIX = 'session-';
55
+ // ---------------------------------------------------------------- naming
56
+ function projectNameOf(cwd) {
57
+ const parts = cwd.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
58
+ return parts[parts.length - 1] || 'unknown';
59
+ }
60
+ /** "Claude · nocville", clamped to the API's 32-char banner limit. */
61
+ function agentDisplayName(label, subject) {
62
+ return (0, text_js_1.truncate)(`${label} · ${subject}`, 32);
63
+ }
64
+ /** Service ids are ≤100 chars of [A-Za-z0-9_.-]. */
65
+ function sanitizeServiceId(raw) {
66
+ return raw
67
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
68
+ .replace(/-+/g, '-')
69
+ .slice(0, 100);
70
+ }
71
+ // ---------------------------------------------------------------- state file
72
+ function stateFilePath(stateDir, sessionId) {
73
+ const safe = sessionId.replace(/[^A-Za-z0-9_.-]/g, '_');
74
+ return (0, node_path_1.join)(stateDir, `${STATE_PREFIX}${safe}.json`);
75
+ }
76
+ function readSessionState(stateDir, sessionId) {
77
+ try {
78
+ const raw = JSON.parse((0, node_fs_1.readFileSync)(stateFilePath(stateDir, sessionId), 'utf-8'));
79
+ if (typeof raw['sessionId'] !== 'string')
80
+ return null;
81
+ return raw;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Atomic write (temp file + rename) so concurrent readers never see half a file. */
88
+ function writeSessionState(stateDir, state) {
89
+ (0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
90
+ const path = stateFilePath(stateDir, state.sessionId);
91
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
92
+ (0, node_fs_1.writeFileSync)(tmp, JSON.stringify(state), 'utf-8');
93
+ (0, node_fs_1.renameSync)(tmp, path);
94
+ }
95
+ function deleteSessionState(stateDir, sessionId) {
96
+ const path = stateFilePath(stateDir, sessionId);
97
+ if ((0, node_fs_1.existsSync)(path)) {
98
+ try {
99
+ (0, node_fs_1.rmSync)(path);
100
+ }
101
+ catch {
102
+ // best effort
103
+ }
104
+ }
105
+ }
106
+ /** Remove state files older than `maxAgeMs`. Returns the number removed. */
107
+ async function cleanupSessionStates(stateDir, maxAgeMs, now = Date.now()) {
108
+ if (!(0, node_fs_1.existsSync)(stateDir))
109
+ return 0;
110
+ let removed = 0;
111
+ for (const file of (0, node_fs_1.readdirSync)(stateDir)) {
112
+ if (!file.startsWith(STATE_PREFIX) || !file.endsWith('.json'))
113
+ continue;
114
+ const path = (0, node_path_1.join)(stateDir, file);
115
+ try {
116
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs <= maxAgeMs)
117
+ continue;
118
+ // Recheck under the same lock as hooks, so a SessionStart cannot be deleted mid-write.
119
+ await (0, lock_js_1.withStateLock)(path, () => {
120
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs > maxAgeMs) {
121
+ (0, node_fs_1.unlinkSync)(path);
122
+ removed++;
123
+ }
124
+ }, 0);
125
+ }
126
+ catch {
127
+ /* active writer, missing file or filesystem error: leave it alone */
128
+ }
129
+ }
130
+ return removed;
131
+ }
132
+ /** Cross-process hourly maintenance, invoked only on SessionStart. */
133
+ async function maybeCleanupSessionStates(stateDir, now = Date.now()) {
134
+ const marker = (0, node_path_1.join)(stateDir, '.last-cleanup');
135
+ try {
136
+ return await (0, lock_js_1.withStateLock)(marker, async () => {
137
+ let last = NaN;
138
+ try {
139
+ last = Number((0, node_fs_1.readFileSync)(marker, 'utf8'));
140
+ }
141
+ catch {
142
+ /* first cleanup */
143
+ }
144
+ if (Number.isFinite(last) && now - last < 3_600_000)
145
+ return 0;
146
+ const removed = await cleanupSessionStates(stateDir, 86_400_000, now);
147
+ (0, node_fs_1.writeFileSync)(marker, String(now), { mode: 0o600 });
148
+ return removed;
149
+ }, 0);
150
+ }
151
+ catch {
152
+ return 0;
153
+ }
154
+ }
155
+ /** Prepend `line` to the history (deduping an identical head), capped. */
156
+ function pushHistory(prev, line) {
157
+ const list = [...(prev ?? [])];
158
+ if (line && list[0] !== line)
159
+ list.unshift(line);
160
+ return list.slice(0, exports.ACTIVITY_HISTORY);
161
+ }
162
+ /**
163
+ * Apply a transition to the previous state and decide when to push.
164
+ * Pure — exposed for testing.
165
+ */
166
+ function applyTransition(prev, input, minGapMs = exports.DEFAULT_MIN_PUSH_GAP_MS) {
167
+ const t = input.transition;
168
+ const history = pushHistory(prev?.activity, t.activity);
169
+ const prevHead = prev?.activity?.[0];
170
+ const subagents = pruneSubagents(prev?.subagents, input.now);
171
+ const changed = !prev ||
172
+ prev.state !== t.state ||
173
+ prev.status !== t.status ||
174
+ Object.keys(subagents).length !== Object.keys(prev.subagents ?? {}).length ||
175
+ history[0] !== prevHead ||
176
+ !!t.offline !== !!prev.offline ||
177
+ (input.name !== undefined && input.name !== prev.name);
178
+ const rev = (prev?.rev ?? 0) + (changed ? 1 : 0);
179
+ const next = {
180
+ ...prev,
181
+ sessionId: input.sessionId,
182
+ startedAt: prev?.startedAt ?? input.now,
183
+ serviceId: prev?.serviceId ?? input.serviceId,
184
+ state: t.state,
185
+ projectDir: input.projectDir,
186
+ lastEvent: input.eventName,
187
+ lastUpdate: input.now,
188
+ activity: history,
189
+ subagents,
190
+ name: input.name ?? prev?.name,
191
+ status: t.status,
192
+ mood: t.mood,
193
+ offline: t.offline || undefined,
194
+ rev,
195
+ pushedRev: prev?.pushedRev ?? 0,
196
+ lastPushAt: prev?.lastPushAt ?? 0,
197
+ flushAt: prev?.flushAt,
198
+ };
199
+ if (t.offline)
200
+ return { next, decision: { kind: 'now' } };
201
+ if (t.quiet || !changed)
202
+ return { next, decision: { kind: 'none' } };
203
+ const phaseChanged = !prev || prev.state !== t.state || !prev.lastPushAt;
204
+ const sinceLast = input.now - (next.lastPushAt ?? 0);
205
+ if (phaseChanged || sinceLast >= minGapMs) {
206
+ // Stamp the push time now: the push itself runs asynchronously (detached),
207
+ // and the next hook must already see it to coalesce.
208
+ next.lastPushAt = input.now;
209
+ return { next, decision: { kind: 'now' } };
210
+ }
211
+ // Coalesce: a flush is already pending (and not stale) → it will pick up this rev.
212
+ if (next.flushAt && next.flushAt > input.now - 2000)
213
+ return { next, decision: { kind: 'none' } };
214
+ const delayMs = Math.max(50, minGapMs - sinceLast);
215
+ next.flushAt = input.now + delayMs;
216
+ return { next, decision: { kind: 'defer', delayMs } };
217
+ }
218
+ // ---------------------------------------------------------------- push
219
+ /** Build the status push for a session (offline sentinel when the session ended). */
220
+ function buildAgentPayload(adapter, state, config) {
221
+ const subject = state.name ?? projectNameOf(state.projectDir);
222
+ const name = config.name ? (0, text_js_1.truncate)(config.name, 32) : agentDisplayName(adapter.label, subject);
223
+ const current = state.activity?.[0] ?? '';
224
+ const { subStatus, behavior } = phasePresentation(state.state, state.offline);
225
+ if (state.offline) {
226
+ return {
227
+ status: 'unknown',
228
+ mood: 'idle',
229
+ name,
230
+ message: 'Session ended',
231
+ subStatus,
232
+ offline: true,
233
+ map: config.map,
234
+ };
235
+ }
236
+ const screenData = {
237
+ layout: 'ticker',
238
+ values: tickerLines({ ...state, subagents: pruneSubagents(state.subagents, Date.now()) }).map((a) => ({ label: '', value: a })),
239
+ };
240
+ return {
241
+ status: state.status ?? 'healthy',
242
+ mood: state.mood ?? 'happy',
243
+ name,
244
+ message: current ? (0, text_js_1.truncate)(current, 120) : undefined,
245
+ archetype: 'ai-agent',
246
+ metrics: { sessionStartedAt: state.startedAt ?? state.lastUpdate },
247
+ appearance: { family: adapter.family },
248
+ agentStats: state.agentStats,
249
+ subStatus,
250
+ behavior,
251
+ alertConfig: { heartbeatInterval: state.watchdogPid ? adapter.heartbeatSeconds : 3600 },
252
+ screenData,
253
+ companion: {
254
+ screenType: 'desk-sign',
255
+ size: { w: 3, h: 1 },
256
+ placement: 'on-desk',
257
+ label: (0, text_js_1.truncate)(projectNameOf(state.projectDir), 40),
258
+ },
259
+ map: config.map,
260
+ };
261
+ }
262
+ async function defaultPush(config, serviceId, payload, timeoutMs) {
263
+ const client = new node_1.NocvilleClient({
264
+ apiUrl: config.apiUrl,
265
+ apiKey: config.apiKey,
266
+ serviceId,
267
+ timeout: timeoutMs,
268
+ retries: 0,
269
+ debug: config.debug,
270
+ });
271
+ return client.pushStatus(payload);
272
+ }
273
+ /**
274
+ * Push the session's current state if it has unpushed changes (or is offline),
275
+ * then record the push. Re-reads the state right before writing so a hook that
276
+ * ran concurrently isn't clobbered. Never throws.
277
+ */
278
+ async function flushSession(adapter, config, sessionId, deps = {}) {
279
+ const path = stateFilePath(adapter.stateDir, sessionId);
280
+ try {
281
+ // Serialize HTTP delivery separately: hooks can update state throughout a request,
282
+ // but an older delivery can never arrive after a newer/offline one.
283
+ return await (0, lock_js_1.withStateLock)(`${path}.push`, async () => {
284
+ const state = readSessionState(adapter.stateDir, sessionId);
285
+ if (!state || !config.apiKey || state.offlineDeliveryStopped)
286
+ return false;
287
+ const rev = state.rev ?? 0;
288
+ if (!deps.force && rev <= (state.pushedRev ?? 0)) {
289
+ await (0, lock_js_1.withStateLock)(path, () => {
290
+ const fresh = readSessionState(adapter.stateDir, sessionId);
291
+ if (fresh && (fresh.rev ?? 0) === rev) {
292
+ fresh.flushAt = undefined;
293
+ writeSessionState(adapter.stateDir, fresh);
294
+ }
295
+ }, deps.lockWaitMs);
296
+ return false;
297
+ }
298
+ const push = deps.push ??
299
+ ((sid, payload) => defaultPush(config, sid, payload, deps.timeoutMs ?? 4000));
300
+ let ok = false;
301
+ try {
302
+ const result = await push(state.serviceId, buildAgentPayload(adapter, state, config));
303
+ ok = result.success;
304
+ deps.onResult?.(result);
305
+ }
306
+ catch {
307
+ /* retry on next hook/watchdog tick */
308
+ }
309
+ await (0, lock_js_1.withStateLock)(path, () => {
310
+ const fresh = readSessionState(adapter.stateDir, sessionId);
311
+ if (!fresh)
312
+ return;
313
+ // Retain terminal state until cleanup so late hooks/watchers cannot revive it.
314
+ if (ok) {
315
+ fresh.pushedRev = Math.max(fresh.pushedRev ?? 0, rev);
316
+ fresh.lastPushAt = Date.now();
317
+ }
318
+ if (!fresh.flushAt || fresh.flushAt <= Date.now())
319
+ fresh.flushAt = undefined;
320
+ writeSessionState(adapter.stateDir, fresh);
321
+ }, deps.lockWaitMs);
322
+ return ok;
323
+ }, deps.lockWaitMs ?? 6000);
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ }
329
+ /** Current parent activity, up to three subagents, then recent parent history. */
330
+ function tickerLines(state) {
331
+ const history = state.activity ?? [];
332
+ const agents = Object.values(state.subagents ?? {})
333
+ .sort((a, b) => b.at - a.at)
334
+ .slice(0, 3)
335
+ .map((entry) => `↳ ${entry.type}: ${entry.activity}`);
336
+ return [...history.slice(0, 1), ...agents, ...history.slice(1)].slice(0, exports.ACTIVITY_HISTORY);
337
+ }
338
+ function applySubagentEvent(prev, agentId, type, event, activity, now) {
339
+ const subagents = pruneSubagents(prev.subagents, now);
340
+ if (event === 'Stop' || event === 'SubagentStop')
341
+ delete subagents[agentId];
342
+ else if (event === 'PreToolUse')
343
+ subagents[agentId] = { type, activity, at: now };
344
+ const next = { ...prev, subagents, lastUpdate: now, lastEvent: event };
345
+ if (JSON.stringify(tickerLines(prev)) !== JSON.stringify(tickerLines(next)))
346
+ next.rev = (prev.rev ?? 0) + 1;
347
+ return next;
348
+ }
349
+ /**
350
+ * Hand a flush to a detached background process so the hook can exit at once.
351
+ * The child inherits the resolved config via env (not argv, so the key never
352
+ * shows up in `ps`).
353
+ */
354
+ function spawnDetachedFlush(adapter, config, sessionId, delayMs, cliPath = process.argv[1], stats = false) {
355
+ spawnDetachedAgent(adapter, config, ['flush', sessionId, `--delay=${Math.round(delayMs)}`, ...(stats ? ['--stats'] : [])], cliPath);
356
+ }
357
+ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
358
+ if (!cliPath)
359
+ return undefined;
360
+ const env = { ...process.env };
361
+ env['NOCVILLE_API_URL'] = config.apiUrl;
362
+ env['NOCVILLE_API_KEY'] = config.apiKey;
363
+ if (config.map)
364
+ env['NOCVILLE_MAP'] = config.map;
365
+ if (config.name)
366
+ env['NOCVILLE_NAME'] = config.name;
367
+ try {
368
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, adapter.command, ...args], {
369
+ detached: true,
370
+ stdio: 'ignore',
371
+ env,
372
+ windowsHide: true,
373
+ });
374
+ child.on('error', () => {
375
+ /* swallow — never fail the agent */
376
+ });
377
+ child.unref();
378
+ return child.pid;
379
+ }
380
+ catch {
381
+ // swallow
382
+ }
383
+ }
384
+ function sleep(ms) {
385
+ return new Promise((resolve) => setTimeout(resolve, ms));
386
+ }
387
+ function pruneSubagents(subagents, now) {
388
+ return Object.fromEntries(Object.entries(subagents ?? {}).filter(([, value]) => now - value.at < 600_000));
389
+ }
390
+ //# sourceMappingURL=session.js.map