@primitive.ai/prim 0.1.0-alpha.56 → 0.1.0-alpha.57
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/dist/{chunk-ROL7SDXX.js → chunk-GQURYENF.js} +1 -1
- package/dist/{chunk-6LAQVM26.js → chunk-GUXBUBQ7.js} +2 -0
- package/dist/chunk-LDQZASIF.js +166 -0
- package/dist/chunk-SWWBK4MP.js +424 -0
- package/dist/{chunk-F5QCFBJ6.js → chunk-TNNHR3EZ.js} +8 -18
- package/dist/{chunk-HSZPTVKU.js → chunk-YIMUIPMD.js} +20 -15
- package/dist/daemon/server.js +7 -0
- package/dist/hooks/post-commit.js +2 -2
- package/dist/hooks/post-tool-use.js +26 -9
- package/dist/hooks/pre-tool-use.js +89 -230
- package/dist/hooks/prim-hook.js +10 -7
- package/dist/hooks/session-start.js +3 -3
- package/dist/index.js +99 -63
- package/dist/statusline-main.js +10 -8
- package/package.json +2 -1
- package/dist/chunk-R5ZJRSLV.js +0 -82
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/lib/activation.ts
|
|
2
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/lib/git.ts
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { lstatSync, realpathSync } from "fs";
|
|
10
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
11
|
+
function gitToplevel(cwd) {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
17
|
+
}).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function gitValue(cwd, args) {
|
|
23
|
+
try {
|
|
24
|
+
const value = execFileSync("git", args, {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
28
|
+
}).trim();
|
|
29
|
+
return value || null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function githubRepositoryFullName(cwd) {
|
|
35
|
+
const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
|
|
36
|
+
if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
|
|
37
|
+
const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
|
|
38
|
+
remote
|
|
39
|
+
);
|
|
40
|
+
const path = match?.[1] ?? match?.[2];
|
|
41
|
+
if (!path) return null;
|
|
42
|
+
const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
43
|
+
if (parts.length !== 2 || parts.some((part) => !part || part !== part.trim())) return null;
|
|
44
|
+
return `${parts[0]}/${parts[1]}`;
|
|
45
|
+
}
|
|
46
|
+
function canonicalGitRoot(cwd) {
|
|
47
|
+
try {
|
|
48
|
+
const top = gitToplevel(cwd);
|
|
49
|
+
return top ? realpathSync.native(top) : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function canonicalRepositoryPath(filePath, cwd, repositoryRoot = canonicalGitRoot(cwd)) {
|
|
55
|
+
if (!repositoryRoot || !filePath || [...filePath].some((char) => char < " " || char === "\x7F" || "~*?[]{}$`\\".includes(char)))
|
|
56
|
+
return null;
|
|
57
|
+
const absolute = resolve(cwd, filePath);
|
|
58
|
+
let target;
|
|
59
|
+
try {
|
|
60
|
+
const stat = lstatSync(absolute);
|
|
61
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
62
|
+
target = realpathSync.native(absolute);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!(error instanceof Error) || error.code !== "ENOENT") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parent = realpathSync.native(dirname(absolute));
|
|
69
|
+
if (!lstatSync(parent).isDirectory()) return null;
|
|
70
|
+
target = resolve(parent, basename(absolute));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const rel = relative(repositoryRoot, target);
|
|
76
|
+
if (!rel || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return sep === "/" ? rel : rel.split(sep).join("/");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/lib/activation.ts
|
|
83
|
+
var PRIM_ACTIVE_KEY = "prim.active";
|
|
84
|
+
var PRIM_REPO_SYNC_ID_KEY = "prim.repoSyncId";
|
|
85
|
+
var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
|
|
86
|
+
var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
87
|
+
function repoActiveFlag(cwd) {
|
|
88
|
+
try {
|
|
89
|
+
const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
|
|
90
|
+
cwd,
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
93
|
+
}).trim();
|
|
94
|
+
if (value === "true") return "true";
|
|
95
|
+
if (value === "false") return "false";
|
|
96
|
+
return void 0;
|
|
97
|
+
} catch {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isRepoActive(cwd) {
|
|
102
|
+
return repoActiveFlag(cwd) === "true";
|
|
103
|
+
}
|
|
104
|
+
function setRepoActive(cwd, active) {
|
|
105
|
+
execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
|
|
106
|
+
cwd,
|
|
107
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function repoSyncId(cwd) {
|
|
111
|
+
try {
|
|
112
|
+
return execFileSync2("git", ["config", "--local", "--get", PRIM_REPO_SYNC_ID_KEY], {
|
|
113
|
+
cwd,
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
116
|
+
}).trim() || void 0;
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function setRepoSyncId(cwd, value) {
|
|
122
|
+
if (!value || value.includes("\0") || value.includes("\n")) {
|
|
123
|
+
throw new Error("invalid repository binding returned by server");
|
|
124
|
+
}
|
|
125
|
+
execFileSync2("git", ["config", "--local", PRIM_REPO_SYNC_ID_KEY, value], {
|
|
126
|
+
cwd,
|
|
127
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function hasProjectPrimInstall(cwd) {
|
|
131
|
+
const root = gitToplevel(cwd);
|
|
132
|
+
if (root === null || root === homedir()) return false;
|
|
133
|
+
for (const rel of PROJECT_INSTALL_FILES) {
|
|
134
|
+
const path = join(root, rel);
|
|
135
|
+
if (existsSync(path)) {
|
|
136
|
+
try {
|
|
137
|
+
const content = readFileSync(path, "utf-8");
|
|
138
|
+
if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function isRepoActiveForCapture(cwd) {
|
|
146
|
+
const flag = repoActiveFlag(cwd);
|
|
147
|
+
if (flag === "true") return true;
|
|
148
|
+
if (flag === "false") return false;
|
|
149
|
+
return hasProjectPrimInstall(cwd);
|
|
150
|
+
}
|
|
151
|
+
function decisionIngestionStatus(cwd) {
|
|
152
|
+
return isRepoActiveForCapture(cwd) ? "enabled" : "disabled";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
gitToplevel,
|
|
157
|
+
githubRepositoryFullName,
|
|
158
|
+
canonicalGitRoot,
|
|
159
|
+
canonicalRepositoryPath,
|
|
160
|
+
isRepoActive,
|
|
161
|
+
setRepoActive,
|
|
162
|
+
repoSyncId,
|
|
163
|
+
setRepoSyncId,
|
|
164
|
+
isRepoActiveForCapture,
|
|
165
|
+
decisionIngestionStatus
|
|
166
|
+
};
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RULES,
|
|
3
|
+
scrub
|
|
4
|
+
} from "./chunk-GUXBUBQ7.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicalGitRoot,
|
|
7
|
+
canonicalRepositoryPath
|
|
8
|
+
} from "./chunk-LDQZASIF.js";
|
|
9
|
+
|
|
10
|
+
// src/hooks/pre-tool-use-scoring.ts
|
|
11
|
+
import { isAbsolute, relative, sep } from "path";
|
|
12
|
+
function anyUnverified(results) {
|
|
13
|
+
return results.some((r) => r.verdict === "unavailable" || r.truncated);
|
|
14
|
+
}
|
|
15
|
+
function unverifiedNote(results) {
|
|
16
|
+
const causes = [];
|
|
17
|
+
const unavailable = results.find((r) => r.verdict === "unavailable");
|
|
18
|
+
if (unavailable) {
|
|
19
|
+
causes.push(
|
|
20
|
+
unavailable.unavailable ? `decision check skipped \u2014 ${unavailable.unavailable}` : "decision check skipped \u2014 not verified"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (results.some((r) => r.truncated)) {
|
|
24
|
+
causes.push("decision check partial \u2014 conflict set truncated (per-file cap hit)");
|
|
25
|
+
}
|
|
26
|
+
return causes.map((c) => `[primitive] ${c}`).join("\n");
|
|
27
|
+
}
|
|
28
|
+
function buildHookOutput(aggregate, results, agent = "claude_code") {
|
|
29
|
+
if (aggregate === "deny") {
|
|
30
|
+
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)";
|
|
31
|
+
return {
|
|
32
|
+
hookSpecificOutput: {
|
|
33
|
+
hookEventName: "PreToolUse",
|
|
34
|
+
permissionDecision: "deny",
|
|
35
|
+
permissionDecisionReason: reason
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (agent === "codex" && aggregate === "ask") {
|
|
40
|
+
const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
41
|
+
const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
42
|
+
const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
|
|
43
|
+
const out = {
|
|
44
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
|
|
45
|
+
};
|
|
46
|
+
if (merged.length > 0) {
|
|
47
|
+
out.hookSpecificOutput.additionalContext = merged;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
if (aggregate === "ask") {
|
|
52
|
+
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";
|
|
53
|
+
const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
54
|
+
const out = {
|
|
55
|
+
hookSpecificOutput: {
|
|
56
|
+
hookEventName: "PreToolUse",
|
|
57
|
+
permissionDecision: "ask",
|
|
58
|
+
permissionDecisionReason: reason
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (additionalContext.length > 0) {
|
|
62
|
+
out.hookSpecificOutput.additionalContext = additionalContext;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const notes = [
|
|
67
|
+
...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
|
|
68
|
+
anyUnverified(results) ? unverifiedNote(results) : ""
|
|
69
|
+
].filter((s) => s.length > 0).join("\n");
|
|
70
|
+
if (notes.length > 0) {
|
|
71
|
+
return {
|
|
72
|
+
hookSpecificOutput: {
|
|
73
|
+
hookEventName: "PreToolUse",
|
|
74
|
+
permissionDecision: "allow",
|
|
75
|
+
additionalContext: notes
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: "PreToolUse",
|
|
82
|
+
permissionDecision: "allow"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function failOpenOutput() {
|
|
87
|
+
return {
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: "PreToolUse",
|
|
90
|
+
permissionDecision: "allow"
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildHermesOutput(aggregate, results) {
|
|
95
|
+
if (aggregate !== "deny" && aggregate !== "ask") {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
const reason = results.filter((r) => r.verdict === "deny" || r.verdict === "ask").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
|
|
99
|
+
const directive = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
|
|
100
|
+
const message = [reason, directive].filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
|
|
101
|
+
return { action: "block", message };
|
|
102
|
+
}
|
|
103
|
+
function failOpenHermes() {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
107
|
+
var APPLY_PATCH_FILE_RE = /^\*\*\* (?:Update|Add|Delete) File: (?<path>.+)$/;
|
|
108
|
+
var MOVE_FILE_RE = /^\*\*\* Move File:\s*(?<src>.+?)\s*->\s*(?<dst>.+?)\s*$/;
|
|
109
|
+
var MOVE_TO_RE = /^\*\*\* Move to: (?<dst>.+)$/;
|
|
110
|
+
var LINE_SPLIT_RE = /\r?\n/;
|
|
111
|
+
function parseApplyPatchTargets(command) {
|
|
112
|
+
const paths = /* @__PURE__ */ new Set();
|
|
113
|
+
let activeUpdate = false;
|
|
114
|
+
let complete = true;
|
|
115
|
+
for (const line of command.split(LINE_SPLIT_RE)) {
|
|
116
|
+
const path = APPLY_PATCH_FILE_RE.exec(line)?.groups?.path;
|
|
117
|
+
if (path) {
|
|
118
|
+
paths.add(path);
|
|
119
|
+
activeUpdate = line.startsWith("*** Update File:");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const moveTo = MOVE_TO_RE.exec(line)?.groups?.dst;
|
|
123
|
+
if (moveTo) {
|
|
124
|
+
if (activeUpdate) paths.add(moveTo);
|
|
125
|
+
else complete = false;
|
|
126
|
+
activeUpdate = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const move = MOVE_FILE_RE.exec(line)?.groups;
|
|
130
|
+
if (move?.src) {
|
|
131
|
+
paths.add(move.src);
|
|
132
|
+
if (move.dst) {
|
|
133
|
+
paths.add(move.dst);
|
|
134
|
+
}
|
|
135
|
+
activeUpdate = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (line === "*** Begin Patch" || line === "*** End Patch") activeUpdate = false;
|
|
139
|
+
else if (line.startsWith("*** ")) complete = false;
|
|
140
|
+
}
|
|
141
|
+
return { paths: [...paths], complete: complete && paths.size > 0 };
|
|
142
|
+
}
|
|
143
|
+
function extractCodexFileTargets(toolName, toolInput) {
|
|
144
|
+
if (toolName !== "apply_patch") {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
148
|
+
return { paths: [], complete: false };
|
|
149
|
+
}
|
|
150
|
+
const command = toolInput.command;
|
|
151
|
+
return typeof command === "string" ? parseApplyPatchTargets(command) : { paths: [], complete: false };
|
|
152
|
+
}
|
|
153
|
+
var HERMES_EDITING_TOOLS = /* @__PURE__ */ new Set(["write_file", "patch"]);
|
|
154
|
+
function extractHermesFileTargets(toolName, toolInput) {
|
|
155
|
+
if (!HERMES_EDITING_TOOLS.has(toolName)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
159
|
+
return { paths: [], complete: false };
|
|
160
|
+
}
|
|
161
|
+
const input = toolInput;
|
|
162
|
+
if (toolName === "patch" && input.mode === "patch") {
|
|
163
|
+
return typeof input.patch === "string" ? parseApplyPatchTargets(input.patch) : { paths: [], complete: false };
|
|
164
|
+
}
|
|
165
|
+
const paths = typeof input.path === "string" && input.path.length > 0 ? [input.path] : [];
|
|
166
|
+
const mode = input.mode;
|
|
167
|
+
return { paths, complete: paths.length > 0 && (mode === void 0 || mode === "replace") };
|
|
168
|
+
}
|
|
169
|
+
function extractFileTargets(toolName, toolInput, agent = "claude_code") {
|
|
170
|
+
if (agent === "codex") {
|
|
171
|
+
return extractCodexFileTargets(toolName, toolInput);
|
|
172
|
+
}
|
|
173
|
+
if (agent === "hermes") {
|
|
174
|
+
return extractHermesFileTargets(toolName, toolInput);
|
|
175
|
+
}
|
|
176
|
+
if (!SUPPORTED_TOOLS.has(toolName)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
180
|
+
return { paths: [], complete: false };
|
|
181
|
+
}
|
|
182
|
+
const input = toolInput;
|
|
183
|
+
if (toolName === "NotebookEdit") {
|
|
184
|
+
const paths2 = typeof input.notebook_path === "string" && input.notebook_path.length > 0 ? [input.notebook_path] : [];
|
|
185
|
+
return { paths: paths2, complete: paths2.length > 0 };
|
|
186
|
+
}
|
|
187
|
+
const paths = typeof input.file_path === "string" && input.file_path.length > 0 ? [input.file_path] : [];
|
|
188
|
+
return { paths, complete: paths.length > 0 };
|
|
189
|
+
}
|
|
190
|
+
function readHookMode(env) {
|
|
191
|
+
if (env.PRIM_BYPASS === "1" || env.PRIM_BYPASS === "true") {
|
|
192
|
+
return "off";
|
|
193
|
+
}
|
|
194
|
+
const mode = env.PRIM_HOOK_MODE;
|
|
195
|
+
if (mode === "off" || mode === "warn") {
|
|
196
|
+
return mode;
|
|
197
|
+
}
|
|
198
|
+
return "block";
|
|
199
|
+
}
|
|
200
|
+
function demoteForMode(verdict, mode) {
|
|
201
|
+
if (mode === "off") {
|
|
202
|
+
return "allow";
|
|
203
|
+
}
|
|
204
|
+
if (mode === "warn" && (verdict === "ask" || verdict === "deny")) {
|
|
205
|
+
return "warn";
|
|
206
|
+
}
|
|
207
|
+
return verdict;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hooks/preflight-v3.ts
|
|
211
|
+
import { Buffer } from "buffer";
|
|
212
|
+
|
|
213
|
+
// src/hooks/shell-targets.ts
|
|
214
|
+
import { parse } from "unbash";
|
|
215
|
+
var OUTPUT_REDIRECTS = /* @__PURE__ */ new Set([">", ">>", ">|", "&>", "&>>"]);
|
|
216
|
+
var READ_ONLY_COMMANDS = new Set(": cat echo false printf pwd test true [".split(" "));
|
|
217
|
+
var SINKS = /* @__PURE__ */ new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
|
|
218
|
+
var STATIC_PARTS = /* @__PURE__ */ new Set(["Literal", "SingleQuoted", "AnsiCQuoted"]);
|
|
219
|
+
function staticPart(part) {
|
|
220
|
+
return STATIC_PARTS.has(part.type) || part.type === "DoubleQuoted" && part.parts.every(staticPart);
|
|
221
|
+
}
|
|
222
|
+
function staticWord(word) {
|
|
223
|
+
if (!word || !word.value || word.value.includes("\0") || word.value.includes("\n")) return;
|
|
224
|
+
if (word.parts ? !word.parts.every(staticPart) : word.value.startsWith("~") || /[*?\[$`]/.test(word.text))
|
|
225
|
+
return;
|
|
226
|
+
return word.value;
|
|
227
|
+
}
|
|
228
|
+
function unknown(state) {
|
|
229
|
+
state.complete = false;
|
|
230
|
+
state.mutation = true;
|
|
231
|
+
}
|
|
232
|
+
function recordTarget(word, state) {
|
|
233
|
+
const path = staticWord(word);
|
|
234
|
+
if (path === "-" || path && (SINKS.has(path) || path.startsWith("/dev/fd/"))) return;
|
|
235
|
+
state.mutation = true;
|
|
236
|
+
if (path) state.paths.add(path);
|
|
237
|
+
else state.complete = false;
|
|
238
|
+
}
|
|
239
|
+
function visitRedirect(redirect, state) {
|
|
240
|
+
const target = staticWord(redirect.target);
|
|
241
|
+
if (redirect.operator === ">&" && (target === "-" || /^\d+$/.test(target ?? ""))) return;
|
|
242
|
+
if (OUTPUT_REDIRECTS.has(redirect.operator) || redirect.operator === ">&")
|
|
243
|
+
recordTarget(redirect.target, state);
|
|
244
|
+
else unknown(state);
|
|
245
|
+
}
|
|
246
|
+
function operands(words, short = "", long = /* @__PURE__ */ new Set()) {
|
|
247
|
+
const result = [];
|
|
248
|
+
let optionsEnded = false;
|
|
249
|
+
for (const word of words) {
|
|
250
|
+
const value = staticWord(word);
|
|
251
|
+
if (value === void 0) return;
|
|
252
|
+
if (optionsEnded || value === "-" || !value.startsWith("-")) result.push(value);
|
|
253
|
+
else if (value === "--") optionsEnded = true;
|
|
254
|
+
else if (value.startsWith("--") ? !long.has(value) : [...value.slice(1)].some((c) => !short.includes(c)))
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
function visitCommand(command, state) {
|
|
260
|
+
for (const redirect of command.redirects) visitRedirect(redirect, state);
|
|
261
|
+
const name = staticWord(command.name);
|
|
262
|
+
if (!name || name.includes("/") || command.suffix.some((word) => !staticWord(word))) {
|
|
263
|
+
unknown(state);
|
|
264
|
+
} else if (!READ_ONLY_COMMANDS.has(name)) {
|
|
265
|
+
state.mutation = true;
|
|
266
|
+
let targets;
|
|
267
|
+
if (name === "tee") {
|
|
268
|
+
targets = operands(command.suffix, "ai", /* @__PURE__ */ new Set(["--append", "--ignore-interrupts"]));
|
|
269
|
+
} else if (name === "touch") {
|
|
270
|
+
targets = operands(command.suffix);
|
|
271
|
+
} else if (name === "rm") {
|
|
272
|
+
targets = operands(
|
|
273
|
+
command.suffix,
|
|
274
|
+
"fivI",
|
|
275
|
+
/* @__PURE__ */ new Set(["--force", "--interactive", "--interactive=once", "--verbose"])
|
|
276
|
+
);
|
|
277
|
+
} else if (name === "cp" || name === "mv") {
|
|
278
|
+
const pair = operands(command.suffix);
|
|
279
|
+
if (pair?.length === 2) targets = name === "cp" ? [pair[1]] : pair;
|
|
280
|
+
}
|
|
281
|
+
if (!targets?.length) state.complete = false;
|
|
282
|
+
else for (const target of targets) state.paths.add(target);
|
|
283
|
+
}
|
|
284
|
+
visitNested([command.name, command.prefix, command.suffix, command.redirects], state);
|
|
285
|
+
}
|
|
286
|
+
var CONTAINERS = /* @__PURE__ */ new Set(["Script", "Pipeline", "AndOr", "CompoundList"]);
|
|
287
|
+
function visitNested(value, state) {
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const child of value) visitNested(child, state);
|
|
290
|
+
} else if (value && typeof value === "object") {
|
|
291
|
+
const child = value;
|
|
292
|
+
if (child.type === "Command") {
|
|
293
|
+
visitCommand(child, state);
|
|
294
|
+
} else if (child.type === "Statement") {
|
|
295
|
+
visitStatement(child, state);
|
|
296
|
+
} else if ("parts" in child) {
|
|
297
|
+
for (const part of value.parts ?? []) visitNested(part, state);
|
|
298
|
+
} else {
|
|
299
|
+
const node = typeof child.type === "string" && "pos" in child && "end" in child;
|
|
300
|
+
if (node && !CONTAINERS.has(child.type)) unknown(state);
|
|
301
|
+
if (child.type === "Function") return;
|
|
302
|
+
for (const nested of Object.values(child)) visitNested(nested, state);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function visitStatement(statement, state) {
|
|
307
|
+
if (statement.background) unknown(state);
|
|
308
|
+
for (const redirect of statement.redirects) visitRedirect(redirect, state);
|
|
309
|
+
visitNested([statement.redirects, statement.command], state);
|
|
310
|
+
}
|
|
311
|
+
function analyzeShellTargets(source) {
|
|
312
|
+
if (!source.trim())
|
|
313
|
+
return { paths: [], coverage: "complete", mutation: "none" };
|
|
314
|
+
try {
|
|
315
|
+
const script = parse(source);
|
|
316
|
+
const state = { paths: /* @__PURE__ */ new Set(), complete: !script.errors?.length, mutation: false };
|
|
317
|
+
for (const statement of script.commands) visitStatement(statement, state);
|
|
318
|
+
if (script.errors?.length) state.mutation = true;
|
|
319
|
+
return {
|
|
320
|
+
paths: [...state.paths],
|
|
321
|
+
coverage: state.complete ? "complete" : "unverified",
|
|
322
|
+
mutation: state.mutation ? "present" : "none"
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/hooks/preflight-v3.ts
|
|
330
|
+
var PREFLIGHT_PROTOCOL_VERSION = 3;
|
|
331
|
+
var MAX_PREFLIGHT_PATHS = 32;
|
|
332
|
+
var MAX_PROPOSAL_BYTES = 6144;
|
|
333
|
+
function resolvePreflightTargets(args) {
|
|
334
|
+
let rawPaths;
|
|
335
|
+
let coverage = "complete";
|
|
336
|
+
let mutation = "present";
|
|
337
|
+
if (args.agent === "claude_code" && args.toolName === "Bash") {
|
|
338
|
+
const command = args.toolInput && typeof args.toolInput === "object" ? args.toolInput.command : void 0;
|
|
339
|
+
if (typeof command !== "string") {
|
|
340
|
+
return { paths: [], coverage: "unverified", mutation: "present" };
|
|
341
|
+
}
|
|
342
|
+
const shell = analyzeShellTargets(command);
|
|
343
|
+
rawPaths = shell.paths;
|
|
344
|
+
coverage = shell.coverage;
|
|
345
|
+
mutation = shell.mutation;
|
|
346
|
+
} else {
|
|
347
|
+
const extracted = extractFileTargets(args.toolName, args.toolInput, args.agent);
|
|
348
|
+
if (!extracted) return { paths: [], coverage: "complete", mutation: "none" };
|
|
349
|
+
rawPaths = extracted.paths;
|
|
350
|
+
if (!extracted.complete) coverage = "unverified";
|
|
351
|
+
}
|
|
352
|
+
const root = canonicalGitRoot(args.cwd);
|
|
353
|
+
const paths = /* @__PURE__ */ new Set();
|
|
354
|
+
for (const rawPath of rawPaths) {
|
|
355
|
+
const canonical = canonicalRepositoryPath(rawPath, args.cwd, root);
|
|
356
|
+
if (canonical) paths.add(canonical);
|
|
357
|
+
else coverage = "unverified";
|
|
358
|
+
}
|
|
359
|
+
const bounded = [...paths];
|
|
360
|
+
if (bounded.length > MAX_PREFLIGHT_PATHS) coverage = "unverified";
|
|
361
|
+
return { paths: bounded.slice(0, MAX_PREFLIGHT_PATHS), coverage, mutation };
|
|
362
|
+
}
|
|
363
|
+
function truncateUtf8(value, maxBytes) {
|
|
364
|
+
const encoded = Buffer.from(value);
|
|
365
|
+
if (encoded.byteLength <= maxBytes) return value;
|
|
366
|
+
return encoded.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
|
|
367
|
+
}
|
|
368
|
+
function proposalFor(toolInput) {
|
|
369
|
+
const redacted = scrub(toolInput, DEFAULT_RULES);
|
|
370
|
+
return truncateUtf8(JSON.stringify(redacted) ?? "", MAX_PROPOSAL_BYTES);
|
|
371
|
+
}
|
|
372
|
+
function parsePreflightResponse(value) {
|
|
373
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
374
|
+
const response = value;
|
|
375
|
+
const verdicts = /* @__PURE__ */ new Set(["allow", "warn", "ask", "block", "unavailable"]);
|
|
376
|
+
if (response.protocolVersion !== PREFLIGHT_PROTOCOL_VERSION || typeof response.verdict !== "string" || !verdicts.has(response.verdict) || typeof response.reasonCode !== "string" || typeof response.message !== "string" || !Array.isArray(response.conflicts) || !Array.isArray(response.bypassed)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return response;
|
|
380
|
+
}
|
|
381
|
+
function resultForPreflight(response) {
|
|
382
|
+
const verdict = response.verdict === "block" ? "deny" : response.verdict;
|
|
383
|
+
const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
384
|
+
const directives = response.conflicts.flatMap((value) => {
|
|
385
|
+
if (!value || typeof value !== "object") return [];
|
|
386
|
+
const { decisionId, shortId } = value;
|
|
387
|
+
const id = typeof shortId === "string" && safe.test(shortId) ? `dec_${shortId}` : typeof decisionId === "string" && safe.test(decisionId) ? decisionId : void 0;
|
|
388
|
+
return id ? [`To reconcile, run: prim reconcile ${id}`] : [];
|
|
389
|
+
});
|
|
390
|
+
const reason = [response.message, ...new Set(directives)].filter(Boolean).join("\n");
|
|
391
|
+
return {
|
|
392
|
+
verdict,
|
|
393
|
+
conflicts: response.conflicts,
|
|
394
|
+
reason,
|
|
395
|
+
additionalContext: ["warn", "ask", "block"].includes(response.verdict) ? reason : "",
|
|
396
|
+
truncated: false,
|
|
397
|
+
unavailable: response.verdict === "unavailable" ? response.message || response.reasonCode : void 0
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function unverifiedResult(message) {
|
|
401
|
+
return {
|
|
402
|
+
verdict: "unavailable",
|
|
403
|
+
conflicts: [],
|
|
404
|
+
reason: "",
|
|
405
|
+
additionalContext: "",
|
|
406
|
+
truncated: false,
|
|
407
|
+
unavailable: message
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export {
|
|
412
|
+
buildHookOutput,
|
|
413
|
+
failOpenOutput,
|
|
414
|
+
buildHermesOutput,
|
|
415
|
+
failOpenHermes,
|
|
416
|
+
readHookMode,
|
|
417
|
+
demoteForMode,
|
|
418
|
+
PREFLIGHT_PROTOCOL_VERSION,
|
|
419
|
+
resolvePreflightTargets,
|
|
420
|
+
proposalFor,
|
|
421
|
+
parsePreflightResponse,
|
|
422
|
+
resultForPreflight,
|
|
423
|
+
unverifiedResult
|
|
424
|
+
};
|
|
@@ -4,20 +4,12 @@ import { platform } from "os";
|
|
|
4
4
|
|
|
5
5
|
// src/protocol/move.ts
|
|
6
6
|
var LEGACY_ENVELOPE_VERSION = 1;
|
|
7
|
-
var AGENT_ENVELOPE_VERSION =
|
|
7
|
+
var AGENT_ENVELOPE_VERSION = 3;
|
|
8
8
|
var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
|
|
9
|
-
function withAgentProvenance(move, producer, workspaceId) {
|
|
10
|
-
return {
|
|
11
|
-
...move,
|
|
12
|
-
env: { ...move.env, workspaceId },
|
|
13
|
-
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
14
|
-
producer
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
9
|
|
|
18
10
|
// src/hooks/prim-hook-core.ts
|
|
19
|
-
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
|
|
20
|
-
|
|
11
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, invocationId) {
|
|
12
|
+
return {
|
|
21
13
|
moveId: randomUUID(),
|
|
22
14
|
capturedAt: Date.now(),
|
|
23
15
|
sessionId: parsed.session_id ?? "",
|
|
@@ -26,15 +18,13 @@ function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
|
|
|
26
18
|
env: {
|
|
27
19
|
cwd: parsed.cwd ?? process.cwd(),
|
|
28
20
|
cliVersion,
|
|
29
|
-
osPlatform: platform()
|
|
21
|
+
osPlatform: platform(),
|
|
22
|
+
...workspaceId ? { workspaceId } : {}
|
|
30
23
|
},
|
|
31
|
-
envelopeVersion:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// keeping the Claude wire shape byte-identical.
|
|
35
|
-
...agent === "claude_code" ? {} : { producer: agent }
|
|
24
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
25
|
+
producer: agent,
|
|
26
|
+
...invocationId ? { invocationId } : {}
|
|
36
27
|
};
|
|
37
|
-
return workspaceId ? withAgentProvenance(move, agent, workspaceId) : move;
|
|
38
28
|
}
|
|
39
29
|
function toCommitMove(commit, cliVersion, cwd) {
|
|
40
30
|
return {
|