@primitive.ai/prim 0.1.0-alpha.15 → 0.1.0-alpha.16

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.
@@ -0,0 +1,88 @@
1
+ // src/daemon/client.ts
2
+ import { createConnection } from "net";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var SOCK_PATH = join(homedir(), ".config", "prim", "sock");
6
+ var DEFAULT_TIMEOUT_MS = 250;
7
+ var nextRequestId = 1;
8
+ function daemonRequest(method, params = {}, opts = {}) {
9
+ const id = nextRequestId++;
10
+ const request = { id, method, params };
11
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12
+ return new Promise((resolve) => {
13
+ let settled = false;
14
+ let socket;
15
+ let buffer = "";
16
+ const settle = (value) => {
17
+ if (settled) {
18
+ return;
19
+ }
20
+ settled = true;
21
+ if (socket) {
22
+ try {
23
+ socket.destroy();
24
+ } catch {
25
+ }
26
+ }
27
+ resolve(value);
28
+ };
29
+ const timer = setTimeout(() => settle(null), timeoutMs);
30
+ timer.unref();
31
+ try {
32
+ socket = createConnection(SOCK_PATH);
33
+ } catch {
34
+ clearTimeout(timer);
35
+ settle(null);
36
+ return;
37
+ }
38
+ socket.on("error", () => {
39
+ clearTimeout(timer);
40
+ settle(null);
41
+ });
42
+ socket.on("connect", () => {
43
+ try {
44
+ socket?.write(`${JSON.stringify(request)}
45
+ `);
46
+ } catch {
47
+ clearTimeout(timer);
48
+ settle(null);
49
+ }
50
+ });
51
+ socket.on("data", (chunk) => {
52
+ buffer += chunk.toString("utf-8");
53
+ const newlineIdx = buffer.indexOf("\n");
54
+ if (newlineIdx === -1) {
55
+ return;
56
+ }
57
+ const line = buffer.slice(0, newlineIdx);
58
+ try {
59
+ const res = JSON.parse(line);
60
+ if (res.id !== id) {
61
+ clearTimeout(timer);
62
+ settle(null);
63
+ return;
64
+ }
65
+ clearTimeout(timer);
66
+ settle(res.ok && res.result !== void 0 ? res.result : null);
67
+ } catch {
68
+ clearTimeout(timer);
69
+ settle(null);
70
+ }
71
+ });
72
+ socket.on("end", () => {
73
+ if (!settled) {
74
+ clearTimeout(timer);
75
+ settle(null);
76
+ }
77
+ });
78
+ });
79
+ }
80
+ async function daemonIsLive(timeoutMs = DEFAULT_TIMEOUT_MS) {
81
+ const result = await daemonRequest("ping", {}, { timeoutMs });
82
+ return result?.pong === true;
83
+ }
84
+
85
+ export {
86
+ daemonRequest,
87
+ daemonIsLive
88
+ };
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getClient,
4
+ getTokenExpiresAt,
5
+ refreshToken
6
+ } from "../chunk-6SIEWWUL.js";
7
+
8
+ // src/daemon/server.ts
9
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
10
+ import { createServer } from "net";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var CONFIG_DIR = join(homedir(), ".config", "prim");
14
+ var SOCK_PATH = join(CONFIG_DIR, "sock");
15
+ var PID_PATH = join(CONFIG_DIR, "daemon.pid");
16
+ var HEARTBEAT_INTERVAL_MS = 3e4;
17
+ var TOKEN_CHECK_INTERVAL_MS = 6e4;
18
+ var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
19
+ var PRESENCE_FRESH_WINDOW_MS = 9e4;
20
+ var SOCKET_DIR_MODE = 448;
21
+ var PID_FILE_MODE = 384;
22
+ var EXIT_OK = 0;
23
+ var EXIT_CRASH = 1;
24
+ var startedAt = Date.now();
25
+ var client = getClient();
26
+ var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
27
+ var lastHeartbeatAt;
28
+ var lastOnlineCount;
29
+ var lastOkAtLocal;
30
+ var heartbeatTimer;
31
+ var tokenCheckTimer;
32
+ function processIsAlive(pid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ function takePidfile() {
41
+ if (existsSync(PID_PATH)) {
42
+ const existing = Number(readFileSync(PID_PATH, "utf-8").trim());
43
+ if (!Number.isNaN(existing) && processIsAlive(existing)) {
44
+ throw new Error(`daemon already running (pid=${existing})`);
45
+ }
46
+ }
47
+ if (!existsSync(CONFIG_DIR)) {
48
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: SOCKET_DIR_MODE });
49
+ }
50
+ writeFileSync(PID_PATH, String(process.pid), { mode: PID_FILE_MODE });
51
+ }
52
+ function cleanup() {
53
+ try {
54
+ unlinkSync(SOCK_PATH);
55
+ } catch {
56
+ }
57
+ try {
58
+ unlinkSync(PID_PATH);
59
+ } catch {
60
+ }
61
+ }
62
+ async function sendHeartbeat() {
63
+ try {
64
+ const result = await client.post("/api/cli/presence/heartbeat", {
65
+ sessionId: activeSessionId
66
+ });
67
+ if (result.accepted) {
68
+ lastOkAtLocal = Date.now();
69
+ if (typeof result.lastHeartbeatAt === "number") {
70
+ lastHeartbeatAt = result.lastHeartbeatAt;
71
+ }
72
+ if (typeof result.onlineCount === "number") {
73
+ lastOnlineCount = result.onlineCount;
74
+ }
75
+ }
76
+ } catch (err) {
77
+ process.stderr.write(
78
+ `[prim-daemon] heartbeat error: ${err instanceof Error ? err.message : String(err)}
79
+ `
80
+ );
81
+ }
82
+ }
83
+ async function ensureTokenFresh() {
84
+ const expiresAt = getTokenExpiresAt();
85
+ if (!expiresAt) {
86
+ return;
87
+ }
88
+ if (Date.now() >= expiresAt - TOKEN_REFRESH_THRESHOLD_MS) {
89
+ try {
90
+ await refreshToken();
91
+ } catch {
92
+ }
93
+ }
94
+ }
95
+ async function handleConflictCheck(params) {
96
+ if (typeof params.file !== "string") {
97
+ throw new Error("conflict_check requires `file: string`");
98
+ }
99
+ return await client.post("/api/cli/decisions/conflict-check", { file: params.file });
100
+ }
101
+ function handleStatusSnapshot() {
102
+ const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
103
+ const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
104
+ return {
105
+ pid: process.pid,
106
+ uptimeMs: Date.now() - startedAt,
107
+ sessionId: activeSessionId,
108
+ lastHeartbeatAt,
109
+ // Withhold a frozen count once it's no longer fresh; the statusline shows
110
+ // "presence: stale" rather than a confident, wrong "team: N".
111
+ onlineCount: presenceFresh ? lastOnlineCount : void 0,
112
+ presenceStale
113
+ };
114
+ }
115
+ async function dispatchRequest(req) {
116
+ const id = req.id;
117
+ try {
118
+ switch (req.method) {
119
+ case "conflict_check": {
120
+ const result = await handleConflictCheck(req.params ?? {});
121
+ return { id, ok: true, result };
122
+ }
123
+ case "session_start": {
124
+ const sid = req.params?.sessionId;
125
+ if (typeof sid === "string" && sid.length > 0) {
126
+ activeSessionId = sid;
127
+ }
128
+ await sendHeartbeat();
129
+ return { id, ok: true, result: { sessionId: activeSessionId } };
130
+ }
131
+ case "session_end": {
132
+ return { id, ok: true, result: { ack: true } };
133
+ }
134
+ case "status_snapshot":
135
+ return { id, ok: true, result: handleStatusSnapshot() };
136
+ case "ping":
137
+ return { id, ok: true, result: { pong: true } };
138
+ default:
139
+ return { id, ok: false, error: `unknown method: ${req.method}` };
140
+ }
141
+ } catch (err) {
142
+ return {
143
+ id,
144
+ ok: false,
145
+ error: err instanceof Error ? err.message : String(err)
146
+ };
147
+ }
148
+ }
149
+ function handleConnection(conn) {
150
+ let buffer = "";
151
+ conn.on("data", (chunk) => {
152
+ buffer += chunk.toString("utf-8");
153
+ let newlineIdx = buffer.indexOf("\n");
154
+ while (newlineIdx !== -1) {
155
+ const line = buffer.slice(0, newlineIdx);
156
+ buffer = buffer.slice(newlineIdx + 1);
157
+ if (line.length > 0) {
158
+ try {
159
+ const req = JSON.parse(line);
160
+ dispatchRequest(req).then(
161
+ (res) => {
162
+ conn.write(`${JSON.stringify(res)}
163
+ `);
164
+ },
165
+ () => {
166
+ }
167
+ );
168
+ } catch {
169
+ }
170
+ }
171
+ newlineIdx = buffer.indexOf("\n");
172
+ }
173
+ });
174
+ conn.on("error", () => {
175
+ });
176
+ }
177
+ function startSocketServer() {
178
+ try {
179
+ unlinkSync(SOCK_PATH);
180
+ } catch {
181
+ }
182
+ const server = createServer(handleConnection);
183
+ server.on("error", (err) => {
184
+ process.stderr.write(`[prim-daemon] socket error: ${err.message}
185
+ `);
186
+ });
187
+ server.listen(SOCK_PATH, () => {
188
+ process.stderr.write(`[prim-daemon] listening on ${SOCK_PATH}
189
+ `);
190
+ });
191
+ }
192
+ function startTimers() {
193
+ void sendHeartbeat();
194
+ heartbeatTimer = setInterval(() => {
195
+ void sendHeartbeat();
196
+ }, HEARTBEAT_INTERVAL_MS);
197
+ tokenCheckTimer = setInterval(() => {
198
+ void ensureTokenFresh();
199
+ }, TOKEN_CHECK_INTERVAL_MS);
200
+ }
201
+ function stopTimers() {
202
+ if (heartbeatTimer) {
203
+ clearInterval(heartbeatTimer);
204
+ }
205
+ if (tokenCheckTimer) {
206
+ clearInterval(tokenCheckTimer);
207
+ }
208
+ }
209
+ function installSignalHandlers() {
210
+ for (const signal of ["SIGTERM", "SIGINT"]) {
211
+ process.on(signal, () => {
212
+ process.stderr.write(`[prim-daemon] ${signal}, shutting down (pid=${process.pid})
213
+ `);
214
+ stopTimers();
215
+ cleanup();
216
+ process.exit(EXIT_OK);
217
+ });
218
+ }
219
+ process.on("uncaughtException", (err) => {
220
+ process.stderr.write(`[prim-daemon] uncaught: ${err.message}
221
+ `);
222
+ stopTimers();
223
+ cleanup();
224
+ process.exit(EXIT_CRASH);
225
+ });
226
+ }
227
+ function main() {
228
+ try {
229
+ takePidfile();
230
+ } catch (err) {
231
+ process.stderr.write(`[prim-daemon] ${err instanceof Error ? err.message : String(err)}
232
+ `);
233
+ process.exit(EXIT_CRASH);
234
+ }
235
+ installSignalHandlers();
236
+ startSocketServer();
237
+ startTimers();
238
+ process.stderr.write(`[prim-daemon] started (pid=${process.pid}, session=${activeSessionId})
239
+ `);
240
+ }
241
+ main();
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ bold,
4
+ color
5
+ } from "../chunk-BEEGFDGU.js";
6
+ import {
7
+ getClient
8
+ } from "../chunk-6SIEWWUL.js";
9
+ import {
10
+ scrubFromCwd,
11
+ toMove
12
+ } from "../chunk-PTLXSXIY.js";
13
+
14
+ // src/hooks/post-tool-use.ts
15
+ import { readFileSync } from "fs";
16
+ import { dirname, join } from "path";
17
+ import { fileURLToPath } from "url";
18
+
19
+ // src/hooks/verdict-footer.ts
20
+ function renderVerdictFooter(ctx) {
21
+ const successPrefix = color("\u2713 Conflict caught before merge", "green");
22
+ const savedCount = `${String(ctx.decisionsSaved)}${ctx.decisionsSavedTruncated ? "+" : ""}`;
23
+ const savedFragment = `${savedCount} decisions saved`;
24
+ const intentFragment = `${bold(ctx.author)}'s intent preserved`;
25
+ return `${successPrefix} \xB7 ${savedFragment} \xB7 ${intentFragment}`;
26
+ }
27
+ function isVerdictFooterContext(value) {
28
+ if (typeof value !== "object" || value === null) {
29
+ return false;
30
+ }
31
+ const v = value;
32
+ return typeof v.author === "string" && typeof v.intent === "string" && typeof v.decisionsSaved === "number" && typeof v.decisionsSavedTruncated === "boolean" && typeof v.decisionId === "string";
33
+ }
34
+
35
+ // src/hooks/post-tool-use.ts
36
+ var STDIN_TIMEOUT_MS = 1e3;
37
+ var INGEST_TIMEOUT_MS = 4e3;
38
+ var EDITING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
39
+ var here = dirname(fileURLToPath(import.meta.url));
40
+ function resolveCliVersion() {
41
+ try {
42
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
43
+ return pkg.version ?? "unknown";
44
+ } catch {
45
+ return "unknown";
46
+ }
47
+ }
48
+ function readStdin() {
49
+ return new Promise((resolve, reject) => {
50
+ const chunks = [];
51
+ const timer = setTimeout(() => {
52
+ reject(new Error("stdin read timeout"));
53
+ }, STDIN_TIMEOUT_MS);
54
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
55
+ process.stdin.on("end", () => {
56
+ clearTimeout(timer);
57
+ resolve(Buffer.concat(chunks).toString("utf-8"));
58
+ });
59
+ process.stdin.on("error", (err) => {
60
+ clearTimeout(timer);
61
+ reject(err);
62
+ });
63
+ });
64
+ }
65
+ function emit() {
66
+ process.stdout.write("{}\n");
67
+ }
68
+ function debug(msg) {
69
+ if (process.env.PRIM_HOOK_VERBOSE === "1") {
70
+ process.stderr.write(`[prim-post-tool-use] ${msg}
71
+ `);
72
+ }
73
+ }
74
+ async function ingestMove(move) {
75
+ const client = getClient();
76
+ return await client.post(
77
+ "/api/cli/moves/ingest",
78
+ { batch: [move] },
79
+ { signal: AbortSignal.timeout(INGEST_TIMEOUT_MS) }
80
+ );
81
+ }
82
+ async function main() {
83
+ let raw;
84
+ try {
85
+ raw = await readStdin();
86
+ } catch {
87
+ emit();
88
+ return;
89
+ }
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(raw);
93
+ } catch {
94
+ emit();
95
+ return;
96
+ }
97
+ const envelope = parsed;
98
+ if (envelope.hook_event_name !== "PostToolUse") {
99
+ emit();
100
+ return;
101
+ }
102
+ const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
103
+ if (!EDITING_TOOLS.has(toolName)) {
104
+ emit();
105
+ return;
106
+ }
107
+ if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
108
+ emit();
109
+ return;
110
+ }
111
+ const cwd = parsed.cwd ?? process.cwd();
112
+ const base = toMove(parsed, resolveCliVersion());
113
+ const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
114
+ try {
115
+ const result = await ingestMove(move);
116
+ debug(`ingested ${move.moveId} (${toolName})`);
117
+ if (isVerdictFooterContext(result.verdictFooter)) {
118
+ process.stderr.write(`${renderVerdictFooter(result.verdictFooter)}
119
+ `);
120
+ }
121
+ } catch (err) {
122
+ debug(`ingest failed: ${err instanceof Error ? err.message : String(err)}`);
123
+ }
124
+ emit();
125
+ }
126
+ main().catch(() => {
127
+ emit();
128
+ });
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getClient,
3
+ checkAffectedDecisions,
4
+ formatDecisionsWarning,
4
5
  getGitContext
