@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
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { parseArgs, resolveConfig, stringFlag, numberFlag, boolFlag } from './core/config.js';
6
+ import { Logger } from './core/logger.js';
7
+ import { ensureSessionFilterTemplate, loadSessionFilter, sessionPublishDecision, } from './core/session-filter.js';
8
+ // NPC modules are imported lazily per command: `claude-code hook` / `codex hook`
9
+ // run on every tool call, so they must not pay for loading every NPC (and pngjs).
10
+ const USAGE = `
11
+ nocville-npc <command> [options]
12
+
13
+ Quick start (AI coworkers):
14
+ nocville-npc setup Paste your agent key (hidden prompt),
15
+ then hook up installed coding agents
16
+ (whichever are installed)
17
+ nocville-npc setup --api-key=<agent key> Same, non-interactive
18
+ nocville-npc uninstall Remove all Nocville hooks
19
+
20
+ Config:
21
+ init [--api-key=<key>] [--api-url=<url>] [--map=<name>]
22
+ Save credentials (prompts for the key if omitted) to ~/.config/nocville/config.json (0600)
23
+ so no other command needs --api-key
24
+ doctor Check API connectivity and a test status push
25
+ sessions [--project=<dir>] [--name=<session name>]
26
+ Show which AI sessions publish to your office
27
+ (allow/deny lists in ~/.config/nocville/sessions.json)
28
+ and test a folder (default: current) or session name
29
+
30
+ AI coding agents (one NPC per session, named "Claude · <project>"):
31
+ claude-code setup Install hooks into ~/.claude/settings.json
32
+ claude-code uninstall Remove them
33
+ codex setup Install hooks into ~/.codex/hooks.json (then trust
34
+ them once with /hooks inside Codex)
35
+ codex uninstall Remove them
36
+ gemini setup Install hooks into ~/.gemini/settings.json
37
+ gemini uninstall Remove them
38
+ cursor setup Install native hooks into ~/.cursor/hooks.json
39
+ cursor uninstall Remove them
40
+ codex watch No-hooks fallback: follow ~/.codex/sessions logs
41
+ --idle-timeout=<min> Despawn a session after this long quiet (default 30)
42
+ --include-exec Also show non-interactive \`codex exec\` runs
43
+
44
+ Monitoring NPC types:
45
+ system-monitor Monitor CPU, memory, and disk usage
46
+ internet-connectivity Monitor network connectivity
47
+ process-watcher Watch for running processes
48
+ log-watcher Watch log files for errors
49
+ receptionist Greeter showing time + local weather
50
+ github-watcher Call out new commits on a repo
51
+ web-page-watcher Render a watched web page as a pixel-art TV
52
+
53
+ Common Options:
54
+ --api-key=<key> Team API key (or NOCVILLE_API_KEY env)
55
+ --api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
56
+ Default: https://nocville.com. For local dev use
57
+ --api-url=http://localhost:3601
58
+ --service-id=<id> Service ID (or NOCVILLE_SERVICE_ID env)
59
+ --name=<display name> Friendly NPC banner name (or NOCVILLE_NAME env).
60
+ Default: a per-NPC name (e.g. "System Monitor").
61
+ --map=<id-or-name> Target map by id or name (or NOCVILLE_MAP env).
62
+ Default: the team's default map.
63
+ --debug Enable debug logging
64
+ --help Show this help message
65
+
66
+ System Monitor Options:
67
+ --poll-interval=<ms> Poll interval in ms (default: 15000)
68
+ --disk-path=<path> Disk path to monitor (default: /)
69
+ --cpu-warn=<n> CPU warning threshold % (default: 75)
70
+ --cpu-crit=<n> CPU critical threshold % (default: 90)
71
+ --mem-warn=<n> Memory warning threshold % (default: 75)
72
+ --mem-crit=<n> Memory critical threshold % (default: 90)
73
+ --disk-warn=<n> Disk warning threshold % (default: 75)
74
+ --disk-crit=<n> Disk critical threshold % (default: 90)
75
+
76
+ Internet Connectivity Options:
77
+ --poll-interval=<ms> Poll interval in ms (default: 30000)
78
+ --dns-hosts=<list> Comma-separated DNS hosts (default: google.com,cloudflare.com)
79
+ --http-urls=<list> Comma-separated HTTP URLs to check
80
+ --latency-hosts=<list> Comma-separated hosts for latency checks
81
+ --slow-threshold=<ms> Slow response threshold in ms (default: 1000)
82
+
83
+ Process Watcher Options:
84
+ --poll-interval=<ms> Poll interval in ms (default: 15000)
85
+ --processes=<list> Comma-separated process names to watch
86
+ --cpu-warn=<n> CPU warning threshold % (default: 80)
87
+ --mem-warn=<n> Memory warning threshold MB (default: 500)
88
+
89
+ Log Watcher Options:
90
+ --files=<list> Comma-separated log file paths
91
+ --error-pattern=<regex> Error pattern (default: ERROR|FATAL)
92
+ --warning-pattern=<regex> Warning pattern (default: WARN)
93
+ --window=<seconds> Sliding window in seconds (default: 300)
94
+ --error-warn=<n> Error count for warning status (default: 5)
95
+ --error-crit=<n> Error count for critical status (default: 20)
96
+ --report-interval=<ms> Report interval in ms (default: 15000)
97
+
98
+ Receptionist Options:
99
+ --poll-interval=<ms> Clock tick in ms (default: 60000)
100
+ --lat=<n> --lon=<n> Explicit coordinates for weather
101
+ --location=<name> Place name to geocode (else IP geo)
102
+ --fahrenheit Show temperature in °F (default: °C)
103
+
104
+ GitHub Watcher Options:
105
+ --repo=<owner/name> Repository to watch (required)
106
+ --branch=<name> Branch to watch (default: repo default branch)
107
+ --token=<token> GitHub token (or GITHUB_TOKEN env)
108
+ --poll-interval=<ms> Poll interval in ms (default: 60000)
109
+
110
+ Web Page Watcher Options:
111
+ --url=<url> Page to watch (required)
112
+ --mode=<auto|screenshot|basic> Render mode (default: auto)
113
+ --interval=<ms> Poll interval in ms (default: 60000, floor 15000)
114
+ --viewport=<WxH> Browser viewport (default: screen x 8)
115
+ --screen=<WxH> TV pixel resolution (default: 160x120; e.g. 160x100)
116
+ --wait-until=<event> Puppeteer waitUntil (default: networkidle2)
117
+ --chrome-path=<path> Chrome executable (or CHROME_PATH env)
118
+ (needs puppeteer-core + Chrome for the pixel-art TV; falls back to basic)
119
+ `.trim();
120
+ async function main() {
121
+ const parsed = parseArgs(process.argv);
122
+ if (parsed.flags['version']) {
123
+ console.log(JSON.parse(readFileSync(join(dirname(realpathSync(process.argv[1])), '..', 'package.json'), 'utf8')).version);
124
+ return;
125
+ }
126
+ if (parsed.flags['help'] || parsed.flags['h']) {
127
+ console.log(USAGE);
128
+ process.exit(0);
129
+ }
130
+ if (!parsed.command) {
131
+ console.error('Error: NPC type required\n');
132
+ console.log(USAGE);
133
+ process.exit(1);
134
+ }
135
+ const config = resolveConfig(parsed.flags);
136
+ const logger = new Logger('cli', config.debug);
137
+ // Setup-style commands resolve the key: --api-key > env > saved > hidden prompt.
138
+ const isAgentSetup = ['claude-code', 'codex', 'gemini', 'cursor'].includes(parsed.command) &&
139
+ parsed.subcommand === 'setup' &&
140
+ !parsed.flags['uninstall'];
141
+ if (isAgentSetup || parsed.command === 'setup') {
142
+ if (parsed.command === 'setup') {
143
+ const { codexHome } = await import('./npcs/codex/defaults.js');
144
+ if (!existsSync(join(homedir(), '.claude')) &&
145
+ !existsSync(codexHome()) &&
146
+ !existsSync(join(homedir(), '.gemini')) &&
147
+ !existsSync(join(homedir(), '.cursor'))) {
148
+ console.error('Nothing to set up: install Claude Code, Codex, Gemini CLI or Cursor first.');
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+ }
153
+ const key = await resolveSetupKey(parsed.flags);
154
+ if (key === null)
155
+ return;
156
+ if (key !== undefined)
157
+ parsed.flags['api-key'] = key;
158
+ }
159
+ // AI-agent integrations have their own config handling (hooks read the saved config).
160
+ if (parsed.command === 'claude-code') {
161
+ const { handleClaudeCodeCommand } = await import('./npcs/claude-code/index.js');
162
+ await handleClaudeCodeCommand({
163
+ subcommand: parsed.subcommand ?? 'hook',
164
+ config,
165
+ flags: parsed.flags,
166
+ args: parsed.args,
167
+ });
168
+ if (parsed.subcommand === 'setup' && !parsed.flags['uninstall'])
169
+ announceSessionFilter();
170
+ return;
171
+ }
172
+ if (parsed.command === 'codex') {
173
+ const { handleCodexCommand } = await import('./npcs/codex/index.js');
174
+ await handleCodexCommand({
175
+ subcommand: parsed.subcommand ?? 'hook',
176
+ config,
177
+ flags: parsed.flags,
178
+ args: parsed.args,
179
+ });
180
+ if (parsed.subcommand === 'setup' && !parsed.flags['uninstall'])
181
+ announceSessionFilter();
182
+ return;
183
+ }
184
+ if (parsed.command === 'gemini') {
185
+ const { handleGeminiCommand } = await import('./npcs/gemini/index.js');
186
+ await handleGeminiCommand({
187
+ subcommand: parsed.subcommand ?? 'hook',
188
+ config,
189
+ flags: parsed.flags,
190
+ args: parsed.args,
191
+ });
192
+ if (parsed.subcommand === 'setup' && !parsed.flags['uninstall'])
193
+ announceSessionFilter();
194
+ return;
195
+ }
196
+ if (parsed.command === 'cursor') {
197
+ const { handleCursorCommand } = await import('./npcs/cursor/index.js');
198
+ await handleCursorCommand({
199
+ subcommand: parsed.subcommand ?? 'hook',
200
+ config,
201
+ flags: parsed.flags,
202
+ args: parsed.args,
203
+ });
204
+ if (parsed.subcommand === 'setup' && !parsed.flags['uninstall'])
205
+ announceSessionFilter();
206
+ return;
207
+ }
208
+ if (parsed.command === 'init') {
209
+ await runInit(parsed.flags, config);
210
+ return;
211
+ }
212
+ if (parsed.command === 'setup') {
213
+ await runSetupAll(parsed.flags);
214
+ if (!process.exitCode)
215
+ announceSessionFilter();
216
+ return;
217
+ }
218
+ if (parsed.command === 'sessions') {
219
+ runSessionsCommand(parsed.flags);
220
+ return;
221
+ }
222
+ if (parsed.command === 'uninstall') {
223
+ const { uninstallHooks } = await import('./npcs/claude-code/index.js');
224
+ const { uninstallCodexHooks } = await import('./npcs/codex/index.js');
225
+ uninstallHooks();
226
+ uninstallCodexHooks();
227
+ const { uninstallGeminiHooks } = await import('./npcs/gemini/index.js');
228
+ uninstallGeminiHooks();
229
+ const { uninstallCursorHooks } = await import('./npcs/cursor/index.js');
230
+ uninstallCursorHooks();
231
+ return;
232
+ }
233
+ // Doctor checks connectivity; it works without an API key (push step is skipped).
234
+ if (parsed.command === 'doctor') {
235
+ const { runDoctor } = await import('./doctor.js');
236
+ await runDoctor(config);
237
+ return;
238
+ }
239
+ // All other NPC types require api-url and token
240
+ if (!config.apiKey) {
241
+ console.error('Error: no API key — run `nocville-npc init --api-key=<key>`, or pass --api-key / NOCVILLE_API_KEY');
242
+ process.exit(1);
243
+ }
244
+ switch (parsed.command) {
245
+ case 'system-monitor': {
246
+ if (!config.serviceId) {
247
+ config.serviceId = 'system-monitor';
248
+ }
249
+ const { SystemMonitorNPC } = await import('./npcs/system-monitor/index.js');
250
+ const npc = new SystemMonitorNPC({
251
+ ...config,
252
+ pollInterval: numberFlag(parsed.flags, 'poll-interval'),
253
+ diskPath: stringFlag(parsed.flags, 'disk-path'),
254
+ cpuThreshold: {
255
+ warning: numberFlag(parsed.flags, 'cpu-warn') ?? 75,
256
+ critical: numberFlag(parsed.flags, 'cpu-crit') ?? 90,
257
+ direction: 'above',
258
+ },
259
+ memThreshold: {
260
+ warning: numberFlag(parsed.flags, 'mem-warn') ?? 75,
261
+ critical: numberFlag(parsed.flags, 'mem-crit') ?? 90,
262
+ direction: 'above',
263
+ },
264
+ diskThreshold: {
265
+ warning: numberFlag(parsed.flags, 'disk-warn') ?? 75,
266
+ critical: numberFlag(parsed.flags, 'disk-crit') ?? 90,
267
+ direction: 'above',
268
+ },
269
+ });
270
+ logger.info('Starting system-monitor NPC');
271
+ await npc.start();
272
+ break;
273
+ }
274
+ case 'internet-connectivity': {
275
+ if (!config.serviceId) {
276
+ config.serviceId = 'internet-connectivity';
277
+ }
278
+ const dnsHosts = stringFlag(parsed.flags, 'dns-hosts')?.split(',');
279
+ const httpUrls = stringFlag(parsed.flags, 'http-urls')?.split(',');
280
+ const latencyHostsRaw = stringFlag(parsed.flags, 'latency-hosts')?.split(',');
281
+ const latencyHosts = latencyHostsRaw?.map((h) => ({ host: h.trim(), port: 80 }));
282
+ const { InternetConnectivityNPC } = await import('./npcs/internet-connectivity/index.js');
283
+ const npc = new InternetConnectivityNPC({
284
+ ...config,
285
+ pollInterval: numberFlag(parsed.flags, 'poll-interval'),
286
+ dnsHosts,
287
+ httpUrls,
288
+ latencyHosts,
289
+ slowThreshold: numberFlag(parsed.flags, 'slow-threshold'),
290
+ });
291
+ logger.info('Starting internet-connectivity NPC');
292
+ await npc.start();
293
+ break;
294
+ }
295
+ case 'process-watcher': {
296
+ if (!config.serviceId) {
297
+ config.serviceId = 'process-watcher';
298
+ }
299
+ const processesStr = stringFlag(parsed.flags, 'processes');
300
+ if (!processesStr) {
301
+ console.error('Error: --processes is required (comma-separated list)');
302
+ process.exit(1);
303
+ }
304
+ const { ProcessWatcherNPC } = await import('./npcs/process-watcher/index.js');
305
+ const npc = new ProcessWatcherNPC({
306
+ ...config,
307
+ pollInterval: numberFlag(parsed.flags, 'poll-interval'),
308
+ processes: processesStr.split(','),
309
+ cpuWarn: numberFlag(parsed.flags, 'cpu-warn'),
310
+ memWarn: numberFlag(parsed.flags, 'mem-warn'),
311
+ });
312
+ logger.info('Starting process-watcher NPC');
313
+ await npc.start();
314
+ break;
315
+ }
316
+ case 'log-watcher': {
317
+ const filesStr = stringFlag(parsed.flags, 'files');
318
+ if (!filesStr) {
319
+ console.error('Error: --files is required (comma-separated list)');
320
+ process.exit(1);
321
+ }
322
+ if (!config.serviceId) {
323
+ config.serviceId = 'log-watcher';
324
+ }
325
+ const { LogWatcherNPC } = await import('./npcs/log-watcher/index.js');
326
+ const npc = new LogWatcherNPC({
327
+ ...config,
328
+ files: filesStr.split(','),
329
+ errorPattern: stringFlag(parsed.flags, 'error-pattern'),
330
+ warningPattern: stringFlag(parsed.flags, 'warning-pattern'),
331
+ windowSeconds: numberFlag(parsed.flags, 'window'),
332
+ errorWarn: numberFlag(parsed.flags, 'error-warn'),
333
+ errorCrit: numberFlag(parsed.flags, 'error-crit'),
334
+ reportInterval: numberFlag(parsed.flags, 'report-interval'),
335
+ });
336
+ logger.info('Starting log-watcher NPC');
337
+ await npc.start();
338
+ break;
339
+ }
340
+ case 'receptionist': {
341
+ if (!config.serviceId) {
342
+ config.serviceId = 'receptionist';
343
+ }
344
+ const { ReceptionistNPC } = await import('./npcs/receptionist/index.js');
345
+ const npc = new ReceptionistNPC({
346
+ ...config,
347
+ pollInterval: numberFlag(parsed.flags, 'poll-interval'),
348
+ lat: numberFlag(parsed.flags, 'lat'),
349
+ lon: numberFlag(parsed.flags, 'lon'),
350
+ location: stringFlag(parsed.flags, 'location'),
351
+ fahrenheit: boolFlag(parsed.flags, 'fahrenheit') ?? false,
352
+ });
353
+ logger.info('Starting receptionist NPC');
354
+ await npc.start();
355
+ break;
356
+ }
357
+ case 'github-watcher': {
358
+ const repo = stringFlag(parsed.flags, 'repo');
359
+ if (!repo) {
360
+ console.error('Error: --repo=<owner/name> is required');
361
+ process.exit(1);
362
+ }
363
+ if (!config.serviceId) {
364
+ const shortName = repo.split('/').pop() || repo;
365
+ config.serviceId = `github-${shortName.replace(/[^a-zA-Z0-9_-]/g, '-')}`.slice(0, 100);
366
+ }
367
+ const { GitHubWatcherNPC } = await import('./npcs/github-watcher/index.js');
368
+ const npc = new GitHubWatcherNPC({
369
+ ...config,
370
+ repo,
371
+ branch: stringFlag(parsed.flags, 'branch'),
372
+ token: stringFlag(parsed.flags, 'token') ?? process.env['GITHUB_TOKEN'],
373
+ pollInterval: numberFlag(parsed.flags, 'poll-interval'),
374
+ });
375
+ logger.info('Starting github-watcher NPC');
376
+ await npc.start();
377
+ break;
378
+ }
379
+ case 'web-page-watcher': {
380
+ const url = stringFlag(parsed.flags, 'url');
381
+ if (!url) {
382
+ console.error('Error: --url=<url> is required');
383
+ process.exit(1);
384
+ }
385
+ const { hostnameOf } = await import('./npcs/web-page-watcher/basic-mode.js');
386
+ const { parseDimensions } = await import('./npcs/web-page-watcher/defaults.js');
387
+ const { WebPageWatcherNPC } = await import('./npcs/web-page-watcher/index.js');
388
+ const parseWxH = (v) => v ? (parseDimensions(v) ?? undefined) : undefined;
389
+ if (!config.serviceId) {
390
+ config.serviceId = `webpage-${hostnameOf(url)}`
391
+ .replace(/[^a-zA-Z0-9_.-]/g, '-')
392
+ .slice(0, 100);
393
+ }
394
+ const modeFlag = stringFlag(parsed.flags, 'mode');
395
+ const mode = modeFlag === 'auto' || modeFlag === 'screenshot' || modeFlag === 'basic'
396
+ ? modeFlag
397
+ : undefined;
398
+ const viewport = parseWxH(stringFlag(parsed.flags, 'viewport'));
399
+ const screen = parseWxH(stringFlag(parsed.flags, 'screen'));
400
+ const npc = new WebPageWatcherNPC({
401
+ ...config,
402
+ url,
403
+ mode,
404
+ chromePath: stringFlag(parsed.flags, 'chrome-path'),
405
+ waitUntil: stringFlag(parsed.flags, 'wait-until'),
406
+ viewportWidth: viewport?.width,
407
+ viewportHeight: viewport?.height,
408
+ screenWidth: screen?.width,
409
+ screenHeight: screen?.height,
410
+ pollInterval: numberFlag(parsed.flags, 'interval') ?? numberFlag(parsed.flags, 'poll-interval'),
411
+ });
412
+ logger.info('Starting web-page-watcher NPC');
413
+ await npc.start();
414
+ break;
415
+ }
416
+ default:
417
+ console.error(`Error: Unknown NPC type "${parsed.command}"\n`);
418
+ console.log(USAGE);
419
+ process.exit(1);
420
+ }
421
+ }
422
+ async function runInit(flags, config) {
423
+ const { saveUserConfig, loadUserConfig, maskKey } = await import('./core/user-config.js');
424
+ let apiKey = stringFlag(flags, 'api-key');
425
+ const apiUrl = stringFlag(flags, 'api-url');
426
+ const map = stringFlag(flags, 'map');
427
+ if (!apiKey && !apiUrl && !map) {
428
+ const saved = loadUserConfig();
429
+ if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
430
+ console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
431
+ console.log(`Current: api ${saved.apiUrl ?? config.apiUrl} key ${maskKey(saved.apiKey)}`);
432
+ return;
433
+ }
434
+ const { resolveApiKey } = await import('./core/api-key.js');
435
+ const resolved = await resolveApiKey({ flags, savedKey: undefined });
436
+ if (!resolved.ok) {
437
+ console.error(resolved.error);
438
+ process.exitCode = 1;
439
+ return;
440
+ }
441
+ apiKey = resolved.key;
442
+ }
443
+ const path = saveUserConfig({ apiKey, apiUrl, map });
444
+ const saved = loadUserConfig();
445
+ console.log(`✓ Saved ${path} (mode 0600)`);
446
+ console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
447
+ console.log(' Next: nocville-npc setup (hooks up installed coding agents)');
448
+ }
449
+ /**
450
+ * Resolve the API key for a setup-style command. Returns the key to save, or
451
+ * `undefined` when the already-saved key should be kept, or `null` (after
452
+ * printing an actionable error and setting a non-zero exit code) when none.
453
+ */
454
+ async function resolveSetupKey(flags) {
455
+ const { resolveApiKey } = await import('./core/api-key.js');
456
+ const resolved = await resolveApiKey({ flags });
457
+ if (!resolved.ok) {
458
+ console.error(resolved.error);
459
+ process.exitCode = 1;
460
+ return null;
461
+ }
462
+ return resolved.source === 'saved' ? undefined : resolved.key;
463
+ }
464
+ /** `setup`: save credentials, then install Claude Code + Codex hooks where present. */
465
+ /** After setup: make sure sessions.json exists and tell the user how to filter sessions. */
466
+ function announceSessionFilter() {
467
+ const { path, created } = ensureSessionFilterTemplate();
468
+ console.log(`\n Choose which sessions appear in your office: ${created ? 'created' : 'edit'} ${tildify(path)}\n` +
469
+ ' (allow/deny by project folder or session name; empty = all sessions publish).\n' +
470
+ ' Check the result with: npx @nocville/npc sessions');
471
+ }
472
+ function tildify(p) {
473
+ const home = homedir();
474
+ return p.startsWith(home) ? '~' + p.slice(home.length) : p;
475
+ }
476
+ /** `nocville-npc sessions`: show the allow/deny rules and test a folder / session name. */
477
+ function runSessionsCommand(flags) {
478
+ const { path, created } = ensureSessionFilterTemplate();
479
+ const filter = loadSessionFilter(path);
480
+ console.log(`Session filter: ${tildify(path)}${created ? ' (created an empty template)' : ''}`);
481
+ if (filter.error) {
482
+ console.log(` ✗ ${filter.error}\n Nothing publishes until the file is fixed.`);
483
+ process.exitCode = 1;
484
+ return;
485
+ }
486
+ const show = (label, list) => console.log(` ${label.padEnd(16)} ${list.length ? list.map((p) => JSON.stringify(p)).join(', ') : '(none)'}`);
487
+ show('allow projects:', filter.allow.projects);
488
+ show('allow sessions:', filter.allow.sessions);
489
+ show('deny projects:', filter.deny.projects);
490
+ show('deny sessions:', filter.deny.sessions);
491
+ const projectDir = stringFlag(flags, 'project') ?? process.cwd();
492
+ const name = stringFlag(flags, 'name');
493
+ const verdict = sessionPublishDecision(filter, { projectDir, name });
494
+ const subject = `Sessions in ${tildify(projectDir)}${name ? ` named "${name}"` : ''}`;
495
+ console.log(`\n${verdict.publish ? '✓' : '✗'} ${subject} ${verdict.publish ? 'publish' : 'do NOT publish'} — ${verdict.reason}.`);
496
+ }
497
+ async function runSetupAll(flags) {
498
+ const { setupHooks } = await import('./npcs/claude-code/index.js');
499
+ const { setupCodexHooks } = await import('./npcs/codex/index.js');
500
+ const { codexHome } = await import('./npcs/codex/defaults.js');
501
+ const opts = {
502
+ apiUrl: stringFlag(flags, 'api-url') ?? process.env['NOCVILLE_API_URL'],
503
+ apiKey: stringFlag(flags, 'api-key') ?? process.env['NOCVILLE_API_KEY'],
504
+ map: stringFlag(flags, 'map'),
505
+ cliPath: process.argv[1],
506
+ };
507
+ let any = false;
508
+ if (existsSync(join(homedir(), '.claude'))) {
509
+ any = setupHooks(opts) || any;
510
+ }
511
+ else {
512
+ console.log('· Claude Code not found (~/.claude missing) — skipped');
513
+ }
514
+ if (existsSync(codexHome())) {
515
+ any = setupCodexHooks(opts) || any;
516
+ }
517
+ else {
518
+ console.log('· Codex not found (~/.codex missing) — skipped');
519
+ }
520
+ if (existsSync(join(homedir(), '.gemini'))) {
521
+ const { setupGeminiHooks } = await import('./npcs/gemini/index.js');
522
+ any = setupGeminiHooks(opts) || any;
523
+ }
524
+ else {
525
+ console.log('· Gemini CLI not found (~/.gemini missing) — skipped');
526
+ }
527
+ if (existsSync(join(homedir(), '.cursor'))) {
528
+ const { setupCursorHooks } = await import('./npcs/cursor/index.js');
529
+ any = setupCursorHooks(opts) || any;
530
+ }
531
+ else {
532
+ console.log('· Cursor not found (~/.cursor missing) — skipped');
533
+ }
534
+ if (!any) {
535
+ console.error('Nothing to set up: install Claude Code, Codex, Gemini CLI or Cursor first.');
536
+ process.exitCode = 1;
537
+ }
538
+ }
539
+ main().catch((err) => {
540
+ const parsed = parseArgs(process.argv);
541
+ if (['gemini', 'cursor'].includes(parsed.command ?? '') &&
542
+ (!parsed.subcommand || parsed.subcommand === 'hook')) {
543
+ process.stdout.write('{}\n');
544
+ return;
545
+ }
546
+ console.error('Fatal error:', err);
547
+ process.exit(1);
548
+ });
549
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * API-key resolution for the setup-style commands (`setup`, `init`,
3
+ * `claude-code setup`, `codex setup`): `--api-key` > `NOCVILLE_API_KEY` >
4
+ * the already-saved config > a hidden interactive prompt (TTY only) > error.
5
+ * The prompt lets users paste the key without it landing in shell history.
6
+ */
7
+ export type ApiKeySource = 'flag' | 'env' | 'saved' | 'prompt';
8
+ export interface ResolveApiKeyOptions {
9
+ flags: Record<string, string | boolean>;
10
+ env?: NodeJS.ProcessEnv;
11
+ /** Key from ~/.config/nocville/config.json (default: read it). */
12
+ savedKey?: string;
13
+ /** Whether stdin is interactive (default: process.stdin.isTTY). */
14
+ isTTY?: boolean;
15
+ /** Prompt implementation (default: hidden readline prompt). Injectable for tests. */
16
+ prompt?: (question: string) => Promise<string>;
17
+ }
18
+ export type ResolveApiKeyResult = {
19
+ ok: true;
20
+ key: string;
21
+ source: ApiKeySource;
22
+ } | {
23
+ ok: false;
24
+ error: string;
25
+ };
26
+ export declare const API_KEY_PROMPT = "Paste your Nocville API key: ";
27
+ export declare const MISSING_API_KEY_ERROR: string;
28
+ export declare function resolveApiKey(opts: ResolveApiKeyOptions): Promise<ResolveApiKeyResult>;
29
+ /**
30
+ * Ask a question on the terminal without echoing the answer. The question is
31
+ * written directly; readline's own echo goes to a muted stream.
32
+ */
33
+ export declare function promptHidden(question: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<string>;
34
+ //# sourceMappingURL=api-key.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { Writable } from 'node:stream';
3
+ import { stringFlag } from './config.js';
4
+ import { loadUserConfig } from './user-config.js';
5
+ export const API_KEY_PROMPT = 'Paste your Nocville API key: ';
6
+ export const MISSING_API_KEY_ERROR = 'Error: no API key. Pass --api-key=<agent key>, set NOCVILLE_API_KEY, or run this ' +
7
+ 'command in an interactive terminal to paste the key (input is hidden). ' +
8
+ 'Your team admin can generate a key in Nocville team settings.';
9
+ export async function resolveApiKey(opts) {
10
+ const fromFlag = stringFlag(opts.flags, 'api-key')?.trim();
11
+ if (fromFlag)
12
+ return { ok: true, key: fromFlag, source: 'flag' };
13
+ const fromEnv = (opts.env ?? process.env)['NOCVILLE_API_KEY']?.trim();
14
+ if (fromEnv)
15
+ return { ok: true, key: fromEnv, source: 'env' };
16
+ const saved = 'savedKey' in opts ? opts.savedKey : loadUserConfig().apiKey;
17
+ if (saved)
18
+ return { ok: true, key: saved, source: 'saved' };
19
+ const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
20
+ if (!isTTY)
21
+ return { ok: false, error: MISSING_API_KEY_ERROR };
22
+ let answer;
23
+ try {
24
+ answer = (await (opts.prompt ?? promptHidden)(API_KEY_PROMPT)).trim();
25
+ }
26
+ catch {
27
+ return { ok: false, error: 'Cancelled — no API key entered.' };
28
+ }
29
+ if (!answer)
30
+ return { ok: false, error: MISSING_API_KEY_ERROR };
31
+ return { ok: true, key: answer, source: 'prompt' };
32
+ }
33
+ /**
34
+ * Ask a question on the terminal without echoing the answer. The question is
35
+ * written directly; readline's own echo goes to a muted stream.
36
+ */
37
+ export function promptHidden(question, input = process.stdin, output = process.stdout) {
38
+ return new Promise((resolve, reject) => {
39
+ const muted = new Writable({
40
+ write(_chunk, _enc, cb) {
41
+ cb();
42
+ },
43
+ });
44
+ output.write(question);
45
+ const rl = createInterface({ input, output: muted, terminal: true });
46
+ let done = false;
47
+ rl.on('line', (line) => {
48
+ done = true;
49
+ rl.close();
50
+ output.write('\n');
51
+ resolve(line);
52
+ });
53
+ rl.on('SIGINT', () => {
54
+ rl.close();
55
+ });
56
+ rl.on('close', () => {
57
+ if (!done) {
58
+ output.write('\n');
59
+ reject(new Error('cancelled'));
60
+ }
61
+ });
62
+ });
63
+ }
64
+ //# sourceMappingURL=api-key.js.map