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