@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,412 @@
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 session_filter_js_1 = require("../../core/session-filter.js");
28
+ const text_js_1 = require("../../core/text.js");
29
+ const lock_js_1 = require("./lock.js");
30
+ /** Behavior hints + subStatus for a phase (shared by every agent NPC). */
31
+ function phasePresentation(state, offline) {
32
+ if (offline)
33
+ return { subStatus: 'offline' };
34
+ switch (state) {
35
+ case 'WORKING':
36
+ // Head to a desk and sit while actively working.
37
+ return { subStatus: 'working', behavior: { activity: 'busy', busy: 'seek-desk' } };
38
+ case 'WAITING':
39
+ // Freeze and raise a hand until the human responds.
40
+ return {
41
+ subStatus: 'waiting',
42
+ behavior: { activity: 'waiting', waitingEmote: 'raised-hand' },
43
+ };
44
+ case 'IDLE':
45
+ default:
46
+ // Settle at the desk and doze off.
47
+ return {
48
+ subStatus: 'idle',
49
+ behavior: { activity: 'idle', idle: 'stay', idlePose: 'sleep' },
50
+ };
51
+ }
52
+ }
53
+ exports.ACTIVITY_HISTORY = 8;
54
+ exports.DEFAULT_MIN_PUSH_GAP_MS = 1500;
55
+ const STATE_PREFIX = 'session-';
56
+ // ---------------------------------------------------------------- naming
57
+ function projectNameOf(cwd) {
58
+ const parts = cwd.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
59
+ return parts[parts.length - 1] || 'unknown';
60
+ }
61
+ /** "Claude · nocville", clamped to the API's 32-char banner limit. */
62
+ function agentDisplayName(label, subject) {
63
+ return (0, text_js_1.truncate)(`${label} · ${subject}`, 32);
64
+ }
65
+ /** Service ids are ≤100 chars of [A-Za-z0-9_.-]. */
66
+ function sanitizeServiceId(raw) {
67
+ return raw
68
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
69
+ .replace(/-+/g, '-')
70
+ .slice(0, 100);
71
+ }
72
+ // ---------------------------------------------------------------- state file
73
+ function stateFilePath(stateDir, sessionId) {
74
+ const safe = sessionId.replace(/[^A-Za-z0-9_.-]/g, '_');
75
+ return (0, node_path_1.join)(stateDir, `${STATE_PREFIX}${safe}.json`);
76
+ }
77
+ function readSessionState(stateDir, sessionId) {
78
+ try {
79
+ const raw = JSON.parse((0, node_fs_1.readFileSync)(stateFilePath(stateDir, sessionId), 'utf-8'));
80
+ if (typeof raw['sessionId'] !== 'string')
81
+ return null;
82
+ return raw;
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ /** Atomic write (temp file + rename) so concurrent readers never see half a file. */
89
+ function writeSessionState(stateDir, state) {
90
+ (0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
91
+ const path = stateFilePath(stateDir, state.sessionId);
92
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
93
+ (0, node_fs_1.writeFileSync)(tmp, JSON.stringify(state), 'utf-8');
94
+ (0, node_fs_1.renameSync)(tmp, path);
95
+ }
96
+ function deleteSessionState(stateDir, sessionId) {
97
+ const path = stateFilePath(stateDir, sessionId);
98
+ if ((0, node_fs_1.existsSync)(path)) {
99
+ try {
100
+ (0, node_fs_1.rmSync)(path);
101
+ }
102
+ catch {
103
+ // best effort
104
+ }
105
+ }
106
+ }
107
+ /** Remove state files older than `maxAgeMs`. Returns the number removed. */
108
+ async function cleanupSessionStates(stateDir, maxAgeMs, now = Date.now()) {
109
+ if (!(0, node_fs_1.existsSync)(stateDir))
110
+ return 0;
111
+ let removed = 0;
112
+ for (const file of (0, node_fs_1.readdirSync)(stateDir)) {
113
+ if (!file.startsWith(STATE_PREFIX) || !file.endsWith('.json'))
114
+ continue;
115
+ const path = (0, node_path_1.join)(stateDir, file);
116
+ try {
117
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs <= maxAgeMs)
118
+ continue;
119
+ // Recheck under the same lock as hooks, so a SessionStart cannot be deleted mid-write.
120
+ await (0, lock_js_1.withStateLock)(path, () => {
121
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs > maxAgeMs) {
122
+ (0, node_fs_1.unlinkSync)(path);
123
+ removed++;
124
+ }
125
+ }, 0);
126
+ }
127
+ catch {
128
+ /* active writer, missing file or filesystem error: leave it alone */
129
+ }
130
+ }
131
+ return removed;
132
+ }
133
+ /** Cross-process hourly maintenance, invoked only on SessionStart. */
134
+ async function maybeCleanupSessionStates(stateDir, now = Date.now()) {
135
+ const marker = (0, node_path_1.join)(stateDir, '.last-cleanup');
136
+ try {
137
+ return await (0, lock_js_1.withStateLock)(marker, async () => {
138
+ let last = NaN;
139
+ try {
140
+ last = Number((0, node_fs_1.readFileSync)(marker, 'utf8'));
141
+ }
142
+ catch {
143
+ /* first cleanup */
144
+ }
145
+ if (Number.isFinite(last) && now - last < 3_600_000)
146
+ return 0;
147
+ const removed = await cleanupSessionStates(stateDir, 86_400_000, now);
148
+ (0, node_fs_1.writeFileSync)(marker, String(now), { mode: 0o600 });
149
+ return removed;
150
+ }, 0);
151
+ }
152
+ catch {
153
+ return 0;
154
+ }
155
+ }
156
+ /** Prepend `line` to the history (deduping an identical head), capped. */
157
+ function pushHistory(prev, line) {
158
+ const list = [...(prev ?? [])];
159
+ if (line && list[0] !== line)
160
+ list.unshift(line);
161
+ return list.slice(0, exports.ACTIVITY_HISTORY);
162
+ }
163
+ /**
164
+ * Apply a transition to the previous state and decide when to push.
165
+ * Pure — exposed for testing.
166
+ */
167
+ function applyTransition(prev, input, minGapMs = exports.DEFAULT_MIN_PUSH_GAP_MS) {
168
+ const t = input.transition;
169
+ const history = pushHistory(prev?.activity, t.activity);
170
+ const prevHead = prev?.activity?.[0];
171
+ const subagents = pruneSubagents(prev?.subagents, input.now);
172
+ const changed = !prev ||
173
+ prev.state !== t.state ||
174
+ prev.status !== t.status ||
175
+ Object.keys(subagents).length !== Object.keys(prev.subagents ?? {}).length ||
176
+ history[0] !== prevHead ||
177
+ !!t.offline !== !!prev.offline ||
178
+ (input.name !== undefined && input.name !== prev.name);
179
+ const rev = (prev?.rev ?? 0) + (changed ? 1 : 0);
180
+ const next = {
181
+ ...prev,
182
+ sessionId: input.sessionId,
183
+ startedAt: prev?.startedAt ?? input.now,
184
+ serviceId: prev?.serviceId ?? input.serviceId,
185
+ state: t.state,
186
+ projectDir: input.projectDir,
187
+ lastEvent: input.eventName,
188
+ lastUpdate: input.now,
189
+ activity: history,
190
+ subagents,
191
+ name: input.name ?? prev?.name,
192
+ status: t.status,
193
+ mood: t.mood,
194
+ offline: t.offline || undefined,
195
+ rev,
196
+ pushedRev: prev?.pushedRev ?? 0,
197
+ lastPushAt: prev?.lastPushAt ?? 0,
198
+ flushAt: prev?.flushAt,
199
+ };
200
+ if (t.offline)
201
+ return { next, decision: { kind: 'now' } };
202
+ if (t.quiet || !changed)
203
+ return { next, decision: { kind: 'none' } };
204
+ const phaseChanged = !prev || prev.state !== t.state || !prev.lastPushAt;
205
+ const sinceLast = input.now - (next.lastPushAt ?? 0);
206
+ if (phaseChanged || sinceLast >= minGapMs) {
207
+ // Stamp the push time now: the push itself runs asynchronously (detached),
208
+ // and the next hook must already see it to coalesce.
209
+ next.lastPushAt = input.now;
210
+ return { next, decision: { kind: 'now' } };
211
+ }
212
+ // Coalesce: a flush is already pending (and not stale) → it will pick up this rev.
213
+ if (next.flushAt && next.flushAt > input.now - 2000)
214
+ return { next, decision: { kind: 'none' } };
215
+ const delayMs = Math.max(50, minGapMs - sinceLast);
216
+ next.flushAt = input.now + delayMs;
217
+ return { next, decision: { kind: 'defer', delayMs } };
218
+ }
219
+ // ---------------------------------------------------------------- push
220
+ /** Build the status push for a session (offline sentinel when the session ended). */
221
+ function buildAgentPayload(adapter, state, config) {
222
+ const subject = state.name ?? projectNameOf(state.projectDir);
223
+ const name = config.name ? (0, text_js_1.truncate)(config.name, 32) : agentDisplayName(adapter.label, subject);
224
+ const current = state.activity?.[0] ?? '';
225
+ const { subStatus, behavior } = phasePresentation(state.state, state.offline);
226
+ if (state.offline) {
227
+ return {
228
+ status: 'unknown',
229
+ mood: 'idle',
230
+ name,
231
+ message: 'Session ended',
232
+ subStatus,
233
+ offline: true,
234
+ map: config.map,
235
+ };
236
+ }
237
+ const screenData = {
238
+ layout: 'ticker',
239
+ values: tickerLines({ ...state, subagents: pruneSubagents(state.subagents, Date.now()) }).map((a) => ({ label: '', value: a })),
240
+ };
241
+ return {
242
+ status: state.status ?? 'healthy',
243
+ mood: state.mood ?? 'happy',
244
+ name,
245
+ message: current ? (0, text_js_1.truncate)(current, 120) : undefined,
246
+ archetype: 'ai-agent',
247
+ metrics: { sessionStartedAt: state.startedAt ?? state.lastUpdate },
248
+ appearance: { family: adapter.family },
249
+ agentStats: state.agentStats,
250
+ subStatus,
251
+ behavior,
252
+ alertConfig: { heartbeatInterval: state.watchdogPid ? adapter.heartbeatSeconds : 3600 },
253
+ screenData,
254
+ companion: {
255
+ screenType: 'desk-sign',
256
+ size: { w: 3, h: 1 },
257
+ placement: 'on-desk',
258
+ label: (0, text_js_1.truncate)(projectNameOf(state.projectDir), 40),
259
+ },
260
+ map: config.map,
261
+ };
262
+ }
263
+ async function defaultPush(config, serviceId, payload, timeoutMs) {
264
+ const client = new node_1.NocvilleClient({
265
+ apiUrl: config.apiUrl,
266
+ apiKey: config.apiKey,
267
+ serviceId,
268
+ timeout: timeoutMs,
269
+ retries: 0,
270
+ debug: config.debug,
271
+ });
272
+ return client.pushStatus(payload);
273
+ }
274
+ /**
275
+ * Push the session's current state if it has unpushed changes (or is offline),
276
+ * then record the push. Re-reads the state right before writing so a hook that
277
+ * ran concurrently isn't clobbered. Never throws.
278
+ */
279
+ async function flushSession(adapter, config, sessionId, deps = {}) {
280
+ const path = stateFilePath(adapter.stateDir, sessionId);
281
+ try {
282
+ // Serialize HTTP delivery separately: hooks can update state throughout a request,
283
+ // but an older delivery can never arrive after a newer/offline one.
284
+ return await (0, lock_js_1.withStateLock)(`${path}.push`, async () => {
285
+ const state = readSessionState(adapter.stateDir, sessionId);
286
+ if (!state || !config.apiKey || state.offlineDeliveryStopped)
287
+ return false;
288
+ const rev = state.rev ?? 0;
289
+ const verdict = (0, session_filter_js_1.sessionPublishDecision)(deps.sessionFilter ?? (0, session_filter_js_1.loadSessionFilter)(), {
290
+ projectDir: state.projectDir,
291
+ name: state.name,
292
+ });
293
+ // Filtered out (sessions.json). Never shown: consume the revision so nothing retries.
294
+ // Already shown (e.g. renamed into the denylist): despawn it once, then go quiet.
295
+ // Checked on every push, so later edits to the file take effect without reinstalling.
296
+ if (!verdict.publish && !state.published) {
297
+ await (0, lock_js_1.withStateLock)(path, () => {
298
+ const fresh = readSessionState(adapter.stateDir, sessionId);
299
+ if (!fresh)
300
+ return;
301
+ fresh.pushedRev = Math.max(fresh.pushedRev ?? 0, fresh.rev ?? 0);
302
+ fresh.flushAt = undefined;
303
+ writeSessionState(adapter.stateDir, fresh);
304
+ }, deps.lockWaitMs);
305
+ return true;
306
+ }
307
+ const despawnFiltered = !verdict.publish;
308
+ if (!deps.force && rev <= (state.pushedRev ?? 0)) {
309
+ await (0, lock_js_1.withStateLock)(path, () => {
310
+ const fresh = readSessionState(adapter.stateDir, sessionId);
311
+ if (fresh && (fresh.rev ?? 0) === rev) {
312
+ fresh.flushAt = undefined;
313
+ writeSessionState(adapter.stateDir, fresh);
314
+ }
315
+ }, deps.lockWaitMs);
316
+ return false;
317
+ }
318
+ const push = deps.push ??
319
+ ((sid, payload) => defaultPush(config, sid, payload, deps.timeoutMs ?? 4000));
320
+ let ok = false;
321
+ try {
322
+ const shown = despawnFiltered ? { ...state, offline: true } : state;
323
+ const result = await push(state.serviceId, buildAgentPayload(adapter, shown, config));
324
+ ok = result.success;
325
+ deps.onResult?.(result);
326
+ }
327
+ catch {
328
+ /* retry on next hook/watchdog tick */
329
+ }
330
+ await (0, lock_js_1.withStateLock)(path, () => {
331
+ const fresh = readSessionState(adapter.stateDir, sessionId);
332
+ if (!fresh)
333
+ return;
334
+ // Retain terminal state until cleanup so late hooks/watchers cannot revive it.
335
+ if (ok) {
336
+ fresh.pushedRev = Math.max(fresh.pushedRev ?? 0, despawnFiltered ? (fresh.rev ?? 0) : rev);
337
+ fresh.lastPushAt = Date.now();
338
+ fresh.published = !despawnFiltered && !state.offline;
339
+ }
340
+ if (!fresh.flushAt || fresh.flushAt <= Date.now())
341
+ fresh.flushAt = undefined;
342
+ writeSessionState(adapter.stateDir, fresh);
343
+ }, deps.lockWaitMs);
344
+ return ok;
345
+ }, deps.lockWaitMs ?? 6000);
346
+ }
347
+ catch {
348
+ return false;
349
+ }
350
+ }
351
+ /** Current parent activity, up to three subagents, then recent parent history. */
352
+ function tickerLines(state) {
353
+ const history = state.activity ?? [];
354
+ const agents = Object.values(state.subagents ?? {})
355
+ .sort((a, b) => b.at - a.at)
356
+ .slice(0, 3)
357
+ .map((entry) => `↳ ${entry.type}: ${entry.activity}`);
358
+ return [...history.slice(0, 1), ...agents, ...history.slice(1)].slice(0, exports.ACTIVITY_HISTORY);
359
+ }
360
+ function applySubagentEvent(prev, agentId, type, event, activity, now) {
361
+ const subagents = pruneSubagents(prev.subagents, now);
362
+ if (event === 'Stop' || event === 'SubagentStop')
363
+ delete subagents[agentId];
364
+ else if (event === 'PreToolUse')
365
+ subagents[agentId] = { type, activity, at: now };
366
+ const next = { ...prev, subagents, lastUpdate: now, lastEvent: event };
367
+ if (JSON.stringify(tickerLines(prev)) !== JSON.stringify(tickerLines(next)))
368
+ next.rev = (prev.rev ?? 0) + 1;
369
+ return next;
370
+ }
371
+ /**
372
+ * Hand a flush to a detached background process so the hook can exit at once.
373
+ * The child inherits the resolved config via env (not argv, so the key never
374
+ * shows up in `ps`).
375
+ */
376
+ function spawnDetachedFlush(adapter, config, sessionId, delayMs, cliPath = process.argv[1], stats = false) {
377
+ spawnDetachedAgent(adapter, config, ['flush', sessionId, `--delay=${Math.round(delayMs)}`, ...(stats ? ['--stats'] : [])], cliPath);
378
+ }
379
+ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
380
+ if (!cliPath)
381
+ return undefined;
382
+ const env = { ...process.env };
383
+ env['NOCVILLE_API_URL'] = config.apiUrl;
384
+ env['NOCVILLE_API_KEY'] = config.apiKey;
385
+ if (config.map)
386
+ env['NOCVILLE_MAP'] = config.map;
387
+ if (config.name)
388
+ env['NOCVILLE_NAME'] = config.name;
389
+ try {
390
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, adapter.command, ...args], {
391
+ detached: true,
392
+ stdio: 'ignore',
393
+ env,
394
+ windowsHide: true,
395
+ });
396
+ child.on('error', () => {
397
+ /* swallow — never fail the agent */
398
+ });
399
+ child.unref();
400
+ return child.pid;
401
+ }
402
+ catch {
403
+ // swallow
404
+ }
405
+ }
406
+ function sleep(ms) {
407
+ return new Promise((resolve) => setTimeout(resolve, ms));
408
+ }
409
+ function pruneSubagents(subagents, now) {
410
+ return Object.fromEntries(Object.entries(subagents ?? {}).filter(([, value]) => now - value.at < 600_000));
411
+ }
412
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNTIME_POINTER = exports.SHIM_PATH = void 0;
4
+ exports.installHookShim = installHookShim;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const user_config_js_1 = require("../../core/user-config.js");
8
+ const runtime_js_1 = require("./runtime.js");
9
+ const SHIM_PATH = () => (0, node_path_1.join)((0, node_path_1.dirname)((0, user_config_js_1.userConfigPath)()), 'bin', 'nocville-hook');
10
+ exports.SHIM_PATH = SHIM_PATH;
11
+ const RUNTIME_POINTER = () => (0, node_path_1.join)((0, node_path_1.dirname)((0, user_config_js_1.userConfigPath)()), 'runtime', 'current.json');
12
+ exports.RUNTIME_POINTER = RUNTIME_POINTER;
13
+ function writeChanged(path, content, mode) {
14
+ try {
15
+ if ((0, node_fs_1.readFileSync)(path, 'utf8') === content)
16
+ return;
17
+ }
18
+ catch {
19
+ /* first install */
20
+ }
21
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
22
+ const tmp = `${path}.${process.pid}.tmp`;
23
+ (0, node_fs_1.writeFileSync)(tmp, content, { mode });
24
+ (0, node_fs_1.renameSync)(tmp, path);
25
+ }
26
+ // Resolve relative to the shim itself, including custom XDG/NOCVILLE_CONFIG installs.
27
+ // JSON is parsed by Node, never interpolated into shell code or decoded with sed.
28
+ const SHIM = `#!/bin/sh
29
+ # nocville-npc-hook: stable one-way hook launcher
30
+ DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd) || exit 0
31
+ IFS= read -r NODE < "$DIR/../runtime/node-path" 2>/dev/null || NODE=''
32
+ if [ ! -x "$NODE" ]; then NODE=$(command -v node 2>/dev/null); fi
33
+ for candidate in "$NODE" /opt/homebrew/bin/node /usr/local/bin/node "$HOME/.volta/bin/node"; do
34
+ if [ -x "$candidate" ]; then NODE="$candidate"; break; fi
35
+ done
36
+ if [ ! -x "$NODE" ]; then cat >/dev/null; if { [ "$1" = gemini ] || [ "$1" = cursor ] || [ "$1" = claude-code ]; }; then printf '{}\\n'; fi; exit 0; fi
37
+ exec "$NODE" "$DIR/launch.cjs" "$DIR/../runtime/current.json" "$@"
38
+ `;
39
+ // Without Node, return a neutral empty object for both Claude and imported Cursor.
40
+ // No payload is evaluated by either shell. Native Codex remains silent.
41
+ const WINDOWS_SHIM = String.raw `@echo off
42
+ setlocal DisableDelayedExpansion
43
+ set "NOCVILLE_NODE="
44
+ set /p "NOCVILLE_NODE="<"%~dp0..\runtime\node-path" 2>nul
45
+ if exist "%NOCVILLE_NODE%" goto launch
46
+ set "NOCVILLE_NODE="
47
+ for /f "delims=" %%N in ('where node.exe 2^>nul') do if not defined NOCVILLE_NODE set "NOCVILLE_NODE=%%N"
48
+ if not defined NOCVILLE_NODE goto no_node
49
+ :launch
50
+ "%NOCVILLE_NODE%" "%~dp0launch.cjs" "%~dp0..\runtime\current.json" %*
51
+ exit /b 0
52
+ :no_node
53
+ more >nul
54
+ if "%~1"=="cursor" echo {}
55
+ if "%~1"=="gemini" echo {}
56
+ if "%~1"=="claude-code" echo {}
57
+ exit /b 0
58
+ `;
59
+ const LAUNCHER = `// Stable bootstrap. Hooks never return decisions or emit launcher errors.
60
+ const fs = require('node:fs');
61
+ const { spawnSync } = require('node:child_process');
62
+ const agent = process.argv[3];
63
+ const hook = !process.argv[4] || process.argv[4] === 'hook';
64
+ let neutral = hook && (agent === 'gemini' || agent === 'cursor');
65
+ let input = Buffer.alloc(0);
66
+ try {
67
+ input = fs.readFileSync(0);
68
+ if (hook && agent === 'claude-code') {
69
+ try {
70
+ const event = JSON.parse(input.toString('utf8'));
71
+ const transcript = typeof event?.transcript_path === 'string' ? event.transcript_path.replace(/\\\\/g, '/') : '';
72
+ neutral = typeof event?.cursor_version === 'string' ||
73
+ (typeof event?.conversation_id === 'string' && !/(?:^|\\/)\\.claude(?:\\/|$)/.test(transcript));
74
+ } catch {}
75
+ }
76
+ const pointer = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
77
+ if (typeof pointer.cli !== 'string' || !fs.existsSync(pointer.cli)) throw new Error('missing runtime');
78
+ const node = typeof pointer.node === 'string' && fs.existsSync(pointer.node) ? pointer.node : process.execPath;
79
+ // Buffer stdin once for payload-based imported-Cursor detection, then replay it unchanged.
80
+ // Never forward child stdout, even in debug mode or on an unexpected failure.
81
+ spawnSync(node, [pointer.cli, ...process.argv.slice(3)], { input, stdio: ['pipe', 'ignore', 'inherit'], windowsHide: true });
82
+ } catch {}
83
+ if (neutral) process.stdout.write('{}\\n');
84
+ `;
85
+ function installHookShim(nodePath, cliPath) {
86
+ writeChanged((0, exports.RUNTIME_POINTER)(), JSON.stringify({ node: nodePath, cli: cliPath }, null, 2) + '\n', 0o600);
87
+ writeChanged((0, node_path_1.join)((0, node_path_1.dirname)((0, exports.RUNTIME_POINTER)()), 'node-path'), nodePath + '\n', 0o600);
88
+ writeChanged((0, node_path_1.join)((0, node_path_1.dirname)((0, exports.SHIM_PATH)()), 'launch.cjs'), LAUNCHER, 0o600);
89
+ writeChanged((0, exports.SHIM_PATH)(), SHIM, 0o755);
90
+ (0, node_fs_1.chmodSync)((0, exports.SHIM_PATH)(), 0o755);
91
+ // Both launchers use the same bootstrap, including imported-Cursor detection.
92
+ writeChanged((0, exports.SHIM_PATH)() + '.cmd', WINDOWS_SHIM.replace(/\n/g, '\r\n'), 0o755);
93
+ (0, runtime_js_1.pruneOldRuntimes)((0, node_path_1.dirname)((0, exports.RUNTIME_POINTER)()));
94
+ return (0, exports.SHIM_PATH)();
95
+ }
96
+ //# sourceMappingURL=shim.js.map
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WATCHDOG_OFFLINE_RETRY_MS = exports.WATCHDOG_HEARTBEAT_MS = exports.WATCHDOG_TICK_MS = void 0;
4
+ exports.ensureWatchdog = ensureWatchdog;
5
+ exports.runWatchdog = runWatchdog;
6
+ const liveness_js_1 = require("./liveness.js");
7
+ const lock_js_1 = require("./lock.js");
8
+ const session_js_1 = require("./session.js");
9
+ exports.WATCHDOG_TICK_MS = 15_000;
10
+ exports.WATCHDOG_HEARTBEAT_MS = 60_000;
11
+ exports.WATCHDOG_OFFLINE_RETRY_MS = 15 * 60_000;
12
+ async function ensureWatchdog(adapter, config, sessionId, cliPath, lockWaitMs) {
13
+ if (adapter.processNames?.length === 0 ||
14
+ !config.apiKey ||
15
+ process.env['NOCVILLE_NO_WATCHDOG'] === '1')
16
+ return;
17
+ await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
18
+ const state = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
19
+ // No heartbeat without proven liveness. Keep the long fallback on unsupported hosts.
20
+ if (!state || state.offline || !state.agentPid)
21
+ return;
22
+ if (state.watchdogPid && (0, liveness_js_1.isAgentAlive)({ pid: state.watchdogPid }))
23
+ return;
24
+ const pid = (0, session_js_1.spawnDetachedAgent)(adapter, config, ['watchdog', sessionId], cliPath);
25
+ if (pid) {
26
+ state.watchdogPid = pid;
27
+ (0, session_js_1.writeSessionState)(adapter.stateDir, state);
28
+ }
29
+ }, lockWaitMs);
30
+ }
31
+ /** Silent, read-only observer of the agent process; never sends input to it. */
32
+ async function runWatchdog(adapter, config, sessionId, deps = {}) {
33
+ if (!sessionId || adapter.processNames?.length === 0)
34
+ return;
35
+ const isAlive = deps.isAlive ?? liveness_js_1.isAgentAlive;
36
+ const now = deps.now ?? Date.now;
37
+ const wait = deps.wait ?? session_js_1.sleep;
38
+ const selfPid = deps.selfPid ?? process.pid;
39
+ let unknownTicks = 0;
40
+ let rejected = false;
41
+ let heartbeatRejected = false;
42
+ const delivery = {
43
+ ...deps,
44
+ onResult: (result) => {
45
+ if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
46
+ rejected = true;
47
+ deps.onResult?.(result);
48
+ },
49
+ };
50
+ // Do not clear a replacement observer's ownership. A failed write retries on the next tick.
51
+ const finish = async (terminal = false, abandoned = false) => {
52
+ await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
53
+ const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
54
+ if (!fresh || (fresh.watchdogPid && fresh.watchdogPid !== selfPid))
55
+ return;
56
+ if (terminal && !fresh.offline) {
57
+ fresh.offline = true;
58
+ fresh.rev = (fresh.rev ?? 0) + 1;
59
+ }
60
+ if (abandoned)
61
+ fresh.offlineDeliveryStopped = true;
62
+ fresh.watchdogPid = undefined;
63
+ (0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
64
+ });
65
+ };
66
+ for (;;) {
67
+ await wait(exports.WATCHDOG_TICK_MS);
68
+ try {
69
+ const state = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
70
+ if (!state)
71
+ return;
72
+ if (state.watchdogPid && state.watchdogPid !== selfPid)
73
+ return;
74
+ if (rejected || state.offlineDeliveryStopped) {
75
+ await finish(true, true);
76
+ return;
77
+ }
78
+ if (heartbeatRejected) {
79
+ await finish();
80
+ return;
81
+ }
82
+ if (!state.offline && !state.agentPid) {
83
+ if (++unknownTicks >= 3) {
84
+ await finish();
85
+ return;
86
+ }
87
+ continue;
88
+ }
89
+ const terminal = state.offline || !isAlive({ pid: state.agentPid, startedAt: state.agentStartedAt });
90
+ if (terminal) {
91
+ if (state.offline && (state.pushedRev ?? 0) >= (state.rev ?? 0)) {
92
+ await finish();
93
+ return;
94
+ }
95
+ const startedAt = await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
96
+ const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
97
+ if (!fresh ||
98
+ fresh.agentPid !== state.agentPid ||
99
+ (fresh.watchdogPid && fresh.watchdogPid !== selfPid))
100
+ return undefined;
101
+ if (!fresh.offline) {
102
+ fresh.offline = true;
103
+ fresh.rev = (fresh.rev ?? 0) + 1;
104
+ }
105
+ fresh.offlineRetryStartedAt ??= now();
106
+ (0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
107
+ return fresh.offlineRetryStartedAt;
108
+ });
109
+ if (startedAt === undefined)
110
+ continue;
111
+ if (now() - startedAt >= exports.WATCHDOG_OFFLINE_RETRY_MS) {
112
+ await finish(true, true);
113
+ return;
114
+ }
115
+ if (await (0, session_js_1.flushSession)(adapter, config, sessionId, delivery)) {
116
+ await finish();
117
+ return;
118
+ }
119
+ }
120
+ else if ((state.rev ?? 0) > (state.pushedRev ?? 0) ||
121
+ now() - (state.lastPushAt ?? 0) >= exports.WATCHDOG_HEARTBEAT_MS) {
122
+ await (0, session_js_1.flushSession)(adapter, config, sessionId, {
123
+ ...deps,
124
+ force: true,
125
+ onResult: (result) => {
126
+ if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
127
+ heartbeatRejected = true;
128
+ deps.onResult?.(result);
129
+ },
130
+ });
131
+ if (heartbeatRejected) {
132
+ // A rejected heartbeat is not evidence the agent died. A later hook
133
+ // can restart observation after credentials/routing recover.
134
+ await finish();
135
+ return;
136
+ }
137
+ }
138
+ if (rejected) {
139
+ await finish(true, true);
140
+ return;
141
+ }
142
+ }
143
+ catch {
144
+ // A transient state-lock/filesystem error must not kill an otherwise live observer.
145
+ continue;
146
+ }
147
+ }
148
+ }
149
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULTS = void 0;
4
+ const node_os_1 = require("node:os");
5
+ const node_path_1 = require("node:path");
6
+ exports.DEFAULTS = {
7
+ stateDir: (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'nocville-claude-code'),
8
+ stateFilePrefix: 'session-',
9
+ stateFileMaxAge: 24 * 60 * 60 * 1000, // 24 hours
10
+ httpTimeout: 4000,
11
+ /** Watchdog-backed sessions become unknown after three minutes without a heartbeat. */
12
+ heartbeatSeconds: 90,
13
+ /** Number of recent activity lines kept in the desk-sign ticker. */
14
+ activityBufferSize: 8,
15
+ /** Coalesce same-phase activity pushes closer together than this. */
16
+ minPushGapMs: 1500,
17
+ /** Per-hook timeout written into settings.json (seconds) — a safety net only. */
18
+ hookTimeoutSeconds: 10,
19
+ };
20
+ //# sourceMappingURL=defaults.js.map