@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,144 @@
1
+ import { isAgentAlive } from './liveness.js';
2
+ import { withStateLock } from './lock.js';
3
+ import { flushSession, readSessionState, sleep, spawnDetachedAgent, stateFilePath, writeSessionState, } from './session.js';
4
+ export const WATCHDOG_TICK_MS = 15_000;
5
+ export const WATCHDOG_HEARTBEAT_MS = 60_000;
6
+ export const WATCHDOG_OFFLINE_RETRY_MS = 15 * 60_000;
7
+ export async function ensureWatchdog(adapter, config, sessionId, cliPath, lockWaitMs) {
8
+ if (adapter.processNames?.length === 0 ||
9
+ !config.apiKey ||
10
+ process.env['NOCVILLE_NO_WATCHDOG'] === '1')
11
+ return;
12
+ await withStateLock(stateFilePath(adapter.stateDir, sessionId), () => {
13
+ const state = readSessionState(adapter.stateDir, sessionId);
14
+ // No heartbeat without proven liveness. Keep the long fallback on unsupported hosts.
15
+ if (!state || state.offline || !state.agentPid)
16
+ return;
17
+ if (state.watchdogPid && isAgentAlive({ pid: state.watchdogPid }))
18
+ return;
19
+ const pid = spawnDetachedAgent(adapter, config, ['watchdog', sessionId], cliPath);
20
+ if (pid) {
21
+ state.watchdogPid = pid;
22
+ writeSessionState(adapter.stateDir, state);
23
+ }
24
+ }, lockWaitMs);
25
+ }
26
+ /** Silent, read-only observer of the agent process; never sends input to it. */
27
+ export async function runWatchdog(adapter, config, sessionId, deps = {}) {
28
+ if (!sessionId || adapter.processNames?.length === 0)
29
+ return;
30
+ const isAlive = deps.isAlive ?? isAgentAlive;
31
+ const now = deps.now ?? Date.now;
32
+ const wait = deps.wait ?? sleep;
33
+ const selfPid = deps.selfPid ?? process.pid;
34
+ let unknownTicks = 0;
35
+ let rejected = false;
36
+ let heartbeatRejected = false;
37
+ const delivery = {
38
+ ...deps,
39
+ onResult: (result) => {
40
+ if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
41
+ rejected = true;
42
+ deps.onResult?.(result);
43
+ },
44
+ };
45
+ // Do not clear a replacement observer's ownership. A failed write retries on the next tick.
46
+ const finish = async (terminal = false, abandoned = false) => {
47
+ await withStateLock(stateFilePath(adapter.stateDir, sessionId), () => {
48
+ const fresh = readSessionState(adapter.stateDir, sessionId);
49
+ if (!fresh || (fresh.watchdogPid && fresh.watchdogPid !== selfPid))
50
+ return;
51
+ if (terminal && !fresh.offline) {
52
+ fresh.offline = true;
53
+ fresh.rev = (fresh.rev ?? 0) + 1;
54
+ }
55
+ if (abandoned)
56
+ fresh.offlineDeliveryStopped = true;
57
+ fresh.watchdogPid = undefined;
58
+ writeSessionState(adapter.stateDir, fresh);
59
+ });
60
+ };
61
+ for (;;) {
62
+ await wait(WATCHDOG_TICK_MS);
63
+ try {
64
+ const state = readSessionState(adapter.stateDir, sessionId);
65
+ if (!state)
66
+ return;
67
+ if (state.watchdogPid && state.watchdogPid !== selfPid)
68
+ return;
69
+ if (rejected || state.offlineDeliveryStopped) {
70
+ await finish(true, true);
71
+ return;
72
+ }
73
+ if (heartbeatRejected) {
74
+ await finish();
75
+ return;
76
+ }
77
+ if (!state.offline && !state.agentPid) {
78
+ if (++unknownTicks >= 3) {
79
+ await finish();
80
+ return;
81
+ }
82
+ continue;
83
+ }
84
+ const terminal = state.offline || !isAlive({ pid: state.agentPid, startedAt: state.agentStartedAt });
85
+ if (terminal) {
86
+ if (state.offline && (state.pushedRev ?? 0) >= (state.rev ?? 0)) {
87
+ await finish();
88
+ return;
89
+ }
90
+ const startedAt = await withStateLock(stateFilePath(adapter.stateDir, sessionId), () => {
91
+ const fresh = readSessionState(adapter.stateDir, sessionId);
92
+ if (!fresh ||
93
+ fresh.agentPid !== state.agentPid ||
94
+ (fresh.watchdogPid && fresh.watchdogPid !== selfPid))
95
+ return undefined;
96
+ if (!fresh.offline) {
97
+ fresh.offline = true;
98
+ fresh.rev = (fresh.rev ?? 0) + 1;
99
+ }
100
+ fresh.offlineRetryStartedAt ??= now();
101
+ writeSessionState(adapter.stateDir, fresh);
102
+ return fresh.offlineRetryStartedAt;
103
+ });
104
+ if (startedAt === undefined)
105
+ continue;
106
+ if (now() - startedAt >= WATCHDOG_OFFLINE_RETRY_MS) {
107
+ await finish(true, true);
108
+ return;
109
+ }
110
+ if (await flushSession(adapter, config, sessionId, delivery)) {
111
+ await finish();
112
+ return;
113
+ }
114
+ }
115
+ else if ((state.rev ?? 0) > (state.pushedRev ?? 0) ||
116
+ now() - (state.lastPushAt ?? 0) >= WATCHDOG_HEARTBEAT_MS) {
117
+ await flushSession(adapter, config, sessionId, {
118
+ ...deps,
119
+ force: true,
120
+ onResult: (result) => {
121
+ if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
122
+ heartbeatRejected = true;
123
+ deps.onResult?.(result);
124
+ },
125
+ });
126
+ if (heartbeatRejected) {
127
+ // A rejected heartbeat is not evidence the agent died. A later hook
128
+ // can restart observation after credentials/routing recover.
129
+ await finish();
130
+ return;
131
+ }
132
+ }
133
+ if (rejected) {
134
+ await finish(true, true);
135
+ return;
136
+ }
137
+ }
138
+ catch {
139
+ // A transient state-lock/filesystem error must not kill an otherwise live observer.
140
+ continue;
141
+ }
142
+ }
143
+ }
144
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1,15 @@
1
+ export declare const DEFAULTS: {
2
+ stateDir: string;
3
+ stateFilePrefix: string;
4
+ stateFileMaxAge: number;
5
+ httpTimeout: number;
6
+ /** Watchdog-backed sessions become unknown after three minutes without a heartbeat. */
7
+ heartbeatSeconds: number;
8
+ /** Number of recent activity lines kept in the desk-sign ticker. */
9
+ activityBufferSize: number;
10
+ /** Coalesce same-phase activity pushes closer together than this. */
11
+ minPushGapMs: number;
12
+ /** Per-hook timeout written into settings.json (seconds) — a safety net only. */
13
+ hookTimeoutSeconds: number;
14
+ };
15
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const DEFAULTS = {
4
+ stateDir: join(tmpdir(), 'nocville-claude-code'),
5
+ stateFilePrefix: 'session-',
6
+ stateFileMaxAge: 24 * 60 * 60 * 1000, // 24 hours
7
+ httpTimeout: 4000,
8
+ /** Watchdog-backed sessions become unknown after three minutes without a heartbeat. */
9
+ heartbeatSeconds: 90,
10
+ /** Number of recent activity lines kept in the desk-sign ticker. */
11
+ activityBufferSize: 8,
12
+ /** Coalesce same-phase activity pushes closer together than this. */
13
+ minPushGapMs: 1500,
14
+ /** Per-hook timeout written into settings.json (seconds) — a safety net only. */
15
+ hookTimeoutSeconds: 10,
16
+ };
17
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,40 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ import type { AgentHookSpec, HookRunOptions } from '../agent-common/hook-runner.js';
3
+ import type { AgentAdapter } from '../agent-common/session.js';
4
+ import type { HookEvent } from './state-machine.js';
5
+ export declare const CLAUDE_ADAPTER: AgentAdapter;
6
+ /**
7
+ * Derive a serviceId from the session and project directory.
8
+ * Format: "claude-{project_name}-{session_id_prefix}" (≤100 chars).
9
+ */
10
+ export declare function deriveServiceId(sessionId: string, cwd: string): string;
11
+ /**
12
+ * Humanize a project dir name into a banner label: split on punctuation and
13
+ * title-case each word ("gridbreak" -> "Gridbreak", "my-cool-app" -> "My Cool App").
14
+ */
15
+ export declare function humanizeProjectName(cwd: string): string;
16
+ /**
17
+ * Correlate a session_id to its user-facing name via the local session store.
18
+ * Claude Code writes ~/.claude/sessions/<pid>.json — keyed by pid, each holding
19
+ * `{ sessionId, name, ... }` — so we scan for the file whose sessionId matches.
20
+ * `dir` is injectable for testing.
21
+ */
22
+ export declare function lookupSessionNameFromStore(sessionId: string, dir?: string): string | null;
23
+ /**
24
+ * Resolve the user-facing session name. Checked fresh only on the cheap,
25
+ * low-frequency events (SessionStart carries `session_title`; UserPromptSubmit
26
+ * falls back to the store scan) so renames propagate; otherwise the cached
27
+ * value is reused. Returns null when unknown (banner falls back to the project).
28
+ */
29
+ export declare function resolveSessionName(event: HookEvent, cached: string | null, dir?: string): string | null;
30
+ export declare const CLAUDE_HOOK_SPEC: AgentHookSpec;
31
+ /**
32
+ * Handle a single Claude Code hook event.
33
+ *
34
+ * Reads the JSON payload (from `rawInput` if provided, else stdin), updates the
35
+ * per-session state file, and pushes an NPC status update (banner
36
+ * "Claude · <session name or project>"). Defaults to an in-process push; the CLI
37
+ * passes `mode: 'detach'` so the push runs in a background process. Never throws.
38
+ */
39
+ export declare function handleHookEvent(config: NpcConfig, rawInput?: string, opts?: HookRunOptions): Promise<void>;
40
+ //# sourceMappingURL=hook-handler.d.ts.map
@@ -0,0 +1,131 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { runAgentHook, readStdin } from '../agent-common/hook-runner.js';
5
+ import { projectNameOf, sanitizeServiceId } from '../agent-common/session.js';
6
+ import { isCursorInvocation } from '../cursor/normalize.js';
7
+ import { handleCursorHookEvent } from '../cursor/hook-handler.js';
8
+ import { DEFAULTS } from './defaults.js';
9
+ import { claudeTransition, eventName } from './state-machine.js';
10
+ export const CLAUDE_ADAPTER = {
11
+ command: 'claude-code',
12
+ label: 'Claude',
13
+ family: 'claude',
14
+ stateDir: DEFAULTS.stateDir,
15
+ heartbeatSeconds: DEFAULTS.heartbeatSeconds,
16
+ processNames: ['claude'],
17
+ };
18
+ /**
19
+ * Derive a serviceId from the session and project directory.
20
+ * Format: "claude-{project_name}-{session_id_prefix}" (≤100 chars).
21
+ */
22
+ export function deriveServiceId(sessionId, cwd) {
23
+ return sanitizeServiceId(`claude-${projectNameOf(cwd)}-${sessionId.slice(0, 8)}`);
24
+ }
25
+ /**
26
+ * Humanize a project dir name into a banner label: split on punctuation and
27
+ * title-case each word ("gridbreak" -> "Gridbreak", "my-cool-app" -> "My Cool App").
28
+ */
29
+ export function humanizeProjectName(cwd) {
30
+ const raw = projectNameOf(cwd);
31
+ const words = raw
32
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
33
+ .trim()
34
+ .split(/\s+/)
35
+ .filter(Boolean);
36
+ if (words.length === 0)
37
+ return raw;
38
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
39
+ }
40
+ /** Default location of Claude Code's per-session sidecar files. */
41
+ function sessionsStoreDir() {
42
+ return join(homedir(), '.claude', 'sessions');
43
+ }
44
+ /**
45
+ * Correlate a session_id to its user-facing name via the local session store.
46
+ * Claude Code writes ~/.claude/sessions/<pid>.json — keyed by pid, each holding
47
+ * `{ sessionId, name, ... }` — so we scan for the file whose sessionId matches.
48
+ * `dir` is injectable for testing.
49
+ */
50
+ export function lookupSessionNameFromStore(sessionId, dir = sessionsStoreDir()) {
51
+ if (!sessionId)
52
+ return null;
53
+ let files;
54
+ try {
55
+ files = readdirSync(dir);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ for (const file of files) {
61
+ if (!file.endsWith('.json'))
62
+ continue;
63
+ try {
64
+ const obj = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
65
+ if (obj.sessionId === sessionId && typeof obj.name === 'string' && obj.name.trim()) {
66
+ return obj.name.trim();
67
+ }
68
+ }
69
+ catch {
70
+ // Skip unreadable / partially-written session files.
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Resolve the user-facing session name. Checked fresh only on the cheap,
77
+ * low-frequency events (SessionStart carries `session_title`; UserPromptSubmit
78
+ * falls back to the store scan) so renames propagate; otherwise the cached
79
+ * value is reused. Returns null when unknown (banner falls back to the project).
80
+ */
81
+ export function resolveSessionName(event, cached, dir) {
82
+ const name = eventName(event);
83
+ const shouldRefresh = name === 'SessionStart' || name === 'UserPromptSubmit' || !cached;
84
+ if (!shouldRefresh)
85
+ return cached;
86
+ const fromPayload = typeof event.session_title === 'string' ? event.session_title.trim() : '';
87
+ if (fromPayload)
88
+ return fromPayload;
89
+ return lookupSessionNameFromStore(event.session_id ?? '', dir) ?? cached;
90
+ }
91
+ export const CLAUDE_HOOK_SPEC = {
92
+ adapter: CLAUDE_ADAPTER,
93
+ serviceIdFor: deriveServiceId,
94
+ transition: (event, prev) => claudeTransition(event, prev),
95
+ resolveName: (event, prev) => {
96
+ // Only scan the store on the low-frequency events; never on PreToolUse.
97
+ const name = eventName(event);
98
+ if (prev && name !== 'SessionStart' && name !== 'UserPromptSubmit')
99
+ return undefined;
100
+ return resolveSessionName(event, prev?.name ?? null) ?? undefined;
101
+ },
102
+ };
103
+ /**
104
+ * Handle a single Claude Code hook event.
105
+ *
106
+ * Reads the JSON payload (from `rawInput` if provided, else stdin), updates the
107
+ * per-session state file, and pushes an NPC status update (banner
108
+ * "Claude · <session name or project>"). Defaults to an in-process push; the CLI
109
+ * passes `mode: 'detach'` so the push runs in a background process. Never throws.
110
+ */
111
+ export async function handleHookEvent(config, rawInput, opts = {}) {
112
+ const input = rawInput ?? readStdin();
113
+ let event;
114
+ try {
115
+ event = JSON.parse(input);
116
+ }
117
+ catch {
118
+ return;
119
+ }
120
+ if (!event || typeof event !== 'object' || Array.isArray(event))
121
+ return;
122
+ if (isCursorInvocation(event)) {
123
+ await handleCursorHookEvent(config, event, opts, 'third-party');
124
+ return;
125
+ }
126
+ await runAgentHook(CLAUDE_HOOK_SPEC, config, event, {
127
+ minPushGapMs: DEFAULTS.minPushGapMs,
128
+ ...opts,
129
+ });
130
+ }
131
+ //# sourceMappingURL=hook-handler.js.map
@@ -0,0 +1,36 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ export { DEFAULTS } from './defaults.js';
3
+ export { processHookEvent, claudeTransition } from './state-machine.js';
4
+ export type { SessionState, HookEvent, StatusOutput, SessionMachineState, } from './state-machine.js';
5
+ export { handleHookEvent, CLAUDE_ADAPTER } from './hook-handler.js';
6
+ export { setupHooks, uninstallHooks } from './setup.js';
7
+ export type { SetupOptions } from './setup.js';
8
+ export interface ClaudeCodeSubcommandOptions {
9
+ subcommand: string;
10
+ config: NpcConfig;
11
+ flags: Record<string, string | boolean>;
12
+ /** Positional args after the subcommand (e.g. the session id for `flush`). */
13
+ args?: string[];
14
+ }
15
+ /**
16
+ * claude-code subcommands: setup | uninstall | hook | flush | cleanup.
17
+ * `hook` and `flush` are invoked by Claude Code / ourselves and must stay
18
+ * silent on stdout and never exit non-zero.
19
+ */
20
+ export declare function handleClaudeCodeCommand(opts: ClaudeCodeSubcommandOptions): Promise<void>;
21
+ /**
22
+ * ClaudeCodeNPC class for programmatic use.
23
+ */
24
+ export declare class ClaudeCodeNPC {
25
+ private config;
26
+ constructor(config: NpcConfig);
27
+ /** Process a hook event from stdin (in-process push). */
28
+ handleHook(): Promise<void>;
29
+ /** Install hooks into Claude Code settings. */
30
+ setup(): void;
31
+ /** Remove all Nocville hooks from Claude Code settings. */
32
+ uninstall(): void;
33
+ /** Clean up stale session state files. */
34
+ cleanup(): Promise<void>;
35
+ }
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { numberFlag, stringFlag } from '../../core/config.js';
2
+ import { readStdin, runFlushCommand } from '../agent-common/hook-runner.js';
3
+ import { runWatchdog } from '../agent-common/watchdog.js';
4
+ import { cleanupSessionStates } from '../agent-common/session.js';
5
+ import { DEFAULTS } from './defaults.js';
6
+ import { CLAUDE_ADAPTER, handleHookEvent } from './hook-handler.js';
7
+ import { setupHooks, uninstallHooks } from './setup.js';
8
+ export { DEFAULTS } from './defaults.js';
9
+ export { processHookEvent, claudeTransition } from './state-machine.js';
10
+ export { handleHookEvent, CLAUDE_ADAPTER } from './hook-handler.js';
11
+ export { setupHooks, uninstallHooks } from './setup.js';
12
+ /** Remove stale session state files. */
13
+ async function cleanupStateFiles() {
14
+ const removed = await cleanupSessionStates(DEFAULTS.stateDir, DEFAULTS.stateFileMaxAge);
15
+ console.log(`Cleanup complete: ${removed} stale file(s) removed`);
16
+ }
17
+ /**
18
+ * claude-code subcommands: setup | uninstall | hook | flush | cleanup.
19
+ * `hook` and `flush` are invoked by Claude Code / ourselves and must stay
20
+ * silent on stdout and never exit non-zero.
21
+ */
22
+ export async function handleClaudeCodeCommand(opts) {
23
+ const { subcommand, config, flags } = opts;
24
+ switch (subcommand) {
25
+ case 'setup': {
26
+ if (flags['uninstall']) {
27
+ uninstallHooks();
28
+ break;
29
+ }
30
+ const ok = setupHooks({
31
+ apiUrl: stringFlag(flags, 'api-url') ?? process.env['NOCVILLE_API_URL'],
32
+ apiKey: stringFlag(flags, 'api-key') ?? process.env['NOCVILLE_API_KEY'],
33
+ map: stringFlag(flags, 'map'),
34
+ cliPath: process.argv[1],
35
+ debug: config.debug,
36
+ });
37
+ if (!ok)
38
+ process.exitCode = 1;
39
+ break;
40
+ }
41
+ case 'uninstall':
42
+ uninstallHooks();
43
+ break;
44
+ case 'hook':
45
+ await handleHookEvent(config, readStdin(), { mode: 'detach' });
46
+ break;
47
+ case 'watchdog':
48
+ await runWatchdog(CLAUDE_ADAPTER, config, opts.args?.[0]);
49
+ break;
50
+ case 'flush':
51
+ await runFlushCommand(CLAUDE_ADAPTER, config, opts.args?.[0], numberFlag(flags, 'delay'), flags['stats'] === true);
52
+ break;
53
+ case 'cleanup':
54
+ await cleanupStateFiles();
55
+ break;
56
+ default: {
57
+ console.error(`Unknown claude-code subcommand: ${subcommand}`);
58
+ console.error('Available subcommands: setup, uninstall, hook, cleanup');
59
+ process.exitCode = 1;
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * ClaudeCodeNPC class for programmatic use.
65
+ */
66
+ export class ClaudeCodeNPC {
67
+ config;
68
+ constructor(config) {
69
+ this.config = config;
70
+ }
71
+ /** Process a hook event from stdin (in-process push). */
72
+ async handleHook() {
73
+ await handleHookEvent(this.config);
74
+ }
75
+ /** Install hooks into Claude Code settings. */
76
+ setup() {
77
+ setupHooks({
78
+ apiUrl: this.config.apiUrl,
79
+ apiKey: this.config.apiKey,
80
+ debug: this.config.debug,
81
+ });
82
+ }
83
+ /** Remove all Nocville hooks from Claude Code settings. */
84
+ uninstall() {
85
+ uninstallHooks();
86
+ }
87
+ /** Clean up stale session state files. */
88
+ async cleanup() {
89
+ await cleanupStateFiles();
90
+ }
91
+ }
92
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,58 @@
1
+ import type { HooksFile } from '../agent-common/hook-settings.js';
2
+ export { extractFlag } from '../agent-common/hook-settings.js';
3
+ export declare function claudeSettingsPath(): string;
4
+ /**
5
+ * Hook events to install. Verified against code.claude.com/docs/en/hooks —
6
+ * all exist; events without matcher support ignore the empty matcher.
7
+ */
8
+ export declare const CLAUDE_HOOK_EVENTS: readonly ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "Notification", "Stop", "SubagentStart", "SubagentStop", "PreCompact"];
9
+ export interface SetupOptions {
10
+ /** Explicit --api-url (or env). Saved to the user config. */
11
+ apiUrl?: string;
12
+ /** Explicit --api-key (or env). Saved to the user config. */
13
+ apiKey?: string;
14
+ /** Explicit --map. Saved to the user config. */
15
+ map?: string;
16
+ /** Absolute path to the built cli.js; defaults to the running script. */
17
+ cliPath?: string;
18
+ /** Override ~/.claude/settings.json (tests). */
19
+ settingsPath?: string;
20
+ debug?: boolean;
21
+ /** Suppress console output (tests). */
22
+ quiet?: boolean;
23
+ }
24
+ type ClaudeSettings = HooksFile;
25
+ /** Install the nocville group into every Claude Code event. Mutates `settings`. */
26
+ export declare function installHooksInto(settings: ClaudeSettings, command: string, opts?: {
27
+ timeout?: number;
28
+ platform?: NodeJS.Platform;
29
+ }): {
30
+ migrated: number;
31
+ installed: number;
32
+ };
33
+ /** Remove all nocville entries (nested or legacy flat). Mutates `settings`. */
34
+ export declare function removeHooksFrom(settings: ClaudeSettings): number;
35
+ export { findNocvilleCommand as findExistingNocvilleCommand } from '../agent-common/hook-settings.js';
36
+ /** `'<node>' '<cli.js>' claude-code hook # nocville-npc-hook` — no key on the command line. */
37
+ export declare function buildHookCommand(_cliPath: string): string;
38
+ /**
39
+ * Credentials embedded in a legacy hook command (`--api-url=… --api-key=…`),
40
+ * so upgrading setup can move them into the user config.
41
+ */
42
+ export declare function legacyCredentials(settings: ClaudeSettings): {
43
+ apiUrl?: string;
44
+ apiKey?: string;
45
+ };
46
+ /**
47
+ * Install Nocville hooks into Claude Code settings (matcher-group format,
48
+ * absolute node/cli paths, 10s timeout safety net). Idempotent: migrates and
49
+ * de-duplicates legacy entries and moves any legacy inline credentials into
50
+ * ~/.config/nocville/config.json. Returns false if setup could not complete.
51
+ */
52
+ export declare function setupHooks(opts: SetupOptions): boolean;
53
+ /** Remove all Nocville hooks from Claude Code settings. */
54
+ export declare function uninstallHooks(opts?: {
55
+ settingsPath?: string;
56
+ quiet?: boolean;
57
+ } | boolean): number;
58
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1,121 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { maskKey, persistCredentials } from '../../core/user-config.js';
5
+ import { buildShimHookCommand, stableNodePath, extractFlag, findNocvilleCommand, installHookGroups, readJsonFile, removeHookGroups, resolveCliPath, writeJsonFile, } from '../agent-common/hook-settings.js';
6
+ import { installHookShim } from '../agent-common/shim.js';
7
+ import { ensureStableCliPath } from '../agent-common/runtime.js';
8
+ import { DEFAULTS } from './defaults.js';
9
+ export { extractFlag } from '../agent-common/hook-settings.js';
10
+ export function claudeSettingsPath() {
11
+ return join(homedir(), '.claude', 'settings.json');
12
+ }
13
+ /**
14
+ * Hook events to install. Verified against code.claude.com/docs/en/hooks —
15
+ * all exist; events without matcher support ignore the empty matcher.
16
+ */
17
+ export const CLAUDE_HOOK_EVENTS = [
18
+ 'SessionStart',
19
+ 'SessionEnd',
20
+ 'UserPromptSubmit',
21
+ 'PreToolUse',
22
+ 'PostToolUse',
23
+ 'PostToolUseFailure',
24
+ 'PermissionRequest',
25
+ 'Notification',
26
+ 'Stop',
27
+ 'SubagentStart',
28
+ 'SubagentStop',
29
+ 'PreCompact',
30
+ ];
31
+ /** Install the nocville group into every Claude Code event. Mutates `settings`. */
32
+ export function installHooksInto(settings, command, opts = {}) {
33
+ return installHookGroups(settings, command, CLAUDE_HOOK_EVENTS, {
34
+ perEvent: Object.fromEntries(CLAUDE_HOOK_EVENTS.map((event) => [
35
+ event,
36
+ {
37
+ ...((opts.platform ?? process.platform) === 'win32' ? { shell: 'powershell' } : {}),
38
+ ...(event === 'SessionEnd'
39
+ ? { timeout: 5 }
40
+ : { async: true, timeout: opts.timeout ?? 10 }),
41
+ },
42
+ ])),
43
+ });
44
+ }
45
+ /** Remove all nocville entries (nested or legacy flat). Mutates `settings`. */
46
+ export function removeHooksFrom(settings) {
47
+ return removeHookGroups(settings);
48
+ }
49
+ export { findNocvilleCommand as findExistingNocvilleCommand } from '../agent-common/hook-settings.js';
50
+ /** `'<node>' '<cli.js>' claude-code hook # nocville-npc-hook` — no key on the command line. */
51
+ export function buildHookCommand(_cliPath) {
52
+ return buildShimHookCommand('claude-code');
53
+ }
54
+ /**
55
+ * Credentials embedded in a legacy hook command (`--api-url=… --api-key=…`),
56
+ * so upgrading setup can move them into the user config.
57
+ */
58
+ export function legacyCredentials(settings) {
59
+ const prev = findNocvilleCommand(settings);
60
+ if (!prev)
61
+ return {};
62
+ return { apiUrl: extractFlag(prev, 'api-url'), apiKey: extractFlag(prev, 'api-key') };
63
+ }
64
+ function out(opts, msg) {
65
+ if (!opts.quiet)
66
+ console.log(msg);
67
+ }
68
+ /**
69
+ * Install Nocville hooks into Claude Code settings (matcher-group format,
70
+ * absolute node/cli paths, 10s timeout safety net). Idempotent: migrates and
71
+ * de-duplicates legacy entries and moves any legacy inline credentials into
72
+ * ~/.config/nocville/config.json. Returns false if setup could not complete.
73
+ */
74
+ export function setupHooks(opts) {
75
+ const settingsPath = opts.settingsPath ?? claudeSettingsPath();
76
+ const dir = join(settingsPath, '..');
77
+ if (!existsSync(dir)) {
78
+ console.error(`${dir} does not exist — is Claude Code installed? Skipping Claude Code setup.`);
79
+ return false;
80
+ }
81
+ const settings = readJsonFile(settingsPath);
82
+ if (settings === null) {
83
+ console.error(`Could not parse ${settingsPath}; fix it (or move it aside) and re-run setup.`);
84
+ return false;
85
+ }
86
+ const creds = persistCredentials(opts, legacyCredentials(settings));
87
+ const resolved = resolveCliPath(opts.cliPath);
88
+ const cliPath = ensureStableCliPath(resolved);
89
+ installHookShim(stableNodePath(), cliPath);
90
+ const command = buildHookCommand(cliPath);
91
+ const { migrated, installed } = installHooksInto(settings, command, {
92
+ timeout: DEFAULTS.hookTimeoutSeconds,
93
+ });
94
+ writeJsonFile(settingsPath, settings);
95
+ out(opts, `✓ Claude Code: installed Nocville hooks for ${installed} events in ${settingsPath}`);
96
+ if (migrated > 0)
97
+ out(opts, ` (migrated ${migrated} legacy entr${migrated === 1 ? 'y' : 'ies'})`);
98
+ out(opts, ` Hook runs: ${process.execPath} ${cliPath}`);
99
+ out(opts, ` API: ${creds.apiUrl ?? 'https://nocville.com'} key: ${maskKey(creds.apiKey)}`);
100
+ if (!creds.apiKey) {
101
+ out(opts, ' ⚠ No API key saved yet — run: nocville-npc init --api-key=<your team key>');
102
+ }
103
+ out(opts, ' New Claude Code sessions will appear in your office. Restart running sessions.');
104
+ return true;
105
+ }
106
+ /** Remove all Nocville hooks from Claude Code settings. */
107
+ export function uninstallHooks(opts = {}) {
108
+ const o = typeof opts === 'boolean' ? {} : opts;
109
+ const settingsPath = o.settingsPath ?? claudeSettingsPath();
110
+ const settings = readJsonFile(settingsPath);
111
+ if (!settings || !existsSync(settingsPath)) {
112
+ out(o, 'Claude Code: no settings.json — nothing to remove.');
113
+ return 0;
114
+ }
115
+ const removed = removeHooksFrom(settings);
116
+ if (removed > 0)
117
+ writeJsonFile(settingsPath, settings);
118
+ out(o, `✓ Claude Code: removed ${removed} Nocville hook(s) from ${settingsPath}`);
119
+ return removed;
120
+ }
121
+ //# sourceMappingURL=setup.js.map