@primitive.ai/prim 0.1.0-alpha.14 → 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,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
+ });
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ appendMove,
4
+ resolveOrg
5
+ } from "../chunk-JZGWQDM5.js";
6
+ import {
7
+ scrubFromCwd,
8
+ shouldFlushAfter,
9
+ toMove
10
+ } from "../chunk-PTLXSXIY.js";
11
+
12
+ // src/hooks/prim-hook.ts
13
+ import { spawn } from "child_process";
14
+ import { readFileSync } from "fs";
15
+ import { dirname, join } from "path";
16
+ import { fileURLToPath } from "url";
17
+ var here = dirname(fileURLToPath(import.meta.url));
18
+ function resolveCliVersion() {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf-8"));
21
+ return pkg.version ?? "unknown";
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ }
26
+ function spawnBackgroundFlush() {
27
+ const entry = join(here, "..", "index.js");
28
+ spawn(process.execPath, [entry, "moves", "flush"], {
29
+ detached: true,
30
+ stdio: "ignore"
31
+ }).unref();
32
+ }
33
+ try {
34
+ const raw = readFileSync(0, "utf-8");
35
+ const parsed = JSON.parse(raw);
36
+ const cwd = parsed.cwd ?? process.cwd();
37
+ const base = toMove(parsed, resolveCliVersion());
38
+ const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
39
+ const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
40
+ appendMove(move, orgId);
41
+ if (shouldFlushAfter(move.eventType)) {
42
+ spawnBackgroundFlush();
43
+ }
44
+ } catch (err) {
45
+ if (process.env.PRIM_HOOK_DEBUG) {
46
+ const detail = err instanceof Error ? err.message : String(err);
47
+ process.stderr.write(`[prim-hook] capture failed: ${detail}
48
+ `);
49
+ }
50
+ }
51
+ process.exit(0);
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonRequest
4
+ } from "../chunk-UTKQTZHL.js";
5
+
6
+ // src/hooks/session-end.ts
7
+ var STDIN_TIMEOUT_MS = 1e3;
8
+ var DAEMON_TIMEOUT_MS = 250;
9
+ function readStdin() {
10
+ return new Promise((resolve, reject) => {
11
+ const chunks = [];
12
+ const timer = setTimeout(() => {
13
+ reject(new Error("stdin read timeout"));
14
+ }, STDIN_TIMEOUT_MS);
15
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
16
+ process.stdin.on("end", () => {
17
+ clearTimeout(timer);
18
+ resolve(Buffer.concat(chunks).toString("utf-8"));
19
+ });
20
+ process.stdin.on("error", (err) => {
21
+ clearTimeout(timer);
22
+ reject(err);
23
+ });
24
+ });
25
+ }
26
+ function emit() {
27
+ process.stdout.write("{}\n");
28
+ }
29
+ async function main() {
30
+ let raw;
31
+ try {
32
+ raw = await readStdin();
33
+ } catch {
34
+ emit();
35
+ return;
36
+ }
37
+ let envelope;
38
+ try {
39
+ envelope = JSON.parse(raw);
40
+ } catch {
41
+ emit();
42
+ return;
43
+ }
44
+ if (envelope.hook_event_name !== "SessionEnd") {
45
+ emit();
46
+ return;
47
+ }
48
+ if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
49
+ emit();
50
+ return;
51
+ }
52
+ await daemonRequest(
53
+ "session_end",
54
+ { sessionId: envelope.session_id },
55
+ { timeoutMs: DAEMON_TIMEOUT_MS }
56
+ );
57
+ emit();
58
+ }
59
+ main().catch(() => {
60
+ emit();
61
+ });
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonRequest
4
+ } from "../chunk-UTKQTZHL.js";
5
+
6
+ // src/hooks/session-start.ts
7
+ var STDIN_TIMEOUT_MS = 1e3;
8
+ var DAEMON_TIMEOUT_MS = 250;
9
+ function readStdin() {
10
+ return new Promise((resolve, reject) => {
11
+ const chunks = [];
12
+ const timer = setTimeout(() => {
13
+ reject(new Error("stdin read timeout"));
14
+ }, STDIN_TIMEOUT_MS);
15
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
16
+ process.stdin.on("end", () => {
17
+ clearTimeout(timer);
18
+ resolve(Buffer.concat(chunks).toString("utf-8"));
19
+ });
20
+ process.stdin.on("error", (err) => {
21
+ clearTimeout(timer);
22
+ reject(err);
23
+ });
24
+ });
25
+ }
26
+ function emit() {
27
+ process.stdout.write("{}\n");
28
+ }
29
+ async function main() {
30
+ let raw;
31
+ try {
32
+ raw = await readStdin();
33
+ } catch {
34
+ emit();
35
+ return;
36
+ }
37
+ let envelope;
38
+ try {
39
+ envelope = JSON.parse(raw);
40
+ } catch {
41
+ emit();
42
+ return;
43
+ }
44
+ if (envelope.hook_event_name !== "SessionStart") {
45
+ emit();
46
+ return;
47
+ }
48
+ if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
49
+ emit();
50
+ return;
51
+ }
52
+ await daemonRequest(
53
+ "session_start",
54
+ { sessionId: envelope.session_id },
55
+ { timeoutMs: DAEMON_TIMEOUT_MS }
56
+ );
57
+ emit();
58
+ }
59
+ main().catch(() => {
60
+ emit();
61
+ });