@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,90 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { maskKey, persistCredentials } from '../../core/user-config.js';
4
+ import { buildShimHookCommand, stableNodePath, readJsonFile, removeHookGroups, resolveCliPath, writeJsonFile, } from '../agent-common/hook-settings.js';
5
+ import { installHookShim } from '../agent-common/shim.js';
6
+ import { ensureStableCliPath } from '../agent-common/runtime.js';
7
+ import { CURSOR_DEFAULTS, cursorHome } from './defaults.js';
8
+ /** Telemetry hooks only; model output and prompt text never enter Nocville. */
9
+ export const CURSOR_HOOK_EVENTS = [
10
+ 'sessionStart',
11
+ 'sessionEnd',
12
+ 'beforeSubmitPrompt',
13
+ 'postToolUse',
14
+ 'postToolUseFailure',
15
+ 'afterFileEdit',
16
+ 'stop',
17
+ 'subagentStop',
18
+ 'preCompact',
19
+ ];
20
+ export function cursorSettingsPath() {
21
+ return join(cursorHome(), 'hooks.json');
22
+ }
23
+ /** Install the nocville group into every Cursor event. Mutates `file`. */
24
+ export function installCursorHooksInto(file, command, opts = {}) {
25
+ const migrated = removeHookGroups(file);
26
+ file.version = 1;
27
+ file.hooks ??= {};
28
+ for (const event of CURSOR_HOOK_EVENTS) {
29
+ const existing = Array.isArray(file.hooks[event]) ? file.hooks[event] : [];
30
+ file.hooks[event] = [
31
+ ...existing,
32
+ { command, timeout: opts.timeout ?? CURSOR_DEFAULTS.hookTimeoutSeconds },
33
+ ];
34
+ }
35
+ return { migrated, installed: CURSOR_HOOK_EVENTS.length };
36
+ }
37
+ export function buildCursorHookCommand(_cliPath) {
38
+ return buildShimHookCommand('cursor');
39
+ }
40
+ function out(opts, msg) {
41
+ if (!opts.quiet)
42
+ console.log(msg);
43
+ }
44
+ /** Merge into Cursor settings, preserving user hooks and a one-time backup. */
45
+ export function setupCursorHooks(opts = {}) {
46
+ const settingsPath = opts.settingsPath ?? cursorSettingsPath();
47
+ const dir = join(settingsPath, '..');
48
+ if (!existsSync(dir)) {
49
+ console.error(`${dir} does not exist — is Cursor installed? Skipping Cursor setup.`);
50
+ return false;
51
+ }
52
+ const file = readJsonFile(settingsPath);
53
+ if (file === null ||
54
+ (file.version !== undefined && file.version !== 1) ||
55
+ (file.hooks !== undefined &&
56
+ (!file.hooks || typeof file.hooks !== 'object' || Array.isArray(file.hooks)))) {
57
+ console.error(`Unsupported or invalid Cursor hook settings in ${settingsPath}; no settings changed.`);
58
+ return false;
59
+ }
60
+ const creds = persistCredentials(opts);
61
+ const resolved = resolveCliPath(opts.cliPath);
62
+ const cliPath = ensureStableCliPath(resolved);
63
+ installHookShim(stableNodePath(), cliPath);
64
+ const { installed } = installCursorHooksInto(file, buildCursorHookCommand(cliPath), {
65
+ timeout: CURSOR_DEFAULTS.hookTimeoutSeconds,
66
+ });
67
+ writeJsonFile(settingsPath, file);
68
+ out(opts, `✓ Cursor: installed Nocville hooks for ${installed} events in ${settingsPath}`);
69
+ out(opts, ` Hook runs: ${process.execPath} ${cliPath}`);
70
+ out(opts, ` API: ${creds.apiUrl ?? 'https://nocville.com'} key: ${maskKey(creds.apiKey)}`);
71
+ if (!creds.apiKey) {
72
+ out(opts, ' ⚠ No API key saved yet — run: nocville-npc init --api-key=<your team key>');
73
+ }
74
+ return true;
75
+ }
76
+ /** Remove all Nocville hooks from `~/.cursor/hooks.json`. */
77
+ export function uninstallCursorHooks(opts = {}) {
78
+ const settingsPath = opts.settingsPath ?? cursorSettingsPath();
79
+ const file = readJsonFile(settingsPath);
80
+ if (!file || !existsSync(settingsPath)) {
81
+ out(opts, 'Cursor: no hooks.json — nothing to remove.');
82
+ return 0;
83
+ }
84
+ const removed = removeHookGroups(file);
85
+ if (removed > 0)
86
+ writeJsonFile(settingsPath, file);
87
+ out(opts, `✓ Cursor: removed ${removed} Nocville hook(s) from ${settingsPath}`);
88
+ return removed;
89
+ }
90
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,9 @@
1
+ export declare const GEMINI_DEFAULTS: {
2
+ stateDir: string;
3
+ stateFileMaxAge: number;
4
+ heartbeatSeconds: number;
5
+ minPushGapMs: number;
6
+ hookTimeoutMs: number;
7
+ };
8
+ export declare const geminiHome: () => string;
9
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { homedir, tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export const GEMINI_DEFAULTS = {
4
+ stateDir: join(tmpdir(), 'nocville-gemini'),
5
+ stateFileMaxAge: 24 * 60 * 60 * 1000,
6
+ heartbeatSeconds: 90,
7
+ minPushGapMs: 1500,
8
+ hookTimeoutMs: 5000,
9
+ };
10
+ export const geminiHome = () => join(homedir(), '.gemini');
11
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,10 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ import type { AgentHookEvent, AgentHookSpec, HookRunOptions } from '../agent-common/hook-runner.js';
3
+ import type { AgentAdapter, AgentSessionState, AgentTransition } from '../agent-common/session.js';
4
+ export declare const GEMINI_ADAPTER: AgentAdapter;
5
+ export declare function geminiServiceId(sessionId: string, cwd: string): string;
6
+ /** Only phase and tool metadata are used; prompts/responses/transcripts are never read. */
7
+ export declare function geminiTransition(event: AgentHookEvent, prev: AgentSessionState | null): AgentTransition;
8
+ export declare const GEMINI_HOOK_SPEC: AgentHookSpec;
9
+ export declare function handleGeminiHookEvent(config: NpcConfig, rawInput?: string, opts?: HookRunOptions): Promise<void>;
10
+ //# sourceMappingURL=hook-handler.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { THINKING, describeToolUse, failureText } from '../agent-common/activity.js';
2
+ import { hookEventName, readStdin, runAgentHook } from '../agent-common/hook-runner.js';
3
+ import { projectNameOf, sanitizeServiceId } from '../agent-common/session.js';
4
+ import { GEMINI_DEFAULTS } from './defaults.js';
5
+ export const GEMINI_ADAPTER = {
6
+ command: 'gemini',
7
+ label: 'Gemini',
8
+ family: 'gemini',
9
+ stateDir: GEMINI_DEFAULTS.stateDir,
10
+ heartbeatSeconds: GEMINI_DEFAULTS.heartbeatSeconds,
11
+ processNames: ['gemini', 'node'],
12
+ nodeCommandPattern: /[\/\\](?:@google[\/\\])?gemini(?:-cli[\/\\]|(?:\.[cm]?js)?(?:\s|$))/i,
13
+ };
14
+ export function geminiServiceId(sessionId, cwd) {
15
+ return sanitizeServiceId(`gemini-${projectNameOf(cwd)}-${sessionId.slice(0, 8)}`);
16
+ }
17
+ const idle = (activity) => ({
18
+ state: 'IDLE',
19
+ activity,
20
+ status: 'healthy',
21
+ mood: 'idle',
22
+ });
23
+ const working = (activity, warning = false) => ({
24
+ state: 'WORKING',
25
+ activity,
26
+ status: warning ? 'warning' : 'healthy',
27
+ mood: warning ? 'concerned' : 'happy',
28
+ });
29
+ /** Only phase and tool metadata are used; prompts/responses/transcripts are never read. */
30
+ export function geminiTransition(event, prev) {
31
+ const cwd = typeof event.cwd === 'string' ? event.cwd : prev?.projectDir;
32
+ switch (hookEventName(event)) {
33
+ case 'SessionStart':
34
+ return idle('Ready for a task');
35
+ case 'BeforeAgent':
36
+ return working(THINKING);
37
+ case 'BeforeTool':
38
+ return working(describeToolUse(event.tool_name, event.tool_input, cwd));
39
+ case 'AfterTool': {
40
+ const response = event['tool_response'];
41
+ if (response && typeof response === 'object' && 'error' in response && response.error)
42
+ return working(failureText(event.tool_name), true);
43
+ return { ...working(), quiet: prev?.state === 'WORKING' };
44
+ }
45
+ case 'Notification':
46
+ if (event['notification_type'] === 'ToolPermission')
47
+ return {
48
+ state: 'WAITING',
49
+ status: 'warning',
50
+ mood: 'concerned',
51
+ activity: 'Needs your approval',
52
+ };
53
+ break;
54
+ case 'AfterAgent':
55
+ return idle('Done — waiting for next task');
56
+ case 'PreCompress':
57
+ return working('Compacting context');
58
+ case 'SessionEnd':
59
+ return {
60
+ state: 'IDLE',
61
+ activity: 'Session ended',
62
+ status: 'unknown',
63
+ mood: 'idle',
64
+ offline: true,
65
+ };
66
+ }
67
+ return {
68
+ state: prev?.state ?? 'IDLE',
69
+ status: prev?.status ?? 'healthy',
70
+ mood: prev?.mood ?? 'idle',
71
+ quiet: true,
72
+ };
73
+ }
74
+ export const GEMINI_HOOK_SPEC = {
75
+ adapter: GEMINI_ADAPTER,
76
+ serviceIdFor: geminiServiceId,
77
+ transition: geminiTransition,
78
+ stdoutOnSuccess: '{}',
79
+ statsEvents: ['AfterAgent'],
80
+ };
81
+ export async function handleGeminiHookEvent(config, rawInput, opts = {}) {
82
+ await runAgentHook(GEMINI_HOOK_SPEC, config, rawInput ?? readStdin(), {
83
+ minPushGapMs: GEMINI_DEFAULTS.minPushGapMs,
84
+ ...opts,
85
+ });
86
+ }
87
+ //# sourceMappingURL=hook-handler.js.map
@@ -0,0 +1,17 @@
1
+ import type { NpcConfig } from '../../core/types.js';
2
+ export { GEMINI_DEFAULTS } from './defaults.js';
3
+ export { GEMINI_ADAPTER, geminiTransition, geminiServiceId, handleGeminiHookEvent, } from './hook-handler.js';
4
+ export { setupGeminiHooks, uninstallGeminiHooks, installGeminiHooksInto, GEMINI_HOOK_EVENTS, } from './setup.js';
5
+ export type { GeminiSetupOptions } from './setup.js';
6
+ export interface GeminiSubcommandOptions {
7
+ subcommand: string;
8
+ config: NpcConfig;
9
+ flags: Record<string, string | boolean>;
10
+ args?: string[];
11
+ }
12
+ /**
13
+ * gemini subcommands: setup | uninstall | hook | flush | watchdog | cleanup.
14
+ * `hook` always emits {}; detached helpers stay silent.
15
+ */
16
+ export declare function handleGeminiCommand(opts: GeminiSubcommandOptions): Promise<void>;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,56 @@
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 { GEMINI_DEFAULTS } from './defaults.js';
6
+ import { GEMINI_ADAPTER, handleGeminiHookEvent } from './hook-handler.js';
7
+ import { setupGeminiHooks, uninstallGeminiHooks } from './setup.js';
8
+ export { GEMINI_DEFAULTS } from './defaults.js';
9
+ export { GEMINI_ADAPTER, geminiTransition, geminiServiceId, handleGeminiHookEvent, } from './hook-handler.js';
10
+ export { setupGeminiHooks, uninstallGeminiHooks, installGeminiHooksInto, GEMINI_HOOK_EVENTS, } from './setup.js';
11
+ /**
12
+ * gemini subcommands: setup | uninstall | hook | flush | watchdog | cleanup.
13
+ * `hook` always emits {}; detached helpers stay silent.
14
+ */
15
+ export async function handleGeminiCommand(opts) {
16
+ const { subcommand, config, flags } = opts;
17
+ switch (subcommand) {
18
+ case 'setup': {
19
+ if (flags['uninstall']) {
20
+ uninstallGeminiHooks();
21
+ break;
22
+ }
23
+ const ok = setupGeminiHooks({
24
+ apiUrl: stringFlag(flags, 'api-url') ?? process.env['NOCVILLE_API_URL'],
25
+ apiKey: stringFlag(flags, 'api-key') ?? process.env['NOCVILLE_API_KEY'],
26
+ map: stringFlag(flags, 'map'),
27
+ cliPath: process.argv[1],
28
+ });
29
+ if (!ok)
30
+ process.exitCode = 1;
31
+ break;
32
+ }
33
+ case 'uninstall':
34
+ uninstallGeminiHooks();
35
+ break;
36
+ case 'hook':
37
+ await handleGeminiHookEvent(config, readStdin(), { mode: 'detach' });
38
+ break;
39
+ case 'watchdog':
40
+ await runWatchdog(GEMINI_ADAPTER, config, opts.args?.[0]);
41
+ break;
42
+ case 'flush':
43
+ await runFlushCommand(GEMINI_ADAPTER, config, opts.args?.[0], numberFlag(flags, 'delay'), flags['stats'] === true);
44
+ break;
45
+ case 'cleanup': {
46
+ const removed = await cleanupSessionStates(GEMINI_DEFAULTS.stateDir, GEMINI_DEFAULTS.stateFileMaxAge);
47
+ console.log(`Cleanup complete: ${removed} stale file(s) removed`);
48
+ break;
49
+ }
50
+ default:
51
+ console.error(`Unknown gemini subcommand: ${subcommand}`);
52
+ console.error('Available subcommands: setup, uninstall, hook, cleanup');
53
+ process.exitCode = 1;
54
+ }
55
+ }
56
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ import type { HooksFile } from '../agent-common/hook-settings.js';
2
+ /** Telemetry hooks only; model output and prompt text never enter Nocville. */
3
+ export declare const GEMINI_HOOK_EVENTS: readonly ["SessionStart", "SessionEnd", "BeforeAgent", "BeforeTool", "AfterTool", "Notification", "AfterAgent", "PreCompress"];
4
+ export declare function geminiSettingsPath(): string;
5
+ export interface GeminiSetupOptions {
6
+ apiUrl?: string;
7
+ apiKey?: string;
8
+ map?: string;
9
+ cliPath?: string;
10
+ /** Override ~/.gemini/settings.json (tests). */
11
+ settingsPath?: string;
12
+ quiet?: boolean;
13
+ }
14
+ /** Install the nocville group into every Gemini event. Mutates `file`. */
15
+ export declare function installGeminiHooksInto(file: HooksFile, command: string, opts?: {
16
+ timeout?: number;
17
+ }): {
18
+ migrated: number;
19
+ installed: number;
20
+ };
21
+ export declare function buildGeminiHookCommand(_cliPath: string): string;
22
+ /** Merge into Gemini settings, preserving user hooks and a one-time backup. */
23
+ export declare function setupGeminiHooks(opts?: GeminiSetupOptions): boolean;
24
+ /** Remove all Nocville hooks from `~/.gemini/settings.json`. */
25
+ export declare function uninstallGeminiHooks(opts?: {
26
+ settingsPath?: string;
27
+ quiet?: boolean;
28
+ }): number;
29
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { maskKey, persistCredentials } from '../../core/user-config.js';
4
+ import { buildShimHookCommand, stableNodePath, installHookGroups, readJsonFile, removeHookGroups, resolveCliPath, writeJsonFile, } from '../agent-common/hook-settings.js';
5
+ import { installHookShim } from '../agent-common/shim.js';
6
+ import { ensureStableCliPath } from '../agent-common/runtime.js';
7
+ import { GEMINI_DEFAULTS, geminiHome } from './defaults.js';
8
+ /** Telemetry hooks only; model output and prompt text never enter Nocville. */
9
+ export const GEMINI_HOOK_EVENTS = [
10
+ 'SessionStart',
11
+ 'SessionEnd',
12
+ 'BeforeAgent',
13
+ 'BeforeTool',
14
+ 'AfterTool',
15
+ 'Notification',
16
+ 'AfterAgent',
17
+ 'PreCompress',
18
+ ];
19
+ export function geminiSettingsPath() {
20
+ return join(geminiHome(), 'settings.json');
21
+ }
22
+ /** Install the nocville group into every Gemini event. Mutates `file`. */
23
+ export function installGeminiHooksInto(file, command, opts = {}) {
24
+ const result = installHookGroups(file, command, GEMINI_HOOK_EVENTS, {
25
+ perEvent: Object.fromEntries(GEMINI_HOOK_EVENTS.map((event) => [
26
+ event,
27
+ {
28
+ name: 'nocville',
29
+ timeout: opts.timeout ?? GEMINI_DEFAULTS.hookTimeoutMs,
30
+ },
31
+ ])),
32
+ });
33
+ // Omit the optional matcher so lifecycle sources/reasons are all observed too.
34
+ for (const event of GEMINI_HOOK_EVENTS) {
35
+ const group = file.hooks[event].at(-1);
36
+ delete group.matcher;
37
+ }
38
+ return result;
39
+ }
40
+ export function buildGeminiHookCommand(_cliPath) {
41
+ return buildShimHookCommand('gemini');
42
+ }
43
+ function out(opts, msg) {
44
+ if (!opts.quiet)
45
+ console.log(msg);
46
+ }
47
+ /** Merge into Gemini settings, preserving user hooks and a one-time backup. */
48
+ export function setupGeminiHooks(opts = {}) {
49
+ const settingsPath = opts.settingsPath ?? geminiSettingsPath();
50
+ const dir = join(settingsPath, '..');
51
+ if (!existsSync(dir)) {
52
+ console.error(`${dir} does not exist — is Gemini installed? Skipping Gemini setup.`);
53
+ return false;
54
+ }
55
+ const file = readJsonFile(settingsPath);
56
+ if (file === null) {
57
+ console.error(`Could not parse ${settingsPath}; fix it (or move it aside) and re-run setup.`);
58
+ return false;
59
+ }
60
+ const creds = persistCredentials(opts);
61
+ const resolved = resolveCliPath(opts.cliPath);
62
+ const cliPath = ensureStableCliPath(resolved);
63
+ installHookShim(stableNodePath(), cliPath);
64
+ const { installed } = installGeminiHooksInto(file, buildGeminiHookCommand(cliPath), {
65
+ timeout: GEMINI_DEFAULTS.hookTimeoutMs,
66
+ });
67
+ writeJsonFile(settingsPath, file);
68
+ out(opts, `✓ Gemini: installed Nocville hooks for ${installed} events in ${settingsPath}`);
69
+ out(opts, ` Hook runs: ${process.execPath} ${cliPath}`);
70
+ out(opts, ` API: ${creds.apiUrl ?? 'https://nocville.com'} key: ${maskKey(creds.apiKey)}`);
71
+ if (!creds.apiKey) {
72
+ out(opts, ' ⚠ No API key saved yet — run: nocville-npc init --api-key=<your team key>');
73
+ }
74
+ return true;
75
+ }
76
+ /** Remove all Nocville hooks from `~/.gemini/settings.json`. */
77
+ export function uninstallGeminiHooks(opts = {}) {
78
+ const settingsPath = opts.settingsPath ?? geminiSettingsPath();
79
+ const file = readJsonFile(settingsPath);
80
+ if (!file || !existsSync(settingsPath)) {
81
+ out(opts, 'Gemini: no settings.json — nothing to remove.');
82
+ return 0;
83
+ }
84
+ const removed = removeHookGroups(file);
85
+ if (removed > 0)
86
+ writeJsonFile(settingsPath, file);
87
+ out(opts, `✓ Gemini: removed ${removed} Nocville hook(s) from ${settingsPath}`);
88
+ return removed;
89
+ }
90
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,8 @@
1
+ export declare const DEFAULTS: {
2
+ pollInterval: number;
3
+ fetchTimeoutMs: number;
4
+ perPage: number;
5
+ /** Polls to keep the 'new-commit' sub-status active after a fresh commit. */
6
+ newCommitPolls: number;
7
+ };
8
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1,8 @@
1
+ export const DEFAULTS = {
2
+ pollInterval: 60000, // 60s
3
+ fetchTimeoutMs: 5000,
4
+ perPage: 10,
5
+ /** Polls to keep the 'new-commit' sub-status active after a fresh commit. */
6
+ newCommitPolls: 2,
7
+ };
8
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1,3 @@
1
+ /** Short relative-time string like "3m ago", "2h ago", "1d ago". */
2
+ export declare function relativeTime(iso: string, now?: number): string;
3
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1,18 @@
1
+ /** Short relative-time string like "3m ago", "2h ago", "1d ago". */
2
+ export function relativeTime(iso, now = Date.now()) {
3
+ const t = Date.parse(iso);
4
+ if (!Number.isFinite(t))
5
+ return 'unknown';
6
+ const secs = Math.max(0, Math.round((now - t) / 1000));
7
+ if (secs < 60)
8
+ return `${secs}s ago`;
9
+ const mins = Math.round(secs / 60);
10
+ if (mins < 60)
11
+ return `${mins}m ago`;
12
+ const hrs = Math.round(mins / 60);
13
+ if (hrs < 24)
14
+ return `${hrs}h ago`;
15
+ const days = Math.round(hrs / 24);
16
+ return `${days}d ago`;
17
+ }
18
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,55 @@
1
+ export interface CommitInfo {
2
+ sha: string;
3
+ author: string;
4
+ message: string;
5
+ date: string;
6
+ }
7
+ export interface FetchCommitsResult {
8
+ /** 304 Not Modified — the ETag matched, nothing changed, no rate-limit cost. */
9
+ notModified: boolean;
10
+ commits: CommitInfo[];
11
+ etag: string | null;
12
+ rateRemaining: number | null;
13
+ status: number;
14
+ }
15
+ /** Minimal fetch signature so tests can inject a stub. */
16
+ export type FetchFn = (url: string, init?: {
17
+ headers?: Record<string, string>;
18
+ signal?: AbortSignal;
19
+ }) => Promise<{
20
+ ok: boolean;
21
+ status: number;
22
+ headers: {
23
+ get(name: string): string | null;
24
+ };
25
+ json(): Promise<unknown>;
26
+ text(): Promise<string>;
27
+ }>;
28
+ export interface FetchOptions {
29
+ token?: string;
30
+ etag?: string | null;
31
+ branch?: string;
32
+ timeoutMs?: number;
33
+ perPage?: number;
34
+ /** Injectable fetch (defaults to the global fetch). */
35
+ fetchFn?: FetchFn;
36
+ }
37
+ /** Look up a repo's default branch (one call at startup). Null on failure. */
38
+ export declare function fetchDefaultBranch(repo: string, opts?: FetchOptions): Promise<string | null>;
39
+ /**
40
+ * Fetch recent commits from the GitHub REST API using conditional requests.
41
+ * Passing the previous ETag yields a 304 (which does not count against the
42
+ * unauthenticated 60/hour limit) when nothing has changed. Never throws —
43
+ * network errors surface as `status: 0`.
44
+ */
45
+ export declare function fetchCommits(repo: string, opts?: FetchOptions): Promise<FetchCommitsResult>;
46
+ /** Count how many commits landed within the last `hours`. */
47
+ export declare function commitsWithin(commits: CommitInfo[], hours: number, now?: number): number;
48
+ /**
49
+ * Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
50
+ * present in the window (more than a page of new commits, or rewritten history)
51
+ * every fetched commit is treated as new. An undefined sha means "not seeded
52
+ * yet" — returns empty so the caller can seed without a spurious announcement.
53
+ */
54
+ export declare function newCommitsSince(commits: CommitInfo[], lastSeenSha: string | null): CommitInfo[];
55
+ //# sourceMappingURL=github-api.d.ts.map
@@ -0,0 +1,120 @@
1
+ function toNum(v) {
2
+ if (v === null)
3
+ return null;
4
+ const n = Number(v);
5
+ return Number.isNaN(n) ? null : n;
6
+ }
7
+ function ghHeaders(opts) {
8
+ const headers = {
9
+ Accept: 'application/vnd.github+json',
10
+ 'User-Agent': 'nocville-npc',
11
+ 'X-GitHub-Api-Version': '2022-11-28',
12
+ };
13
+ if (opts.token)
14
+ headers['Authorization'] = `Bearer ${opts.token}`;
15
+ return headers;
16
+ }
17
+ /** Look up a repo's default branch (one call at startup). Null on failure. */
18
+ export async function fetchDefaultBranch(repo, opts = {}) {
19
+ const doFetch = opts.fetchFn ?? globalThis.fetch;
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
22
+ try {
23
+ const res = await doFetch(`https://api.github.com/repos/${repo}`, {
24
+ headers: ghHeaders(opts),
25
+ signal: controller.signal,
26
+ });
27
+ if (!res.ok)
28
+ return null;
29
+ const body = (await res.json());
30
+ return body.default_branch ?? null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ finally {
36
+ clearTimeout(timer);
37
+ }
38
+ }
39
+ /**
40
+ * Fetch recent commits from the GitHub REST API using conditional requests.
41
+ * Passing the previous ETag yields a 304 (which does not count against the
42
+ * unauthenticated 60/hour limit) when nothing has changed. Never throws —
43
+ * network errors surface as `status: 0`.
44
+ */
45
+ export async function fetchCommits(repo, opts = {}) {
46
+ const perPage = opts.perPage ?? 10;
47
+ const doFetch = opts.fetchFn ?? globalThis.fetch;
48
+ let url = `https://api.github.com/repos/${repo}/commits?per_page=${perPage}`;
49
+ if (opts.branch)
50
+ url += `&sha=${encodeURIComponent(opts.branch)}`;
51
+ const headers = ghHeaders(opts);
52
+ if (opts.etag)
53
+ headers['If-None-Match'] = opts.etag;
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5000);
56
+ try {
57
+ const res = await doFetch(url, { headers, signal: controller.signal });
58
+ const rateRemaining = toNum(res.headers.get('x-ratelimit-remaining'));
59
+ const etag = res.headers.get('etag');
60
+ if (res.status === 304) {
61
+ return {
62
+ notModified: true,
63
+ commits: [],
64
+ etag: opts.etag ?? null,
65
+ rateRemaining,
66
+ status: 304,
67
+ };
68
+ }
69
+ if (!res.ok) {
70
+ await res.text().catch(() => undefined);
71
+ return { notModified: false, commits: [], etag, rateRemaining, status: res.status };
72
+ }
73
+ const body = (await res.json());
74
+ const commits = body.map((c) => parseCommit(c));
75
+ return { notModified: false, commits, etag, rateRemaining, status: res.status };
76
+ }
77
+ catch {
78
+ return { notModified: false, commits: [], etag: null, rateRemaining: null, status: 0 };
79
+ }
80
+ finally {
81
+ clearTimeout(timer);
82
+ }
83
+ }
84
+ function parseCommit(c) {
85
+ const commit = c['commit'] ?? {};
86
+ const commitAuthor = commit['author'] ?? {};
87
+ const ghAuthor = c['author'] ?? {};
88
+ const message = String(commit['message'] ?? '').split('\n')[0] ?? '';
89
+ return {
90
+ sha: String(c['sha'] ?? ''),
91
+ author: String(commitAuthor['name'] ?? ghAuthor['login'] ?? 'unknown'),
92
+ message,
93
+ date: String(commitAuthor['date'] ?? ''),
94
+ };
95
+ }
96
+ /** Count how many commits landed within the last `hours`. */
97
+ export function commitsWithin(commits, hours, now = Date.now()) {
98
+ const cutoff = now - hours * 3600_000;
99
+ return commits.filter((c) => {
100
+ const t = Date.parse(c.date);
101
+ return Number.isFinite(t) && t >= cutoff;
102
+ }).length;
103
+ }
104
+ /**
105
+ * Return the commits newer than `lastSeenSha`, newest-first. When the sha isn't
106
+ * present in the window (more than a page of new commits, or rewritten history)
107
+ * every fetched commit is treated as new. An undefined sha means "not seeded
108
+ * yet" — returns empty so the caller can seed without a spurious announcement.
109
+ */
110
+ export function newCommitsSince(commits, lastSeenSha) {
111
+ if (!lastSeenSha)
112
+ return [];
113
+ const idx = commits.findIndex((c) => c.sha === lastSeenSha);
114
+ if (idx === 0)
115
+ return []; // newest is already seen
116
+ if (idx === -1)
117
+ return commits; // fell out of the window — treat all as new
118
+ return commits.slice(0, idx);
119
+ }
120
+ //# sourceMappingURL=github-api.js.map
@@ -0,0 +1,35 @@
1
+ import { PollingNPC } from '../../core/polling-npc.js';
2
+ import type { PollingNpcConfig } from '../../core/polling-npc.js';
3
+ import type { CollectedMetrics, Archetype, CompanionDecl } from '../../core/types.js';
4
+ import type { FetchFn } from './github-api.js';
5
+ export interface GitHubWatcherConfig extends PollingNpcConfig {
6
+ /** "owner/name" */
7
+ repo: string;
8
+ token?: string;
9
+ branch?: string;
10
+ /** Injectable fetch (for tests). */
11
+ fetchFn?: FetchFn;
12
+ }
13
+ export declare class GitHubWatcherNPC extends PollingNPC {
14
+ private repo;
15
+ private token?;
16
+ private branch?;
17
+ private fetchFn?;
18
+ private etag;
19
+ private lastSeenSha;
20
+ private commits;
21
+ private rateRemaining;
22
+ private newCommitPollsLeft;
23
+ constructor(config: GitHubWatcherConfig);
24
+ get name(): string;
25
+ protected get defaultDisplayName(): string;
26
+ get archetype(): Archetype;
27
+ get companion(): CompanionDecl;
28
+ protected onStart(): Promise<void>;
29
+ protected collect(): Promise<CollectedMetrics>;
30
+ private reportNewCommits;
31
+ private reportIdle;
32
+ private report;
33
+ private buildBoard;
34
+ }
35
+ //# sourceMappingURL=index.d.ts.map