5
- } from "../chunk-SHLF6OL2.js";
6
+ } from "../chunk-S47B4VGC.js";
7
+ import {
8
+ getClient
9
+ } from "../chunk-6SIEWWUL.js";
6
10
 
7
11
  // src/hooks/pre-commit.ts
8
12
  import { execSync } from "child_process";
@@ -156,8 +160,19 @@ async function syncAffectedSpecs(deps = defaultDeps) {
156
160
  }
157
161
  return synced;
158
162
  }
163
+ async function runDecisionsCheck() {
164
+ const stagedFiles = getStagedFiles();
165
+ if (stagedFiles.length === 0) {
166
+ return { decisions: [], truncated: false };
167
+ }
168
+ return checkAffectedDecisions(stagedFiles);
169
+ }
159
170
  async function main() {
160
- await syncAffectedSpecs();
171
+ const [, decisionsResult] = await Promise.all([syncAffectedSpecs(), runDecisionsCheck()]);
172
+ const warning = formatDecisionsWarning(decisionsResult);
173
+ if (warning) {
174
+ console.error(warning);
175
+ }
161
176
  process.exit(0);
162
177
  }
163
178
  if (!process.env.VITEST) {
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getClient
4
+ } from "../chunk-6SIEWWUL.js";
5
+ import {
6
+ daemonRequest
7
+ } from "../chunk-UTKQTZHL.js";
8
+
9
+ // src/hooks/pre-tool-use-scoring.ts
10
+ import { isAbsolute, relative, sep } from "path";
11
+ var VERDICT_SEVERITY = {
12
+ allow: 0,
13
+ warn: 1,
14
+ ask: 2,
15
+ deny: 3
16
+ };
17
+ function toRepoRelative(filePath, cwd) {
18
+ const rel = isAbsolute(filePath) ? relative(cwd, filePath) : filePath;
19
+ return sep === "/" ? rel : rel.split(sep).join("/");
20
+ }
21
+ function aggregateCheckResults(results) {
22
+ let worst = "allow";
23
+ for (const r of results) {
24
+ if (r.verdict !== "unavailable" && VERDICT_SEVERITY[r.verdict] > VERDICT_SEVERITY[worst]) {
25
+ worst = r.verdict;
26
+ }
27
+ }
28
+ return worst;
29
+ }
30
+ function anyUnverified(results) {
31
+ return results.some((r) => r.verdict === "unavailable" || r.truncated);
32
+ }
33
+ function unverifiedNote(results) {
34
+ const causes = [];
35
+ const unavailable = results.find((r) => r.verdict === "unavailable");
36
+ if (unavailable) {
37
+ causes.push(
38
+ unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
39
+ );
40
+ }
41
+ if (results.some((r) => r.truncated)) {
42
+ causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
43
+ }
44
+ return causes.map((c) => `[primitive] ${c}`).join("\n");
45
+ }
46
+ function buildHookOutput(aggregate, results) {
47
+ if (aggregate === "deny") {
48
+ const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
49
+ return {
50
+ hookSpecificOutput: {
51
+ hookEventName: "PreToolUse",
52
+ permissionDecision: "deny",
53
+ permissionDecisionReason: reason
54
+ }
55
+ };
56
+ }
57
+ if (aggregate === "ask") {
58
+ const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
59
+ const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0).join("\n");
60
+ const out = {
61
+ hookSpecificOutput: {
62
+ hookEventName: "PreToolUse",
63
+ permissionDecision: "ask",
64
+ permissionDecisionReason: reason
65
+ }
66
+ };
67
+ if (additionalContext.length > 0) {
68
+ out.hookSpecificOutput.additionalContext = additionalContext;
69
+ }
70
+ return out;
71
+ }
72
+ const notes = [
73
+ ...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
74
+ anyUnverified(results) ? unverifiedNote(results) : ""
75
+ ].filter((s) => s.length > 0).join("\n");
76
+ if (notes.length > 0) {
77
+ return {
78
+ hookSpecificOutput: {
79
+ hookEventName: "PreToolUse",
80
+ permissionDecision: "allow",
81
+ additionalContext: notes
82
+ }
83
+ };
84
+ }
85
+ return {
86
+ hookSpecificOutput: {
87
+ hookEventName: "PreToolUse",
88
+ permissionDecision: "allow"
89
+ }
90
+ };
91
+ }
92
+ function failOpenOutput() {
93
+ return {
94
+ hookSpecificOutput: {
95
+ hookEventName: "PreToolUse",
96
+ permissionDecision: "allow"
97
+ }
98
+ };
99
+ }
100
+ var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
101
+ function extractFilePaths(toolName, toolInput) {
102
+ if (!SUPPORTED_TOOLS.has(toolName)) {
103
+ return [];
104
+ }
105
+ if (!toolInput || typeof toolInput !== "object") {
106
+ return [];
107
+ }
108
+ const input = toolInput;
109
+ if (typeof input.file_path === "string" && input.file_path.length > 0) {
110
+ return [input.file_path];
111
+ }
112
+ return [];
113
+ }
114
+ function readHookMode(env) {
115
+ if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
116
+ return "off";
117
+ }
118
+ const mode = env.PRIM_HOOK_MODE;
119
+ if (mode === "off" || mode === "warn") {
120
+ return mode;
121
+ }
122
+ return "block";
123
+ }
124
+ function demoteForMode(verdict, mode) {
125
+ if (mode === "off") {
126
+ return "allow";
127
+ }
128
+ if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
129
+ return "warn";
130
+ }
131
+ return verdict;
132
+ }
133
+
134
+ // src/hooks/pre-tool-use.ts
135
+ var HOOK_TIMEOUT_MS = 4500;
136
+ var STDIN_TIMEOUT_MS = 1e3;
137
+ var DAEMON_TIMEOUT_MS = 250;
138
+ async function readStdin() {
139
+ return new Promise((resolve, reject) => {
140
+ const chunks = [];
141
+ const timer = setTimeout(() => {
142
+ reject(new Error("stdin read timeout"));
143
+ }, STDIN_TIMEOUT_MS);
144
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
145
+ process.stdin.on("end", () => {
146
+ clearTimeout(timer);
147
+ resolve(Buffer.concat(chunks).toString("utf-8"));
148
+ });
149
+ process.stdin.on("error", (err) => {
150
+ clearTimeout(timer);
151
+ reject(err);
152
+ });
153
+ });
154
+ }
155
+ function emit(output) {
156
+ process.stdout.write(`${JSON.stringify(output)}
157
+ `);
158
+ }
159
+ async function checkOneFile(file) {
160
+ const fromDaemon = await daemonRequest(
161
+ "conflict_check",
162
+ { file },
163
+ { timeoutMs: DAEMON_TIMEOUT_MS }
164
+ );
165
+ if (fromDaemon) {
166
+ return fromDaemon;
167
+ }
168
+ const client = getClient();
169
+ return await client.post(
170
+ "/api/cli/decisions/conflict-check",
171
+ { file },
172
+ { signal: AbortSignal.timeout(HOOK_TIMEOUT_MS) }
173
+ );
174
+ }
175
+ async function main() {
176
+ let raw;
177
+ try {
178
+ raw = await readStdin();
179
+ } catch {
180
+ emit(failOpenOutput());
181
+ return;
182
+ }
183
+ let envelope;
184
+ try {
185
+ envelope = JSON.parse(raw);
186
+ } catch {
187
+ emit(failOpenOutput());
188
+ return;
189
+ }
190
+ if (envelope.hook_event_name !== "PreToolUse") {
191
+ emit(failOpenOutput());
192
+ return;
193
+ }
194
+ const env = process.env;
195
+ const mode = readHookMode(env);
196
+ if (mode === "off") {
197
+ emit(failOpenOutput());
198
+ return;
199
+ }
200
+ const toolName = typeof envelope.tool_name === "string" ? envelope.tool_name : "";
201
+ const cwd = typeof envelope.cwd === "string" && envelope.cwd.length > 0 ? envelope.cwd : process.cwd();
202
+ const files = extractFilePaths(toolName, envelope.tool_input).map((f) => toRepoRelative(f, cwd));
203
+ if (files.length === 0) {
204
+ emit(failOpenOutput());
205
+ return;
206
+ }
207
+ let results;
208
+ try {
209
+ results = await Promise.all(files.map((f) => checkOneFile(f)));
210
+ } catch {
211
+ emit(failOpenOutput());
212
+ return;
213
+ }
214
+ const rawAggregate = aggregateCheckResults(results);
215
+ const aggregate = demoteForMode(rawAggregate, mode);
216
+ emit(buildHookOutput(aggregate, results));
217
+ }
218
+ main().catch(() => {
219
+ emit(failOpenOutput());
220
+ });