@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.
- package/README.md +268 -2
- package/dist/cjs/cli.js +584 -0
- package/dist/cjs/core/api-key.js +69 -0
- package/dist/cjs/core/base-npc.js +160 -0
- package/dist/cjs/core/config.js +108 -0
- package/dist/cjs/core/event-npc.js +14 -0
- package/dist/cjs/core/logger.js +39 -0
- package/dist/cjs/core/polling-npc.js +65 -0
- package/dist/cjs/core/screen.js +28 -0
- package/dist/cjs/core/session-filter.js +150 -0
- package/dist/cjs/core/text.js +15 -0
- package/dist/cjs/core/thresholds.js +48 -0
- package/dist/cjs/core/types.js +10 -0
- package/dist/cjs/core/user-config.js +92 -0
- package/dist/cjs/doctor.js +130 -0
- package/dist/cjs/index.js +95 -0
- package/dist/cjs/npcs/agent-common/activity.js +291 -0
- package/dist/cjs/npcs/agent-common/git-stats.js +71 -0
- package/dist/cjs/npcs/agent-common/hook-runner.js +252 -0
- package/dist/cjs/npcs/agent-common/hook-settings.js +279 -0
- package/dist/cjs/npcs/agent-common/liveness.js +112 -0
- package/dist/cjs/npcs/agent-common/lock.js +99 -0
- package/dist/cjs/npcs/agent-common/runtime.js +154 -0
- package/dist/cjs/npcs/agent-common/session.js +412 -0
- package/dist/cjs/npcs/agent-common/shim.js +96 -0
- package/dist/cjs/npcs/agent-common/watchdog.js +149 -0
- package/dist/cjs/npcs/claude-code/defaults.js +20 -0
- package/dist/cjs/npcs/claude-code/hook-handler.js +139 -0
- package/dist/cjs/npcs/claude-code/index.js +104 -0
- package/dist/cjs/npcs/claude-code/setup.js +133 -0
- package/dist/cjs/npcs/claude-code/state-machine.js +130 -0
- package/dist/cjs/npcs/codex/defaults.js +32 -0
- package/dist/cjs/npcs/codex/hook-handler.js +116 -0
- package/dist/cjs/npcs/codex/index.js +92 -0
- package/dist/cjs/npcs/codex/rollout.js +246 -0
- package/dist/cjs/npcs/codex/setup.js +104 -0
- package/dist/cjs/npcs/codex/watcher.js +361 -0
- package/dist/cjs/npcs/cursor/defaults.js +15 -0
- package/dist/cjs/npcs/cursor/hook-handler.js +65 -0
- package/dist/cjs/npcs/cursor/index.js +72 -0
- package/dist/cjs/npcs/cursor/normalize.js +54 -0
- package/dist/cjs/npcs/cursor/setup.js +98 -0
- package/dist/cjs/npcs/gemini/defaults.js +15 -0
- package/dist/cjs/npcs/gemini/hook-handler.js +93 -0
- package/dist/cjs/npcs/gemini/index.js +69 -0
- package/dist/cjs/npcs/gemini/setup.js +98 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +11 -0
- package/dist/cjs/npcs/github-watcher/format.js +21 -0
- package/dist/cjs/npcs/github-watcher/github-api.js +126 -0
- package/dist/cjs/npcs/github-watcher/index.js +148 -0
- package/dist/cjs/npcs/internet-connectivity/checks.js +92 -0
- package/dist/cjs/npcs/internet-connectivity/defaults.js +22 -0
- package/dist/cjs/npcs/internet-connectivity/index.js +251 -0
- package/dist/cjs/npcs/log-watcher/buffer.js +40 -0
- package/dist/cjs/npcs/log-watcher/defaults.js +12 -0
- package/dist/cjs/npcs/log-watcher/index.js +161 -0
- package/dist/cjs/npcs/log-watcher/tail.js +81 -0
- package/dist/cjs/npcs/process-watcher/defaults.js +9 -0
- package/dist/cjs/npcs/process-watcher/index.js +140 -0
- package/dist/cjs/npcs/process-watcher/process-scanner.js +114 -0
- package/dist/cjs/npcs/receptionist/defaults.js +9 -0
- package/dist/cjs/npcs/receptionist/index.js +120 -0
- package/dist/cjs/npcs/receptionist/weather.js +129 -0
- package/dist/cjs/npcs/system-monitor/collectors.js +68 -0
- package/dist/cjs/npcs/system-monitor/defaults.js +13 -0
- package/dist/cjs/npcs/system-monitor/index.js +105 -0
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +58 -0
- package/dist/cjs/npcs/web-page-watcher/capture.js +80 -0
- package/dist/cjs/npcs/web-page-watcher/defaults.js +31 -0
- package/dist/cjs/npcs/web-page-watcher/index.js +234 -0
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +181 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +549 -0
- package/dist/core/api-key.d.ts +34 -0
- package/dist/core/api-key.js +64 -0
- package/dist/core/base-npc.d.ts +41 -0
- package/dist/core/base-npc.js +156 -0
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +101 -0
- package/dist/core/event-npc.d.ts +8 -0
- package/dist/core/event-npc.js +10 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +35 -0
- package/dist/core/polling-npc.d.ts +18 -0
- package/dist/core/polling-npc.js +61 -0
- package/dist/core/screen.d.ts +11 -0
- package/dist/core/screen.js +24 -0
- package/dist/core/session-filter.d.ts +59 -0
- package/dist/core/session-filter.js +143 -0
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +10 -0
- package/dist/core/thresholds.js +44 -0
- package/dist/core/types.d.ts +60 -0
- package/dist/core/types.js +7 -0
- package/dist/core/user-config.d.ts +43 -0
- package/dist/core/user-config.js +85 -0
- package/dist/doctor.d.ts +8 -0
- package/dist/doctor.js +126 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/npcs/agent-common/activity.d.ts +42 -0
- package/dist/npcs/agent-common/activity.js +280 -0
- package/dist/npcs/agent-common/git-stats.d.ts +11 -0
- package/dist/npcs/agent-common/git-stats.js +67 -0
- package/dist/npcs/agent-common/hook-runner.d.ts +62 -0
- package/dist/npcs/agent-common/hook-runner.js +245 -0
- package/dist/npcs/agent-common/hook-settings.d.ts +75 -0
- package/dist/npcs/agent-common/hook-settings.js +265 -0
- package/dist/npcs/agent-common/liveness.d.ts +12 -0
- package/dist/npcs/agent-common/liveness.js +105 -0
- package/dist/npcs/agent-common/lock.d.ts +6 -0
- package/dist/npcs/agent-common/lock.js +95 -0
- package/dist/npcs/agent-common/runtime.d.ts +16 -0
- package/dist/npcs/agent-common/runtime.js +149 -0
- package/dist/npcs/agent-common/session.d.ts +192 -0
- package/dist/npcs/agent-common/session.js +390 -0
- package/dist/npcs/agent-common/shim.d.ts +4 -0
- package/dist/npcs/agent-common/shim.js +90 -0
- package/dist/npcs/agent-common/watchdog.d.ts +16 -0
- package/dist/npcs/agent-common/watchdog.js +144 -0
- package/dist/npcs/claude-code/defaults.d.ts +15 -0
- package/dist/npcs/claude-code/defaults.js +17 -0
- package/dist/npcs/claude-code/hook-handler.d.ts +40 -0
- package/dist/npcs/claude-code/hook-handler.js +131 -0
- package/dist/npcs/claude-code/index.d.ts +36 -0
- package/dist/npcs/claude-code/index.js +92 -0
- package/dist/npcs/claude-code/setup.d.ts +58 -0
- package/dist/npcs/claude-code/setup.js +121 -0
- package/dist/npcs/claude-code/state-machine.d.ts +28 -0
- package/dist/npcs/claude-code/state-machine.js +125 -0
- package/dist/npcs/codex/defaults.d.ts +24 -0
- package/dist/npcs/codex/defaults.js +28 -0
- package/dist/npcs/codex/hook-handler.d.ts +24 -0
- package/dist/npcs/codex/hook-handler.js +110 -0
- package/dist/npcs/codex/index.d.ts +21 -0
- package/dist/npcs/codex/index.js +76 -0
- package/dist/npcs/codex/rollout.d.ts +39 -0
- package/dist/npcs/codex/rollout.js +242 -0
- package/dist/npcs/codex/setup.d.ts +37 -0
- package/dist/npcs/codex/setup.js +96 -0
- package/dist/npcs/codex/watcher.d.ts +65 -0
- package/dist/npcs/codex/watcher.js +356 -0
- package/dist/npcs/cursor/defaults.d.ts +9 -0
- package/dist/npcs/cursor/defaults.js +11 -0
- package/dist/npcs/cursor/hook-handler.d.ts +8 -0
- package/dist/npcs/cursor/hook-handler.js +60 -0
- package/dist/npcs/cursor/index.d.ts +18 -0
- package/dist/npcs/cursor/index.js +57 -0
- package/dist/npcs/cursor/normalize.d.ts +6 -0
- package/dist/npcs/cursor/normalize.js +49 -0
- package/dist/npcs/cursor/setup.d.ts +29 -0
- package/dist/npcs/cursor/setup.js +90 -0
- package/dist/npcs/gemini/defaults.d.ts +9 -0
- package/dist/npcs/gemini/defaults.js +11 -0
- package/dist/npcs/gemini/hook-handler.d.ts +10 -0
- package/dist/npcs/gemini/hook-handler.js +87 -0
- package/dist/npcs/gemini/index.d.ts +17 -0
- package/dist/npcs/gemini/index.js +56 -0
- package/dist/npcs/gemini/setup.d.ts +29 -0
- package/dist/npcs/gemini/setup.js +90 -0
- package/dist/npcs/github-watcher/defaults.d.ts +8 -0
- package/dist/npcs/github-watcher/defaults.js +8 -0
- package/dist/npcs/github-watcher/format.d.ts +3 -0
- package/dist/npcs/github-watcher/format.js +18 -0
- package/dist/npcs/github-watcher/github-api.d.ts +55 -0
- package/dist/npcs/github-watcher/github-api.js +120 -0
- package/dist/npcs/github-watcher/index.d.ts +35 -0
- package/dist/npcs/github-watcher/index.js +144 -0
- package/dist/npcs/internet-connectivity/checks.d.ts +24 -0
- package/dist/npcs/internet-connectivity/checks.js +84 -0
- package/dist/npcs/internet-connectivity/defaults.d.ts +20 -0
- package/dist/npcs/internet-connectivity/defaults.js +19 -0
- package/dist/npcs/internet-connectivity/index.d.ts +55 -0
- package/dist/npcs/internet-connectivity/index.js +246 -0
- package/dist/npcs/log-watcher/buffer.d.ts +18 -0
- package/dist/npcs/log-watcher/buffer.js +34 -0
- package/dist/npcs/log-watcher/defaults.d.ts +9 -0
- package/dist/npcs/log-watcher/defaults.js +9 -0
- package/dist/npcs/log-watcher/index.d.ts +45 -0
- package/dist/npcs/log-watcher/index.js +157 -0
- package/dist/npcs/log-watcher/tail.d.ts +9 -0
- package/dist/npcs/log-watcher/tail.js +78 -0
- package/dist/npcs/process-watcher/defaults.d.ts +6 -0
- package/dist/npcs/process-watcher/defaults.js +6 -0
- package/dist/npcs/process-watcher/index.d.ts +26 -0
- package/dist/npcs/process-watcher/index.js +136 -0
- package/dist/npcs/process-watcher/process-scanner.d.ts +19 -0
- package/dist/npcs/process-watcher/process-scanner.js +110 -0
- package/dist/npcs/receptionist/defaults.d.ts +6 -0
- package/dist/npcs/receptionist/defaults.js +6 -0
- package/dist/npcs/receptionist/index.d.ts +29 -0
- package/dist/npcs/receptionist/index.js +116 -0
- package/dist/npcs/receptionist/weather.d.ts +37 -0
- package/dist/npcs/receptionist/weather.js +121 -0
- package/dist/npcs/system-monitor/collectors.d.ts +45 -0
- package/dist/npcs/system-monitor/collectors.js +60 -0
- package/dist/npcs/system-monitor/defaults.d.ts +7 -0
- package/dist/npcs/system-monitor/defaults.js +10 -0
- package/dist/npcs/system-monitor/index.d.ts +27 -0
- package/dist/npcs/system-monitor/index.js +101 -0
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +16 -0
- package/dist/npcs/web-page-watcher/basic-mode.js +52 -0
- package/dist/npcs/web-page-watcher/capture.d.ts +49 -0
- package/dist/npcs/web-page-watcher/capture.js +75 -0
- package/dist/npcs/web-page-watcher/defaults.d.ts +19 -0
- package/dist/npcs/web-page-watcher/defaults.js +27 -0
- package/dist/npcs/web-page-watcher/index.d.ts +42 -0
- package/dist/npcs/web-page-watcher/index.js +230 -0
- package/dist/npcs/web-page-watcher/pixelate.d.ts +36 -0
- package/dist/npcs/web-page-watcher/pixelate.js +172 -0
- package/package.json +73 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNumstat = parseNumstat;
|
|
4
|
+
exports.computeGitStats = computeGitStats;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
8
|
+
/** Parse NUL-delimited numstat, including binary files and rename path pairs. */
|
|
9
|
+
function parseNumstat(raw) {
|
|
10
|
+
const fields = raw.split('\0'), files = [];
|
|
11
|
+
for (let i = 0; i < fields.length; i++) {
|
|
12
|
+
const record = fields[i];
|
|
13
|
+
if (!record)
|
|
14
|
+
continue;
|
|
15
|
+
const match = /^(\d+|-)\t(\d+|-)\t([\s\S]*)$/.exec(record);
|
|
16
|
+
if (!match)
|
|
17
|
+
continue;
|
|
18
|
+
let path = match[3];
|
|
19
|
+
if (!path) {
|
|
20
|
+
i++;
|
|
21
|
+
path = fields[++i] ?? '';
|
|
22
|
+
}
|
|
23
|
+
if (path)
|
|
24
|
+
files.push({
|
|
25
|
+
path,
|
|
26
|
+
insertions: match[1] === '-' ? 0 : Number(match[1]),
|
|
27
|
+
deletions: match[2] === '-' ? 0 : Number(match[2]),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
/** Both commands share one wall-clock budget; call only from detached flushes. */
|
|
33
|
+
function computeGitStats(cwd, timeoutMs = 1500) {
|
|
34
|
+
const deadline = node_perf_hooks_1.performance.now() + Math.max(1, Math.min(1500, timeoutMs));
|
|
35
|
+
const git = (args) => {
|
|
36
|
+
const remaining = Math.floor(deadline - node_perf_hooks_1.performance.now());
|
|
37
|
+
if (remaining <= 0)
|
|
38
|
+
return null;
|
|
39
|
+
const result = (0, node_child_process_1.spawnSync)('git', ['--no-optional-locks', '-c', 'core.fsmonitor=false', '-C', cwd, ...args], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: remaining,
|
|
42
|
+
killSignal: 'SIGKILL',
|
|
43
|
+
maxBuffer: 1 << 20,
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
return result.status === 0 && !result.error ? result.stdout : null;
|
|
47
|
+
};
|
|
48
|
+
const diff = git(['diff', '--no-ext-diff', '--no-textconv', '--numstat', '-z', 'HEAD', '--']);
|
|
49
|
+
if (diff === null)
|
|
50
|
+
return null;
|
|
51
|
+
const untracked = git(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
52
|
+
if (untracked === null)
|
|
53
|
+
return null;
|
|
54
|
+
const files = parseNumstat(diff);
|
|
55
|
+
for (const path of untracked.split('\0'))
|
|
56
|
+
if (path)
|
|
57
|
+
files.push({ path, insertions: 0, deletions: 0 });
|
|
58
|
+
if (files.length > 100_000)
|
|
59
|
+
return null;
|
|
60
|
+
files.sort((a, b) => b.insertions + b.deletions - (a.insertions + a.deletions) || a.path.localeCompare(b.path));
|
|
61
|
+
return {
|
|
62
|
+
filesChanged: files.length,
|
|
63
|
+
insertions: files.reduce((n, file) => n + file.insertions, 0),
|
|
64
|
+
deletions: files.reduce((n, file) => n + file.deletions, 0),
|
|
65
|
+
lastFiles: files.slice(0, 5).map((file) => (0, node_path_1.basename)(file.path)
|
|
66
|
+
// eslint-disable-next-line no-control-regex -- Strip display controls from Git filenames.
|
|
67
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, '')
|
|
68
|
+
.slice(0, 120)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=git-stats.js.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hookEventName = hookEventName;
|
|
4
|
+
exports.readStdin = readStdin;
|
|
5
|
+
exports.runAgentHook = runAgentHook;
|
|
6
|
+
exports.runFlushCommand = runFlushCommand;
|
|
7
|
+
exports.collectSessionGitStats = collectSessionGitStats;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const activity_js_1 = require("./activity.js");
|
|
10
|
+
const git_stats_js_1 = require("./git-stats.js");
|
|
11
|
+
const liveness_js_1 = require("./liveness.js");
|
|
12
|
+
const lock_js_1 = require("./lock.js");
|
|
13
|
+
const session_js_1 = require("./session.js");
|
|
14
|
+
const watchdog_js_1 = require("./watchdog.js");
|
|
15
|
+
function hookEventName(event) {
|
|
16
|
+
return event.hook_event_name ?? event.event ?? '';
|
|
17
|
+
}
|
|
18
|
+
/** Read all of stdin synchronously; '' on error. */
|
|
19
|
+
function readStdin() {
|
|
20
|
+
try {
|
|
21
|
+
return (0, node_fs_1.readFileSync)(0, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Process one hook invocation. Never throws. Only a configured neutral JSON response
|
|
29
|
+
* reaches stdout; other hosts receive no output.
|
|
30
|
+
*/
|
|
31
|
+
async function runAgentHook(spec, config, rawInput, opts = {}) {
|
|
32
|
+
try {
|
|
33
|
+
await runAgentHookUnsafe(spec, config, rawInput, opts);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (config.debug) {
|
|
37
|
+
console.error(`[nocville ${spec.adapter.command} hook] ${err?.message ?? err}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (spec.stdoutOnSuccess !== undefined)
|
|
42
|
+
process.stdout.write(spec.stdoutOnSuccess + '\n');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function runAgentHookUnsafe(spec, config, rawInput, opts) {
|
|
46
|
+
if (typeof rawInput === 'string' && !rawInput.trim())
|
|
47
|
+
return;
|
|
48
|
+
let event;
|
|
49
|
+
try {
|
|
50
|
+
event = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!event || typeof event !== 'object')
|
|
56
|
+
return;
|
|
57
|
+
const sessionId = typeof event.session_id === 'string' ? event.session_id : '';
|
|
58
|
+
if (!sessionId)
|
|
59
|
+
return;
|
|
60
|
+
const { adapter } = spec;
|
|
61
|
+
const eventName = hookEventName(event);
|
|
62
|
+
// Leave headroom for launcher startup and the 1.5s HTTP attempt within Codex's 3s cap.
|
|
63
|
+
const shutdown = eventName === 'SessionEnd' || eventName === 'Interrupt';
|
|
64
|
+
const lockWaitMs = shutdown ? 150 : adapter.family === 'gemini' ? 500 : undefined;
|
|
65
|
+
const canResolvePid = adapter.processNames?.length !== 0 &&
|
|
66
|
+
!shutdown &&
|
|
67
|
+
opts.mode === 'detach' &&
|
|
68
|
+
process.env['NOCVILLE_NO_WATCHDOG'] !== '1';
|
|
69
|
+
const now = (opts.now ?? Date.now)();
|
|
70
|
+
if (eventName === 'SessionStart')
|
|
71
|
+
await (0, session_js_1.maybeCleanupSessionStates)(adapter.stateDir, now);
|
|
72
|
+
const { decision, offline, resolvePid, collectStats } = await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
|
|
73
|
+
const prev = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
|
|
74
|
+
if (prev && adapter.processNames?.length === 0 && (prev.agentPid || prev.watchdogPid)) {
|
|
75
|
+
// Retire observers installed before an adapter opted out of process liveness.
|
|
76
|
+
prev.agentPid = undefined;
|
|
77
|
+
prev.agentStartedAt = undefined;
|
|
78
|
+
prev.watchdogPid = undefined;
|
|
79
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, prev);
|
|
80
|
+
}
|
|
81
|
+
const identity = spec.deliveryIdentity?.(event);
|
|
82
|
+
const deliveries = (prev?.hookDeliveries ?? []).filter((d) => now - d.at < 5000);
|
|
83
|
+
if (identity) {
|
|
84
|
+
const duplicate = deliveries.findIndex((d) => d.key === identity.key && d.source !== identity.source);
|
|
85
|
+
if (duplicate >= 0 && prev) {
|
|
86
|
+
deliveries.splice(duplicate, 1);
|
|
87
|
+
prev.hookDeliveries = deliveries;
|
|
88
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, prev);
|
|
89
|
+
return {
|
|
90
|
+
decision: { kind: 'none' },
|
|
91
|
+
offline: !!prev.offline,
|
|
92
|
+
resolvePid: false,
|
|
93
|
+
collectStats: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
deliveries.push({ ...identity, at: now });
|
|
97
|
+
}
|
|
98
|
+
const cwd = (typeof event.cwd === 'string' && event.cwd) || prev?.projectDir || process.cwd();
|
|
99
|
+
// An offline sentinel awaiting retry cannot be revived by late async tool hooks.
|
|
100
|
+
if (prev?.offline && eventName !== 'SessionStart')
|
|
101
|
+
return {
|
|
102
|
+
decision: { kind: 'now' },
|
|
103
|
+
offline: true,
|
|
104
|
+
resolvePid: false,
|
|
105
|
+
collectStats: false,
|
|
106
|
+
};
|
|
107
|
+
const agentId = typeof event['agent_id'] === 'string' ? event['agent_id'] : '';
|
|
108
|
+
if (adapter.family === 'claude' &&
|
|
109
|
+
agentId &&
|
|
110
|
+
prev &&
|
|
111
|
+
eventName !== 'PermissionRequest' &&
|
|
112
|
+
eventName !== 'SubagentStart') {
|
|
113
|
+
const next = (0, session_js_1.applySubagentEvent)(prev, agentId, typeof event['agent_type'] === 'string' ? event['agent_type'] : 'subagent', eventName, (0, activity_js_1.describeToolUse)(event.tool_name, event.tool_input, cwd), now);
|
|
114
|
+
let decision = { kind: 'none' };
|
|
115
|
+
if (next.rev !== prev.rev) {
|
|
116
|
+
const gap = opts.minPushGapMs ?? session_js_1.DEFAULT_MIN_PUSH_GAP_MS;
|
|
117
|
+
const elapsed = now - (prev.lastPushAt ?? 0);
|
|
118
|
+
if (!prev.lastPushAt || elapsed >= gap) {
|
|
119
|
+
next.lastPushAt = now;
|
|
120
|
+
decision = { kind: 'now' };
|
|
121
|
+
}
|
|
122
|
+
else if (!next.flushAt || next.flushAt <= now - 2000) {
|
|
123
|
+
const delayMs = Math.max(50, gap - elapsed);
|
|
124
|
+
next.flushAt = now + delayMs;
|
|
125
|
+
decision = { kind: 'defer', delayMs };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const resolvePid = canResolvePid && (0, liveness_js_1.reserveAgentPidAttempt)(next, now);
|
|
129
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, next);
|
|
130
|
+
return { decision, offline: false, resolvePid, collectStats: false };
|
|
131
|
+
}
|
|
132
|
+
// Codex has no documented agent_id in hook payloads: keep its parent transitions.
|
|
133
|
+
// https://developers.openai.com/codex/hooks
|
|
134
|
+
const { next, decision } = (0, session_js_1.applyTransition)(prev, {
|
|
135
|
+
sessionId,
|
|
136
|
+
serviceId: spec.serviceIdFor(sessionId, cwd),
|
|
137
|
+
projectDir: cwd,
|
|
138
|
+
eventName,
|
|
139
|
+
name: spec.resolveName?.(event, prev),
|
|
140
|
+
transition: spec.transition(event, prev),
|
|
141
|
+
now,
|
|
142
|
+
}, opts.minPushGapMs ?? session_js_1.DEFAULT_MIN_PUSH_GAP_MS);
|
|
143
|
+
if (!prev || prev.offline) {
|
|
144
|
+
next.agentPid = undefined;
|
|
145
|
+
next.agentStartedAt = undefined;
|
|
146
|
+
next.agentPidResolveAttempts = 0;
|
|
147
|
+
next.agentPidLastResolveAt = undefined;
|
|
148
|
+
next.watchdogPid = undefined;
|
|
149
|
+
next.offlineRetryStartedAt = undefined;
|
|
150
|
+
next.offlineDeliveryStopped = undefined;
|
|
151
|
+
next.subagents = {};
|
|
152
|
+
next.agentStats = undefined;
|
|
153
|
+
next.statsPendingRev = undefined;
|
|
154
|
+
}
|
|
155
|
+
if (identity)
|
|
156
|
+
next.hookDeliveries = deliveries.slice(-64);
|
|
157
|
+
const resolvePid = canResolvePid && (0, liveness_js_1.reserveAgentPidAttempt)(next, now);
|
|
158
|
+
const collectStats = (spec.statsEvents ?? ['Stop']).includes(eventName) && !next.offline;
|
|
159
|
+
if (collectStats)
|
|
160
|
+
next.statsPendingRev = next.rev ?? 0;
|
|
161
|
+
else if (next.state === 'WORKING' || next.offline)
|
|
162
|
+
next.statsPendingRev = undefined;
|
|
163
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, next);
|
|
164
|
+
return { decision, offline: !!next.offline, resolvePid, collectStats };
|
|
165
|
+
}, lockWaitMs);
|
|
166
|
+
if (resolvePid && !offline) {
|
|
167
|
+
const proc = (0, liveness_js_1.resolveAgentPid)(adapter, sessionId);
|
|
168
|
+
await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
|
|
169
|
+
const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
|
|
170
|
+
if (!fresh || fresh.offline)
|
|
171
|
+
return;
|
|
172
|
+
fresh.agentPid = proc?.pid;
|
|
173
|
+
fresh.agentStartedAt = proc?.startedAt;
|
|
174
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
|
|
175
|
+
}, lockWaitMs);
|
|
176
|
+
}
|
|
177
|
+
if (opts.mode === 'detach' && !offline)
|
|
178
|
+
await (0, watchdog_js_1.ensureWatchdog)(adapter, config, sessionId, opts.cliPath, lockWaitMs);
|
|
179
|
+
// SessionEnd is synchronous and bounded by the agent's short shutdown timeout.
|
|
180
|
+
if (offline) {
|
|
181
|
+
await (0, session_js_1.flushSession)(adapter, config, sessionId, {
|
|
182
|
+
push: opts.push,
|
|
183
|
+
timeoutMs: 1500,
|
|
184
|
+
lockWaitMs: 150,
|
|
185
|
+
});
|
|
186
|
+
// If delivery failed, the watchdog retries; unsupported hosts get one detached retry.
|
|
187
|
+
const pending = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
|
|
188
|
+
if (opts.mode === 'detach' && pending && (pending.rev ?? 0) > (pending.pushedRev ?? 0))
|
|
189
|
+
(0, session_js_1.spawnDetachedFlush)(adapter, config, sessionId, 0, opts.cliPath);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (decision.kind === 'none' && !collectStats)
|
|
193
|
+
return;
|
|
194
|
+
const mode = opts.mode ?? 'inline';
|
|
195
|
+
const delayMs = decision.kind === 'defer' ? decision.delayMs : 0;
|
|
196
|
+
if (opts.scheduleFlush && decision.kind === 'defer') {
|
|
197
|
+
opts.scheduleFlush(sessionId, delayMs);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (mode === 'detach') {
|
|
201
|
+
(0, session_js_1.spawnDetachedFlush)(adapter, config, sessionId, delayMs, opts.cliPath, collectStats);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (delayMs > 0) {
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
void (0, session_js_1.flushSession)(adapter, config, sessionId, { push: opts.push });
|
|
207
|
+
}, delayMs);
|
|
208
|
+
timer.unref?.();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await (0, session_js_1.flushSession)(adapter, config, sessionId, { push: opts.push });
|
|
212
|
+
}
|
|
213
|
+
/** `<agent> flush <session> --delay=ms`: the detached half of a hook push. */
|
|
214
|
+
async function runFlushCommand(adapter, config, sessionId, delayMs, stats = false) {
|
|
215
|
+
if (!sessionId)
|
|
216
|
+
return;
|
|
217
|
+
try {
|
|
218
|
+
if (delayMs && delayMs > 0)
|
|
219
|
+
await (0, session_js_1.sleep)(Math.min(delayMs, 10_000));
|
|
220
|
+
if (stats || (0, session_js_1.readSessionState)(adapter.stateDir, sessionId)?.statsPendingRev !== undefined)
|
|
221
|
+
await collectSessionGitStats(adapter, sessionId);
|
|
222
|
+
await (0, session_js_1.flushSession)(adapter, config, sessionId);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// never surface errors from a background flush
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Git runs outside the state lock, and a newer turn can invalidate its result. */
|
|
229
|
+
async function collectSessionGitStats(adapter, sessionId, compute = git_stats_js_1.computeGitStats) {
|
|
230
|
+
const snapshot = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
|
|
231
|
+
if (!snapshot || snapshot.offline || snapshot.state === 'WORKING')
|
|
232
|
+
return;
|
|
233
|
+
// Another scheduled flush may already have consumed an explicit --stats request.
|
|
234
|
+
if (snapshot.statsPendingRev === undefined)
|
|
235
|
+
return;
|
|
236
|
+
const revision = snapshot.statsPendingRev;
|
|
237
|
+
const stats = compute(snapshot.projectDir);
|
|
238
|
+
await (0, lock_js_1.withStateLock)((0, session_js_1.stateFilePath)(adapter.stateDir, sessionId), () => {
|
|
239
|
+
const fresh = (0, session_js_1.readSessionState)(adapter.stateDir, sessionId);
|
|
240
|
+
if (!fresh || fresh.offline || fresh.state === 'WORKING' || fresh.statsPendingRev !== revision)
|
|
241
|
+
return;
|
|
242
|
+
fresh.statsPendingRev = undefined;
|
|
243
|
+
fresh.agentStats = stats ?? undefined;
|
|
244
|
+
if (stats && stats.filesChanged > 0) {
|
|
245
|
+
const line = `Uncommitted changes — ${stats.filesChanged} file${stats.filesChanged === 1 ? '' : 's'}, +${stats.insertions}/−${stats.deletions}`;
|
|
246
|
+
fresh.activity = (0, session_js_1.pushHistory)(fresh.activity, line);
|
|
247
|
+
}
|
|
248
|
+
fresh.rev = (fresh.rev ?? 0) + 1;
|
|
249
|
+
(0, session_js_1.writeSessionState)(adapter.stateDir, fresh);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=hook-runner.js.map
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HOOK_MARKER_COMMENT = exports.HOOK_MARKER = void 0;
|
|
4
|
+
exports.installHookGroups = installHookGroups;
|
|
5
|
+
exports.removeHookGroups = removeHookGroups;
|
|
6
|
+
exports.findNocvilleCommand = findNocvilleCommand;
|
|
7
|
+
exports.extractFlag = extractFlag;
|
|
8
|
+
exports.shellQuote = shellQuote;
|
|
9
|
+
exports.buildAgentHookCommand = buildAgentHookCommand;
|
|
10
|
+
exports.stableNodePath = stableNodePath;
|
|
11
|
+
exports.resolveCliPath = resolveCliPath;
|
|
12
|
+
exports.readJsonFile = readJsonFile;
|
|
13
|
+
exports.writeJsonFile = writeJsonFile;
|
|
14
|
+
exports.buildShimHookCommand = buildShimHookCommand;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const shim_js_1 = require("./shim.js");
|
|
18
|
+
/**
|
|
19
|
+
* Matcher-group hook config shared by Claude Code (`~/.claude/settings.json`)
|
|
20
|
+
* and Codex (`~/.codex/hooks.json`) — both use:
|
|
21
|
+
*
|
|
22
|
+
* { "hooks": { "<Event>": [ { "matcher": "", "hooks": [ { "type": "command", "command": "…" } ] } ] } }
|
|
23
|
+
*
|
|
24
|
+
* Install/uninstall only ever touch entries whose command carries the
|
|
25
|
+
* `nocville-npc` marker; the user's own hooks are preserved verbatim.
|
|
26
|
+
*/
|
|
27
|
+
/** Marker used to identify nocville-npc hook commands (flat or nested). */
|
|
28
|
+
exports.HOOK_MARKER = 'nocville-npc';
|
|
29
|
+
/**
|
|
30
|
+
* Trailing shell comment appended to hook commands. `/bin/sh` ignores it, but it
|
|
31
|
+
* guarantees the marker is present for idempotency regardless of install path.
|
|
32
|
+
*/
|
|
33
|
+
exports.HOOK_MARKER_COMMENT = '# nocville-npc-hook';
|
|
34
|
+
function isCommandHookWithMarker(hook) {
|
|
35
|
+
if (!hook ||
|
|
36
|
+
typeof hook !== 'object' ||
|
|
37
|
+
typeof hook.command !== 'string')
|
|
38
|
+
return false;
|
|
39
|
+
const command = hook.command;
|
|
40
|
+
if (command.includes(exports.HOOK_MARKER) ||
|
|
41
|
+
/nocville-hook\.cmd"?\s+(?:claude-code|codex|gemini|cursor)\s+hook\b/.test(command))
|
|
42
|
+
return true;
|
|
43
|
+
// Recognize the legacy encoded Windows form so setup replaces it and uninstall removes it.
|
|
44
|
+
const encoded = /^powershell\.exe -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/=]+)$/.exec(command);
|
|
45
|
+
if (!encoded)
|
|
46
|
+
return false;
|
|
47
|
+
const decoded = Buffer.from(encoded[1], 'base64').toString('utf16le');
|
|
48
|
+
return (decoded.startsWith(`& cmd.exe /d /s /c '`) &&
|
|
49
|
+
decoded.endsWith(` # ${exports.HOOK_MARKER}`) &&
|
|
50
|
+
/nocville-hook\.cmd" (?:claude-code|codex|gemini|cursor) hook/.test(decoded));
|
|
51
|
+
}
|
|
52
|
+
function isGroup(entry) {
|
|
53
|
+
return (typeof entry === 'object' &&
|
|
54
|
+
entry !== null &&
|
|
55
|
+
Array.isArray(entry.hooks));
|
|
56
|
+
}
|
|
57
|
+
/** Old flat format: a bare command hook (no nested `hooks` array). */
|
|
58
|
+
function isFlatNocvilleEntry(entry) {
|
|
59
|
+
return !isGroup(entry) && isCommandHookWithMarker(entry);
|
|
60
|
+
}
|
|
61
|
+
/** Strip nocville hooks from one event's entry list. Returns kept entries + removed count. */
|
|
62
|
+
function stripNocville(entries) {
|
|
63
|
+
const kept = [];
|
|
64
|
+
let removed = 0;
|
|
65
|
+
let flat = 0;
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (isFlatNocvilleEntry(entry)) {
|
|
68
|
+
removed++;
|
|
69
|
+
flat++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (isGroup(entry)) {
|
|
73
|
+
const keptHooks = entry.hooks.filter((h) => !isCommandHookWithMarker(h));
|
|
74
|
+
const diff = entry.hooks.length - keptHooks.length;
|
|
75
|
+
if (diff === 0)
|
|
76
|
+
kept.push(entry);
|
|
77
|
+
else {
|
|
78
|
+
removed += diff;
|
|
79
|
+
if (keptHooks.length > 0)
|
|
80
|
+
kept.push({ ...entry, hooks: keptHooks });
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
kept.push(entry);
|
|
85
|
+
}
|
|
86
|
+
return { kept, removed, flat };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Install one nocville hook group into each of `events`. Idempotent: existing
|
|
90
|
+
* nocville entries (flat legacy or nested) are removed first, preserving the
|
|
91
|
+
* user's own hooks, then a single group per event is appended. Mutates `file`.
|
|
92
|
+
*/
|
|
93
|
+
function installHookGroups(file, command, events, extra = {}) {
|
|
94
|
+
if (!file.hooks || typeof file.hooks !== 'object' || Array.isArray(file.hooks))
|
|
95
|
+
file.hooks = {};
|
|
96
|
+
const root = file.hooks;
|
|
97
|
+
let migrated = 0;
|
|
98
|
+
let installed = 0;
|
|
99
|
+
// Drop nocville entries from events we no longer install, too.
|
|
100
|
+
for (const ev of Object.keys(root)) {
|
|
101
|
+
if (events.includes(ev) || !Array.isArray(root[ev]))
|
|
102
|
+
continue;
|
|
103
|
+
const { kept } = stripNocville(root[ev]);
|
|
104
|
+
if (kept.length === 0)
|
|
105
|
+
delete root[ev];
|
|
106
|
+
else
|
|
107
|
+
root[ev] = kept;
|
|
108
|
+
}
|
|
109
|
+
for (const ev of events) {
|
|
110
|
+
const existing = Array.isArray(root[ev]) ? root[ev] : [];
|
|
111
|
+
const { kept, flat } = stripNocville(existing);
|
|
112
|
+
migrated += flat;
|
|
113
|
+
const hook = { type: 'command', command };
|
|
114
|
+
if (extra.timeout !== undefined)
|
|
115
|
+
hook.timeout = extra.timeout;
|
|
116
|
+
Object.assign(hook, extra.perEvent?.[ev]);
|
|
117
|
+
kept.push({ matcher: '', hooks: [hook] });
|
|
118
|
+
installed++;
|
|
119
|
+
root[ev] = kept;
|
|
120
|
+
}
|
|
121
|
+
return { migrated, installed };
|
|
122
|
+
}
|
|
123
|
+
/** Remove every nocville hook entry. Mutates `file`; returns the count removed. */
|
|
124
|
+
function removeHookGroups(file) {
|
|
125
|
+
if (!file.hooks || typeof file.hooks !== 'object')
|
|
126
|
+
return 0;
|
|
127
|
+
const root = file.hooks;
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const ev of Object.keys(root)) {
|
|
130
|
+
const arr = root[ev];
|
|
131
|
+
if (!Array.isArray(arr))
|
|
132
|
+
continue;
|
|
133
|
+
const res = stripNocville(arr);
|
|
134
|
+
removed += res.removed;
|
|
135
|
+
if (res.kept.length === 0)
|
|
136
|
+
delete root[ev];
|
|
137
|
+
else
|
|
138
|
+
root[ev] = res.kept;
|
|
139
|
+
}
|
|
140
|
+
if (Object.keys(root).length === 0)
|
|
141
|
+
delete file.hooks;
|
|
142
|
+
return removed;
|
|
143
|
+
}
|
|
144
|
+
/** First nocville hook command found in any event (any format). */
|
|
145
|
+
function findNocvilleCommand(file) {
|
|
146
|
+
const root = file.hooks;
|
|
147
|
+
if (!root || typeof root !== 'object')
|
|
148
|
+
return undefined;
|
|
149
|
+
for (const ev of Object.keys(root)) {
|
|
150
|
+
const arr = root[ev];
|
|
151
|
+
if (!Array.isArray(arr))
|
|
152
|
+
continue;
|
|
153
|
+
for (const entry of arr) {
|
|
154
|
+
if (isFlatNocvilleEntry(entry))
|
|
155
|
+
return entry.command;
|
|
156
|
+
if (isGroup(entry)) {
|
|
157
|
+
const hook = entry.hooks.find(isCommandHookWithMarker);
|
|
158
|
+
if (hook)
|
|
159
|
+
return hook.command;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
/** Extract a `--flag` value from a command string; supports `=`, quotes, or the space form. */
|
|
166
|
+
function extractFlag(command, flag) {
|
|
167
|
+
const re = new RegExp(`--${flag}(?:=|\\s+)('([^']*)'|"([^"]*)"|([^\\s]+))`);
|
|
168
|
+
const m = command.match(re);
|
|
169
|
+
if (!m)
|
|
170
|
+
return undefined;
|
|
171
|
+
return m[2] ?? m[3] ?? m[4];
|
|
172
|
+
}
|
|
173
|
+
/** POSIX single-quote a string so /bin/sh treats it as one literal token. */
|
|
174
|
+
function shellQuote(s) {
|
|
175
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Absolute hook command: `'<node>' '<cli.js>' <agent> hook # nocville-npc-hook`.
|
|
179
|
+
*
|
|
180
|
+
* Agents run hooks via `/bin/sh` without the user's interactive PATH (nvm /
|
|
181
|
+
* homebrew shims are absent), so a bare `nocville-npc` would fail; absolute
|
|
182
|
+
* paths fix that. No API key on the command line — the hook reads the saved
|
|
183
|
+
* user config (~/.config/nocville/config.json).
|
|
184
|
+
*/
|
|
185
|
+
function buildAgentHookCommand(cliPath, agent, extraArgs = [], nodePath = stableNodePath()) {
|
|
186
|
+
const args = extraArgs.length ? ' ' + extraArgs.join(' ') : '';
|
|
187
|
+
return `${shellQuote(nodePath)} ${shellQuote(cliPath)} ${agent} hook${args} ${exports.HOOK_MARKER_COMMENT}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The node binary to bake into hook commands. `process.execPath` is often a
|
|
191
|
+
* versioned path (e.g. Homebrew's /opt/homebrew/Cellar/node/24.4.1/bin/node)
|
|
192
|
+
* that disappears on upgrade, so prefer an unversioned PATH entry (e.g.
|
|
193
|
+
* /opt/homebrew/bin/node) that resolves to the same binary.
|
|
194
|
+
*/
|
|
195
|
+
function stableNodePath(execPath = process.execPath, pathEnv = process.env['PATH'] ?? '') {
|
|
196
|
+
let target;
|
|
197
|
+
try {
|
|
198
|
+
target = (0, node_fs_1.realpathSync)(execPath);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return execPath;
|
|
202
|
+
}
|
|
203
|
+
const exe = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
204
|
+
for (const dir of pathEnv.split(node_path_1.delimiter)) {
|
|
205
|
+
if (!dir || /[\\/]Cellar[\\/]/.test(dir))
|
|
206
|
+
continue;
|
|
207
|
+
const candidate = (0, node_path_1.join)(dir, exe);
|
|
208
|
+
try {
|
|
209
|
+
if (candidate !== execPath && (0, node_fs_1.realpathSync)(candidate) === target)
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// not here
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return execPath;
|
|
217
|
+
}
|
|
218
|
+
/** Realpath the running CLI (follows npm bin symlinks). */
|
|
219
|
+
function resolveCliPath(explicit) {
|
|
220
|
+
const candidate = explicit ?? process.argv[1];
|
|
221
|
+
if (candidate) {
|
|
222
|
+
try {
|
|
223
|
+
return (0, node_fs_1.realpathSync)(candidate);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return candidate;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return 'nocville-npc';
|
|
230
|
+
}
|
|
231
|
+
/** Read a JSON hooks/settings file; missing → {}; unparsable → null (refuse to overwrite). */
|
|
232
|
+
function readJsonFile(path) {
|
|
233
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
234
|
+
return {};
|
|
235
|
+
try {
|
|
236
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
237
|
+
if (!raw.trim())
|
|
238
|
+
return {};
|
|
239
|
+
const parsed = JSON.parse(raw);
|
|
240
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
241
|
+
return null;
|
|
242
|
+
return parsed;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Write JSON, keeping a one-time `.nocville-backup` of the original file. */
|
|
249
|
+
function writeJsonFile(path, data) {
|
|
250
|
+
const backup = `${path}.nocville-backup`;
|
|
251
|
+
if ((0, node_fs_1.existsSync)(path) && !(0, node_fs_1.existsSync)(backup)) {
|
|
252
|
+
try {
|
|
253
|
+
(0, node_fs_1.copyFileSync)(path, backup);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// best effort
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
(0, node_fs_1.writeFileSync)(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
260
|
+
}
|
|
261
|
+
function buildShimHookCommand(agent, platform = process.platform) {
|
|
262
|
+
if (platform === 'win32') {
|
|
263
|
+
const shim = `${(0, shim_js_1.SHIM_PATH)()}.cmd`;
|
|
264
|
+
if (agent === 'claude-code') {
|
|
265
|
+
// installHooksInto sets shell: powershell, bypassing Git Bash detection.
|
|
266
|
+
return `& '${shim.replace(/['‘’‚‛]/g, '$&$&')}' ${agent} hook ${exports.HOOK_MARKER_COMMENT}`;
|
|
267
|
+
}
|
|
268
|
+
const payload = `""${shim}" ${agent} hook"`;
|
|
269
|
+
if (agent === 'gemini') {
|
|
270
|
+
// Gemini uses PowerShell (7 or 5.1). Stop native-argument parsing so cmd
|
|
271
|
+
// receives its quotes intact. The newline ends --% before Gemini appends
|
|
272
|
+
// its own PowerShell exit-code check. No encoded command or policy bypass.
|
|
273
|
+
return `cmd.exe /d /s /c --% ${payload}\n`;
|
|
274
|
+
}
|
|
275
|
+
return `cmd.exe /d /s /c ${payload}`;
|
|
276
|
+
}
|
|
277
|
+
return `${shellQuote((0, shim_js_1.SHIM_PATH)())} ${agent} hook ${exports.HOOK_MARKER_COMMENT}`;
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=hook-settings.js.map
|