@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +303 -37
- package/SKILL.md +269 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-5AFRMOPX.js +209 -0
- package/dist/chunk-5GYQ4J3D.js +126 -0
- package/dist/chunk-67VNLLBN.js +789 -0
- package/dist/chunk-BTNNEH2W.js +79 -0
- package/dist/chunk-EWZMDD44.js +1474 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FBAYCS3X.js +327 -0
- package/dist/chunk-FKXFUUWJ.js +36 -0
- package/dist/chunk-FOLKHXLJ.js +1529 -0
- package/dist/chunk-H4UNYFUC.js +196 -0
- package/dist/chunk-HU4PW6I6.js +264 -0
- package/dist/chunk-KFXF2V5G.js +155 -0
- package/dist/chunk-LFFLHSQO.js +78 -0
- package/dist/chunk-M6RIJVRE.js +18358 -0
- package/dist/chunk-NAHHFSLH.js +258 -0
- package/dist/chunk-OAAP2MY6.js +262 -0
- package/dist/chunk-PCVGBE5B.js +659 -0
- package/dist/chunk-VW3FDYRT.js +517 -0
- package/dist/chunk-WX52U7Z2.js +112 -0
- package/dist/chunk-ZB4F75SZ.js +843 -0
- package/dist/daemon/server.js +21633 -0
- package/dist/hook-runtime/daemon/server.js +21633 -0
- package/dist/hook-runtime/hooks/post-commit.js +19258 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +26365 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +25832 -0
- package/dist/hook-runtime/hooks/prim-hook.js +26386 -0
- package/dist/hook-runtime/hooks/session-end.js +1078 -0
- package/dist/hook-runtime/hooks/session-start.js +28779 -0
- package/dist/hook-runtime/index.js +50987 -0
- package/dist/hook-runtime/statusline-main.js +809 -0
- package/dist/hooks/post-commit.js +190 -0
- package/dist/hooks/post-rewrite.js +203 -0
- package/dist/hooks/post-tool-use.js +288 -0
- package/dist/hooks/pre-commit.js +16 -101
- package/dist/hooks/pre-tool-use.js +369 -0
- package/dist/hooks/prim-hook.js +229 -0
- package/dist/hooks/session-end.js +59 -0
- package/dist/hooks/session-start.js +316 -0
- package/dist/index.js +8875 -429
- package/dist/statusline-main.js +809 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/daemon/client.ts
|
|
4
|
+
import { createConnection } from "net";
|
|
5
|
+
import { join as join3 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/lib/paths.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { isAbsolute, join, normalize } from "path";
|
|
10
|
+
var UNSAFE_CONFIG_PATH_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
11
|
+
function absolutePath(value, variable) {
|
|
12
|
+
if (value !== void 0 && UNSAFE_CONFIG_PATH_CHARACTERS.test(value)) {
|
|
13
|
+
throw new Error(`${variable} contains unsafe characters`);
|
|
14
|
+
}
|
|
15
|
+
if (!value || value.trim() !== value || !isAbsolute(value)) return;
|
|
16
|
+
const normalized = normalize(value);
|
|
17
|
+
return normalized === value ? value : void 0;
|
|
18
|
+
}
|
|
19
|
+
function resolvePrimConfigDirectory(options = {}) {
|
|
20
|
+
const env = options.env ?? process.env;
|
|
21
|
+
const explicit = absolutePath(env.PRIM_CONFIG_DIR, "PRIM_CONFIG_DIR");
|
|
22
|
+
if (explicit) return { path: explicit, source: "explicit" };
|
|
23
|
+
const xdgConfigHome = absolutePath(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME");
|
|
24
|
+
if (xdgConfigHome) return { path: join(xdgConfigHome, "prim"), source: "xdg" };
|
|
25
|
+
const home = absolutePath(options.homeDir ?? homedir(), "HOME");
|
|
26
|
+
if (!home) {
|
|
27
|
+
throw new Error("cannot resolve Primitive config directory: HOME is not an absolute path");
|
|
28
|
+
}
|
|
29
|
+
return { path: join(home, ".config", "prim"), source: "default" };
|
|
30
|
+
}
|
|
31
|
+
function primConfigDirectory(options = {}) {
|
|
32
|
+
return resolvePrimConfigDirectory(options).path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/daemon/principal.ts
|
|
36
|
+
import { createHash as createHash2 } from "crypto";
|
|
37
|
+
|
|
38
|
+
// src/lib/credentials.ts
|
|
39
|
+
import { createHash } from "crypto";
|
|
40
|
+
import { readFileSync } from "fs";
|
|
41
|
+
import { join as join2 } from "path";
|
|
42
|
+
|
|
43
|
+
// src/lib/workos-connect.ts
|
|
44
|
+
var DISCOVERY_MAX_BYTES = 16 * 1024;
|
|
45
|
+
var WORKOS_CONNECT_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
46
|
+
var MAX_DEVICE_LIFETIME_SECONDS = 60 * 60;
|
|
47
|
+
var MAX_TOKEN_LIFETIME_SECONDS = 24 * 60 * 60;
|
|
48
|
+
var MAX_URL_LENGTH = 2048;
|
|
49
|
+
var MAX_CLIENT_ID_LENGTH = 256;
|
|
50
|
+
var CONNECT_CLIENT_ID_PATTERN = /^client_[A-Za-z0-9_-]+$/u;
|
|
51
|
+
var WorkosConnectProtocolError = class extends Error {
|
|
52
|
+
code;
|
|
53
|
+
constructor(code, message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "WorkosConnectProtocolError";
|
|
56
|
+
this.code = code;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function recordFrom(value, label) {
|
|
60
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
+
throw new WorkosConnectProtocolError("invalid_response", `${label} must be an object`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function exactString(value, field, maxLength) {
|
|
66
|
+
const containsControl = typeof value === "string" && [...value].some((character) => {
|
|
67
|
+
const code = character.charCodeAt(0);
|
|
68
|
+
return code <= 31 || code === 127;
|
|
69
|
+
});
|
|
70
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || value.trim() !== value || containsControl) {
|
|
71
|
+
throw new WorkosConnectProtocolError("invalid_response", `${field} is invalid`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function canonicalIssuer(value) {
|
|
76
|
+
const issuer = exactString(value, "issuer", MAX_URL_LENGTH);
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(issuer);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
|
|
82
|
+
}
|
|
83
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== issuer) {
|
|
84
|
+
throw new WorkosConnectProtocolError("invalid_response", "issuer must be an HTTPS origin");
|
|
85
|
+
}
|
|
86
|
+
return issuer;
|
|
87
|
+
}
|
|
88
|
+
function parseWorkosConnectCredentialMetadata(value) {
|
|
89
|
+
const record = recordFrom(value, "credential metadata");
|
|
90
|
+
if (record.version !== 1 || record.family !== "workos_connect") {
|
|
91
|
+
throw new WorkosConnectProtocolError("invalid_metadata", "credential metadata is invalid");
|
|
92
|
+
}
|
|
93
|
+
const issuer = canonicalIssuer(record.issuer);
|
|
94
|
+
const clientId = exactString(record.clientId, "clientId", MAX_CLIENT_ID_LENGTH);
|
|
95
|
+
const accessTokenHash = exactString(record.accessTokenHash, "accessTokenHash", 64);
|
|
96
|
+
const refreshTokenHash = exactString(record.refreshTokenHash, "refreshTokenHash", 64);
|
|
97
|
+
if (!CONNECT_CLIENT_ID_PATTERN.test(clientId)) {
|
|
98
|
+
throw new WorkosConnectProtocolError("invalid_metadata", "credential clientId is invalid");
|
|
99
|
+
}
|
|
100
|
+
if (!/^[a-f0-9]{64}$/u.test(accessTokenHash)) {
|
|
101
|
+
throw new WorkosConnectProtocolError(
|
|
102
|
+
"invalid_metadata",
|
|
103
|
+
"credential accessTokenHash is invalid"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!/^[a-f0-9]{64}$/u.test(refreshTokenHash)) {
|
|
107
|
+
throw new WorkosConnectProtocolError(
|
|
108
|
+
"invalid_metadata",
|
|
109
|
+
"credential refreshTokenHash is invalid"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
version: 1,
|
|
114
|
+
family: "workos_connect",
|
|
115
|
+
issuer,
|
|
116
|
+
clientId,
|
|
117
|
+
accessTokenHash,
|
|
118
|
+
refreshTokenHash
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/lib/credentials.ts
|
|
123
|
+
var CONFIG_DIRECTORY = primConfigDirectory();
|
|
124
|
+
var TOKEN_FILE_PATH = join2(CONFIG_DIRECTORY, "token");
|
|
125
|
+
var REFRESH_TOKEN_PATH = join2(CONFIG_DIRECTORY, "refresh_token");
|
|
126
|
+
var TOKEN_EXPIRES_PATH = join2(CONFIG_DIRECTORY, "token_expires_at");
|
|
127
|
+
var TERMINAL_REFRESH_PATH = join2(CONFIG_DIRECTORY, "refresh_terminal");
|
|
128
|
+
var CREDENTIAL_METADATA_PATH = join2(CONFIG_DIRECTORY, "credential_metadata.json");
|
|
129
|
+
var CREDENTIAL_FAMILY_PATH = join2(CONFIG_DIRECTORY, "credential_family.json");
|
|
130
|
+
var CREDENTIAL_MIGRATION_PATH = join2(CONFIG_DIRECTORY, "credential_migration.json");
|
|
131
|
+
var CREDENTIAL_LOCK_PATH = join2(CONFIG_DIRECTORY, "credentials.lock");
|
|
132
|
+
var MAX_CREDENTIAL_METADATA_BYTES = 4096;
|
|
133
|
+
var CREDENTIAL_MIGRATION_VERSION = 1;
|
|
134
|
+
var CREDENTIAL_MIGRATION_STATE = "family_bound";
|
|
135
|
+
function readTrimmed(path) {
|
|
136
|
+
try {
|
|
137
|
+
const value = readFileSync(path, "utf8").trim();
|
|
138
|
+
return value || void 0;
|
|
139
|
+
} catch {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function resolveAuthCredential(options = {}) {
|
|
144
|
+
const envToken = (options.env ?? process.env).PRIM_TOKEN?.trim();
|
|
145
|
+
if (envToken) return { token: envToken, source: "environment" };
|
|
146
|
+
const stored = readTrimmed(options.tokenFilePath ?? TOKEN_FILE_PATH);
|
|
147
|
+
if (!stored) return void 0;
|
|
148
|
+
const metadata = readStoredCredentialMetadata(
|
|
149
|
+
options.metadataPath ?? CREDENTIAL_METADATA_PATH,
|
|
150
|
+
options.familyPath ?? CREDENTIAL_FAMILY_PATH,
|
|
151
|
+
options.migrationPath ?? CREDENTIAL_MIGRATION_PATH
|
|
152
|
+
);
|
|
153
|
+
if (metadata.state === "legacy_broker") {
|
|
154
|
+
if (metadata.metadata) {
|
|
155
|
+
const refreshToken2 = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
|
|
156
|
+
if (!refreshToken2 || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken2)) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { token: stored, source: "token_file" };
|
|
161
|
+
}
|
|
162
|
+
if (metadata.state === "invalid") return void 0;
|
|
163
|
+
const refreshToken = readTrimmed(options.refreshTokenPath ?? REFRESH_TOKEN_PATH);
|
|
164
|
+
if (!refreshToken || metadata.metadata.accessTokenHash !== credentialFingerprint(stored) || metadata.metadata.refreshTokenHash !== credentialFingerprint(refreshToken)) {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
return { token: stored, source: "token_file" };
|
|
168
|
+
}
|
|
169
|
+
function credentialFingerprint(value) {
|
|
170
|
+
return createHash("sha256").update(value).digest("hex");
|
|
171
|
+
}
|
|
172
|
+
function parseLegacyBrokerCredentialMetadata(value) {
|
|
173
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
174
|
+
throw new Error("credential family is invalid");
|
|
175
|
+
}
|
|
176
|
+
const record = value;
|
|
177
|
+
if (record.version !== 1 || record.family !== "legacy_broker" || typeof record.accessTokenHash !== "string" || typeof record.refreshTokenHash !== "string" || !/^[a-f0-9]{64}$/u.test(record.accessTokenHash) || !/^[a-f0-9]{64}$/u.test(record.refreshTokenHash)) {
|
|
178
|
+
throw new Error("credential family is invalid");
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
version: 1,
|
|
182
|
+
family: "legacy_broker",
|
|
183
|
+
accessTokenHash: record.accessTokenHash,
|
|
184
|
+
refreshTokenHash: record.refreshTokenHash
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function readBoundedCredentialJson(path) {
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = readFileSync(path, "utf8");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
193
|
+
}
|
|
194
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_CREDENTIAL_METADATA_BYTES) return null;
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(raw);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function readCredentialFamily(path) {
|
|
202
|
+
const parsed = readBoundedCredentialJson(path);
|
|
203
|
+
if (parsed === void 0) return void 0;
|
|
204
|
+
if (parsed === null) return { state: "invalid" };
|
|
205
|
+
try {
|
|
206
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.family === "legacy_broker") {
|
|
207
|
+
return { state: "legacy_broker", metadata: parseLegacyBrokerCredentialMetadata(parsed) };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
state: "workos_connect",
|
|
211
|
+
metadata: parseWorkosConnectCredentialMetadata(parsed)
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return { state: "invalid" };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function isCredentialMigrationSentinel(value) {
|
|
218
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
219
|
+
const record = value;
|
|
220
|
+
const keys = Object.keys(record).sort();
|
|
221
|
+
return keys.length === 2 && keys[0] === "state" && keys[1] === "version" && record.version === CREDENTIAL_MIGRATION_VERSION && record.state === CREDENTIAL_MIGRATION_STATE;
|
|
222
|
+
}
|
|
223
|
+
function readCredentialMigrationState(path = CREDENTIAL_MIGRATION_PATH) {
|
|
224
|
+
const parsed = readBoundedCredentialJson(path);
|
|
225
|
+
if (parsed === void 0) return "absent";
|
|
226
|
+
return parsed !== null && isCredentialMigrationSentinel(parsed) ? "valid" : "invalid";
|
|
227
|
+
}
|
|
228
|
+
function readStoredCredentialMetadata(metadataPath = CREDENTIAL_METADATA_PATH, familyPath = CREDENTIAL_FAMILY_PATH, migrationPath = CREDENTIAL_MIGRATION_PATH) {
|
|
229
|
+
const family = readCredentialFamily(familyPath);
|
|
230
|
+
if (family) return family;
|
|
231
|
+
const parsed = readBoundedCredentialJson(metadataPath);
|
|
232
|
+
if (parsed === void 0) {
|
|
233
|
+
return readCredentialMigrationState(migrationPath) === "absent" ? { state: "legacy_broker" } : { state: "invalid" };
|
|
234
|
+
}
|
|
235
|
+
if (parsed === null) return { state: "invalid" };
|
|
236
|
+
try {
|
|
237
|
+
return {
|
|
238
|
+
state: "workos_connect",
|
|
239
|
+
metadata: parseWorkosConnectCredentialMetadata(parsed)
|
|
240
|
+
};
|
|
241
|
+
} catch {
|
|
242
|
+
return { state: "invalid" };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function decodeJwtPayload(token) {
|
|
246
|
+
const parts = token.split(".");
|
|
247
|
+
if (parts.length !== 3) return void 0;
|
|
248
|
+
try {
|
|
249
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
250
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return void 0;
|
|
251
|
+
return payload;
|
|
252
|
+
} catch {
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/daemon/principal.ts
|
|
258
|
+
var PRINCIPAL_COMPONENT_MAX_LENGTH = 512;
|
|
259
|
+
function boundedClaim(value) {
|
|
260
|
+
return typeof value === "string" && value.length > 0 && value.length <= PRINCIPAL_COMPONENT_MAX_LENGTH ? value : void 0;
|
|
261
|
+
}
|
|
262
|
+
function sha256(value) {
|
|
263
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
264
|
+
}
|
|
265
|
+
function resolveDaemonPrincipal(token = resolveAuthCredential()?.token) {
|
|
266
|
+
if (!token) return void 0;
|
|
267
|
+
const payload = decodeJwtPayload(token);
|
|
268
|
+
const subject = boundedClaim(payload?.sub);
|
|
269
|
+
const organizationId = boundedClaim(payload?.org_id);
|
|
270
|
+
if (!(subject && organizationId)) return void 0;
|
|
271
|
+
const issuer = boundedClaim(payload?.iss) ?? "unknown-issuer";
|
|
272
|
+
return {
|
|
273
|
+
principalId: sha256(`${issuer}\0${subject}`),
|
|
274
|
+
organizationId,
|
|
275
|
+
credentialFingerprint: sha256(token)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/daemon/protocol.ts
|
|
280
|
+
var DAEMON_JSON_REQUEST_MAX_BYTES = 64 * 1024;
|
|
281
|
+
var DAEMON_JSON_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
282
|
+
function isRecord(value) {
|
|
283
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
284
|
+
}
|
|
285
|
+
function parseDaemonResponseEnvelope(value) {
|
|
286
|
+
if (!isRecord(value)) return void 0;
|
|
287
|
+
if (!(typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0)) {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
if (typeof value.ok !== "boolean") return void 0;
|
|
291
|
+
if (value.error !== void 0 && typeof value.error !== "string") return void 0;
|
|
292
|
+
return {
|
|
293
|
+
id: value.id,
|
|
294
|
+
ok: value.ok,
|
|
295
|
+
...value.result === void 0 ? {} : { result: value.result },
|
|
296
|
+
...value.error === void 0 ? {} : { error: value.error }
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/daemon/client.ts
|
|
301
|
+
var SOCK_PATH = join3(primConfigDirectory(), "sock");
|
|
302
|
+
var DEFAULT_TIMEOUT_MS = 250;
|
|
303
|
+
var nextRequestId = 1;
|
|
304
|
+
function daemonRequest(method, params = {}, opts = {}) {
|
|
305
|
+
const id = nextRequestId++;
|
|
306
|
+
const caller = resolveDaemonPrincipal();
|
|
307
|
+
const request = {
|
|
308
|
+
id,
|
|
309
|
+
method,
|
|
310
|
+
params,
|
|
311
|
+
...caller ? { caller } : {}
|
|
312
|
+
};
|
|
313
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
314
|
+
return new Promise((resolve4) => {
|
|
315
|
+
let settled = false;
|
|
316
|
+
let socket;
|
|
317
|
+
let buffer = Buffer.alloc(0);
|
|
318
|
+
const settle = (value) => {
|
|
319
|
+
if (settled) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
settled = true;
|
|
323
|
+
if (socket) {
|
|
324
|
+
try {
|
|
325
|
+
socket.destroy();
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
resolve4(value);
|
|
330
|
+
};
|
|
331
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
332
|
+
timer.unref();
|
|
333
|
+
try {
|
|
334
|
+
socket = createConnection(SOCK_PATH);
|
|
335
|
+
} catch {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
settle(null);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
socket.on("error", () => {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
settle(null);
|
|
343
|
+
});
|
|
344
|
+
socket.on("connect", () => {
|
|
345
|
+
try {
|
|
346
|
+
socket?.write(`${JSON.stringify(request)}
|
|
347
|
+
`);
|
|
348
|
+
} catch {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
settle(null);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
socket.on("data", (chunk) => {
|
|
354
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
355
|
+
if (buffer.length > DAEMON_JSON_RESPONSE_MAX_BYTES) {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
settle(null);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const newlineIdx = buffer.indexOf(10);
|
|
361
|
+
if (newlineIdx === -1) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const line = buffer.subarray(0, newlineIdx).toString("utf8");
|
|
365
|
+
try {
|
|
366
|
+
const res = parseDaemonResponseEnvelope(JSON.parse(line));
|
|
367
|
+
if (!res || res.id !== id) {
|
|
368
|
+
clearTimeout(timer);
|
|
369
|
+
settle(null);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
settle(res.ok && res.result !== void 0 ? res.result : null);
|
|
374
|
+
} catch {
|
|
375
|
+
clearTimeout(timer);
|
|
376
|
+
settle(null);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
socket.on("end", () => {
|
|
380
|
+
if (!settled) {
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
settle(null);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/hooks/agent.ts
|
|
389
|
+
function parseAgent(argv) {
|
|
390
|
+
const i = argv.indexOf("--agent");
|
|
391
|
+
const value = i !== -1 ? argv[i + 1] : void 0;
|
|
392
|
+
if (value === "codex" || value === "cursor" || value === "hermes") {
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
return "claude_code";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/commands/cursor-install.ts
|
|
399
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
400
|
+
import { homedir as homedir3 } from "os";
|
|
401
|
+
import { isAbsolute as isAbsolute5, join as join8 } from "path";
|
|
402
|
+
|
|
403
|
+
// src/lib/atomic-file.ts
|
|
404
|
+
import { randomUUID } from "crypto";
|
|
405
|
+
import {
|
|
406
|
+
constants,
|
|
407
|
+
closeSync,
|
|
408
|
+
fchmodSync,
|
|
409
|
+
fsyncSync,
|
|
410
|
+
mkdirSync,
|
|
411
|
+
openSync,
|
|
412
|
+
renameSync,
|
|
413
|
+
rmSync,
|
|
414
|
+
writeFileSync
|
|
415
|
+
} from "fs";
|
|
416
|
+
import { dirname } from "path";
|
|
417
|
+
|
|
418
|
+
// src/lib/bin-path.ts
|
|
419
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
420
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
421
|
+
import { fileURLToPath } from "url";
|
|
422
|
+
var PKG_NAME = "@primitive.ai/prim";
|
|
423
|
+
var STABLE_HOOK_LAUNCHER_NAME = "prim-hook-launcher-v1";
|
|
424
|
+
function shellQuote(value) {
|
|
425
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
426
|
+
}
|
|
427
|
+
var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
|
|
428
|
+
var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
|
|
429
|
+
function stableHookCommand(bin, args = "") {
|
|
430
|
+
if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
|
|
431
|
+
throw new Error("invalid stable hook command");
|
|
432
|
+
}
|
|
433
|
+
const resolver = [
|
|
434
|
+
// #242 and earlier identify managed commands by this package marker. Keep
|
|
435
|
+
// it as inert data during the rolling window so an older uninstall or
|
|
436
|
+
// reinstall can remove the stable entry without invoking npm or PATH.
|
|
437
|
+
`prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
|
|
438
|
+
'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
|
|
439
|
+
'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
|
|
440
|
+
'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
|
|
441
|
+
'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
|
|
442
|
+
"fi; fi; ",
|
|
443
|
+
`case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
|
|
444
|
+
'exec "$prim_launcher" "$@"'
|
|
445
|
+
].join("");
|
|
446
|
+
const suffix = args ? ` ${args}` : "";
|
|
447
|
+
return `/bin/sh -c ${shellQuote(resolver)} ${STABLE_HOOK_LAUNCHER_NAME} ${bin}${suffix}`;
|
|
448
|
+
}
|
|
449
|
+
function detachedHookShimCommand(bin, args = "") {
|
|
450
|
+
return `payload=$(cat); { trap '' HUP; printf '%s' "$payload" | { ${stableHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/lib/hook-runtime.ts
|
|
454
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
455
|
+
import {
|
|
456
|
+
constants as constants2,
|
|
457
|
+
accessSync,
|
|
458
|
+
chmodSync,
|
|
459
|
+
copyFileSync,
|
|
460
|
+
existsSync as existsSync3,
|
|
461
|
+
lstatSync,
|
|
462
|
+
mkdirSync as mkdirSync3,
|
|
463
|
+
mkdtempSync,
|
|
464
|
+
readFileSync as readFileSync4,
|
|
465
|
+
readdirSync,
|
|
466
|
+
realpathSync,
|
|
467
|
+
renameSync as renameSync2,
|
|
468
|
+
rmSync as rmSync3,
|
|
469
|
+
statSync as statSync2,
|
|
470
|
+
utimesSync,
|
|
471
|
+
writeFileSync as writeFileSync3
|
|
472
|
+
} from "fs";
|
|
473
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join6, resolve } from "path";
|
|
474
|
+
|
|
475
|
+
// src/lib/file-lock.ts
|
|
476
|
+
import { randomBytes } from "crypto";
|
|
477
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
478
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
479
|
+
var syncSleeper = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
480
|
+
|
|
481
|
+
// src/lib/hook-runtime.ts
|
|
482
|
+
var RELEASE_PREFIX = "release-";
|
|
483
|
+
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
484
|
+
var RELEASE_GRACE_MS = 10 * 60 * 1e3;
|
|
485
|
+
var HOOK_RUNTIME_ENTRIES = {
|
|
486
|
+
// SessionStart self-healing resolves this exact CLI entry to run
|
|
487
|
+
// `daemon ensure`; it must travel with the immutable hook release.
|
|
488
|
+
prim: "dist/index.js",
|
|
489
|
+
"prim-hook": "dist/hooks/prim-hook.js",
|
|
490
|
+
"prim-pre-commit": "dist/hooks/pre-commit.js",
|
|
491
|
+
"prim-post-commit": "dist/hooks/post-commit.js",
|
|
492
|
+
"prim-post-rewrite": "dist/hooks/post-rewrite.js",
|
|
493
|
+
"prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
|
|
494
|
+
"prim-post-tool-use": "dist/hooks/post-tool-use.js",
|
|
495
|
+
"prim-session-start": "dist/hooks/session-start.js",
|
|
496
|
+
"prim-session-end": "dist/hooks/session-end.js",
|
|
497
|
+
"prim-daemon-server": "dist/daemon/server.js",
|
|
498
|
+
"prim-statusline": "dist/statusline-main.js"
|
|
499
|
+
};
|
|
500
|
+
var SOURCE_ENTRIES = Object.fromEntries(
|
|
501
|
+
Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, target]) => [
|
|
502
|
+
bin,
|
|
503
|
+
target.replace(/^dist\//u, "")
|
|
504
|
+
])
|
|
505
|
+
);
|
|
506
|
+
var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
|
|
507
|
+
set -eu
|
|
508
|
+
case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
|
|
509
|
+
IFS= read -r prim_release_name < "$prim_launcher_dir/hook-runtime/current" || exit 69
|
|
510
|
+
case "$prim_release_name" in ${RELEASE_PREFIX}*) ;; *) exit 69 ;; esac
|
|
511
|
+
prim_release_id=\${prim_release_name#${RELEASE_PREFIX}}
|
|
512
|
+
case "$prim_release_id" in *[!0-9a-f]*|"") exit 69 ;; esac
|
|
513
|
+
[ "\${#prim_release_id}" -eq 64 ] || exit 69
|
|
514
|
+
prim_release="$prim_launcher_dir/hook-runtime/releases/$prim_release_name"
|
|
515
|
+
case "\${1-}" in
|
|
516
|
+
${Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, entry]) => ` ${bin}) prim_entry="$prim_release/${entry}" ;;`).join("\n")}
|
|
517
|
+
*) exit 64 ;;
|
|
518
|
+
esac
|
|
519
|
+
shift
|
|
520
|
+
IFS= read -r prim_node < "$prim_release/node" || exit 69
|
|
521
|
+
case "$prim_node" in /*) ;; *) exit 69 ;; esac
|
|
522
|
+
[ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
|
|
523
|
+
exec "$prim_node" "$prim_entry" "$@"
|
|
524
|
+
`;
|
|
525
|
+
|
|
526
|
+
// src/commands/claude-install.ts
|
|
527
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
528
|
+
import { homedir as homedir2 } from "os";
|
|
529
|
+
import { join as join7 } from "path";
|
|
530
|
+
|
|
531
|
+
// src/lib/git.ts
|
|
532
|
+
import { execFileSync } from "child_process";
|
|
533
|
+
import { createHash as createHash4 } from "crypto";
|
|
534
|
+
import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
|
|
535
|
+
import { basename, dirname as dirname5, isAbsolute as isAbsolute4, relative, resolve as resolve2, sep } from "path";
|
|
536
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
537
|
+
var GITHUB_OWNER_PATTERN = /^(?![A-Za-z0-9-]*--)[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u;
|
|
538
|
+
var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9._-]{1,100}$/u;
|
|
539
|
+
function gitToplevel(cwd) {
|
|
540
|
+
try {
|
|
541
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
542
|
+
cwd,
|
|
543
|
+
encoding: "utf-8",
|
|
544
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
545
|
+
timeout: GIT_TIMEOUT_MS
|
|
546
|
+
}).trim();
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function gitValue(cwd, args) {
|
|
552
|
+
try {
|
|
553
|
+
const value = execFileSync("git", args, {
|
|
554
|
+
cwd,
|
|
555
|
+
encoding: "utf-8",
|
|
556
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
557
|
+
timeout: GIT_TIMEOUT_MS
|
|
558
|
+
}).trim();
|
|
559
|
+
return value || void 0;
|
|
560
|
+
} catch {
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function githubRepositoryFullName(cwd) {
|
|
565
|
+
const remote = gitValue(cwd, ["config", "--get", "remote.origin.url"]);
|
|
566
|
+
if (!remote || [...remote].some((char) => char < " " || char === "\x7F")) return null;
|
|
567
|
+
const match = /^(?:git@github\.com:([^?#]+)|(?:https|ssh):\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/([^?#]+))$/i.exec(
|
|
568
|
+
remote
|
|
569
|
+
);
|
|
570
|
+
const path = match?.[1] ?? match?.[2];
|
|
571
|
+
if (!path) return null;
|
|
572
|
+
const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
|
|
573
|
+
if (parts.length !== 2 || !GITHUB_OWNER_PATTERN.test(parts[0] ?? "") || !GITHUB_REPOSITORY_PATTERN.test(parts[1] ?? "")) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
return `${parts[0]}/${parts[1]}`;
|
|
577
|
+
}
|
|
578
|
+
function normalizeOriginRemote(remote) {
|
|
579
|
+
const value = remote.trim();
|
|
580
|
+
if (!value) return void 0;
|
|
581
|
+
const scp = !value.includes("://") ? /^(?:[^@/\s]+@)?(?<host>[^:/\s]+):(?<path>.+)$/.exec(value)?.groups : void 0;
|
|
582
|
+
if (scp?.host && scp.path) {
|
|
583
|
+
const path = scp.path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
584
|
+
return path ? `${scp.host.toLowerCase()}/${path}` : void 0;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const url = new URL(value);
|
|
588
|
+
url.username = "";
|
|
589
|
+
url.password = "";
|
|
590
|
+
url.search = "";
|
|
591
|
+
url.hash = "";
|
|
592
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
593
|
+
if (url.host && path) return `${url.host.toLowerCase()}/${path}`;
|
|
594
|
+
return void 0;
|
|
595
|
+
} catch {
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function opaqueRepoKey(source, value) {
|
|
600
|
+
return `repo_v1_${createHash4("sha256").update(`${source}\0${value}`).digest("hex")}`;
|
|
601
|
+
}
|
|
602
|
+
function resolveRepositoryContext(cwd = process.cwd()) {
|
|
603
|
+
const top = gitToplevel(cwd);
|
|
604
|
+
if (!top) return null;
|
|
605
|
+
let repoRoot;
|
|
606
|
+
try {
|
|
607
|
+
repoRoot = realpathSync2.native(top);
|
|
608
|
+
} catch {
|
|
609
|
+
repoRoot = resolve2(top);
|
|
610
|
+
}
|
|
611
|
+
const origin = gitValue(repoRoot, ["config", "--get", "remote.origin.url"]);
|
|
612
|
+
const repoFullName = githubRepositoryFullName(repoRoot) ?? void 0;
|
|
613
|
+
const normalizedOrigin = origin ? normalizeOriginRemote(origin) : void 0;
|
|
614
|
+
if (normalizedOrigin) {
|
|
615
|
+
return {
|
|
616
|
+
repoRoot,
|
|
617
|
+
repoKey: opaqueRepoKey("origin", normalizedOrigin),
|
|
618
|
+
identitySource: "origin",
|
|
619
|
+
repoFullName
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const roots = gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"])?.split(/\s+/).filter(Boolean).sort();
|
|
623
|
+
if (roots && roots.length > 0) {
|
|
624
|
+
return {
|
|
625
|
+
repoRoot,
|
|
626
|
+
repoKey: opaqueRepoKey("root_commit", roots.join("\n")),
|
|
627
|
+
identitySource: "root_commit",
|
|
628
|
+
repoFullName
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return { repoRoot, repoFullName };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/commands/claude-install.ts
|
|
635
|
+
var CAPTURE_BIN = "prim-hook";
|
|
636
|
+
var GATE_BIN = "prim-pre-tool-use";
|
|
637
|
+
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
638
|
+
var SESSION_START_BIN = "prim-session-start";
|
|
639
|
+
var SESSION_END_BIN = "prim-session-end";
|
|
640
|
+
var STATUSLINE_BIN = "prim-statusline";
|
|
641
|
+
var STATUSLINE_ARGS = "";
|
|
642
|
+
var STATUSLINE_COMMAND = stableHookCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
|
|
643
|
+
var USER_SCOPE_PATH = join7(homedir2(), ".claude", "settings.json");
|
|
644
|
+
function projectRoot() {
|
|
645
|
+
return gitToplevel() ?? process.cwd();
|
|
646
|
+
}
|
|
647
|
+
var CAPTURE_EVENTS = [
|
|
648
|
+
"SessionStart",
|
|
649
|
+
"UserPromptSubmit",
|
|
650
|
+
"PreToolUse",
|
|
651
|
+
"PostToolUse",
|
|
652
|
+
"PostToolUseFailure",
|
|
653
|
+
"Stop",
|
|
654
|
+
"SessionEnd",
|
|
655
|
+
"SubagentStop"
|
|
656
|
+
];
|
|
657
|
+
function makeRegistration(event, matcher, bin, args = "") {
|
|
658
|
+
return {
|
|
659
|
+
event,
|
|
660
|
+
matcher,
|
|
661
|
+
bin,
|
|
662
|
+
command: stableHookCommand(bin, args)
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function makeDetachedRegistration(event, matcher, bin, args = "") {
|
|
666
|
+
return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
|
|
667
|
+
}
|
|
668
|
+
var REGISTRATIONS = [
|
|
669
|
+
...CAPTURE_EVENTS.map(
|
|
670
|
+
(event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
|
|
671
|
+
),
|
|
672
|
+
makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
|
|
673
|
+
makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
|
|
674
|
+
makeRegistration(
|
|
675
|
+
"PostToolUseFailure",
|
|
676
|
+
"Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
677
|
+
POST_TOOL_USE_BIN
|
|
678
|
+
),
|
|
679
|
+
makeRegistration("SessionStart", "*", SESSION_START_BIN),
|
|
680
|
+
makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
681
|
+
];
|
|
682
|
+
var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
|
|
683
|
+
var LEGACY_PRIM_PERMISSION_RULES = [
|
|
684
|
+
"Bash(npx --yes @primitive.ai/prim@latest:*)",
|
|
685
|
+
"Bash(npx --yes @primitive.ai/prim:*)"
|
|
686
|
+
];
|
|
687
|
+
var ALL_PRIM_PERMISSION_RULES = /* @__PURE__ */ new Set([
|
|
688
|
+
PRIM_PERMISSION_RULE,
|
|
689
|
+
...LEGACY_PRIM_PERMISSION_RULES
|
|
690
|
+
]);
|
|
691
|
+
|
|
692
|
+
// src/commands/cursor-install.ts
|
|
693
|
+
var CAPTURE_BIN2 = "prim-hook";
|
|
694
|
+
var GATE_BIN2 = "prim-pre-tool-use";
|
|
695
|
+
var POST_TOOL_BIN = "prim-post-tool-use";
|
|
696
|
+
var SESSION_START_BIN2 = "prim-session-start";
|
|
697
|
+
var SESSION_END_BIN2 = "prim-session-end";
|
|
698
|
+
var STATUSLINE_BIN2 = "prim-statusline";
|
|
699
|
+
var CURSOR_EVENTS = [
|
|
700
|
+
"sessionStart",
|
|
701
|
+
"beforeSubmitPrompt",
|
|
702
|
+
"preToolUse",
|
|
703
|
+
"postToolUse",
|
|
704
|
+
"postToolUseFailure",
|
|
705
|
+
"afterAgentResponse",
|
|
706
|
+
"subagentStop",
|
|
707
|
+
"stop",
|
|
708
|
+
"sessionEnd"
|
|
709
|
+
];
|
|
710
|
+
var REGISTRATIONS2 = [
|
|
711
|
+
...CURSOR_EVENTS.map((event) => ({ event, bin: CAPTURE_BIN2 })),
|
|
712
|
+
{ event: "preToolUse", bin: GATE_BIN2, matcher: "Write|Delete|Shell", timeout: 10 },
|
|
713
|
+
{ event: "postToolUse", bin: POST_TOOL_BIN },
|
|
714
|
+
{ event: "postToolUseFailure", bin: POST_TOOL_BIN },
|
|
715
|
+
{ event: "sessionStart", bin: SESSION_START_BIN2 },
|
|
716
|
+
{ event: "sessionEnd", bin: SESSION_END_BIN2 }
|
|
717
|
+
];
|
|
718
|
+
function cursorConfigDirectory(env = process.env, home = homedir3(), platform = process.platform) {
|
|
719
|
+
const explicit = env.CURSOR_CONFIG_DIR?.trim();
|
|
720
|
+
if (explicit && isAbsolute5(explicit)) return explicit;
|
|
721
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
722
|
+
if (platform !== "darwin" && xdg && isAbsolute5(xdg)) return join8(xdg, "cursor");
|
|
723
|
+
return join8(home, ".cursor");
|
|
724
|
+
}
|
|
725
|
+
function cursorHooksPath(scope, root = projectRoot()) {
|
|
726
|
+
return scope === "user" ? join8(cursorConfigDirectory(), "hooks.json") : join8(root, ".cursor", "hooks.json");
|
|
727
|
+
}
|
|
728
|
+
function plainObject(value) {
|
|
729
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
730
|
+
}
|
|
731
|
+
function readCursorHooks(path) {
|
|
732
|
+
if (!existsSync5(path)) return {};
|
|
733
|
+
let parsed;
|
|
734
|
+
try {
|
|
735
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
736
|
+
} catch {
|
|
737
|
+
throw new Error(`invalid Cursor hooks JSON at ${path}`);
|
|
738
|
+
}
|
|
739
|
+
if (!plainObject(parsed)) throw new Error(`Cursor hooks config must be an object: ${path}`);
|
|
740
|
+
if (parsed.version !== void 0 && parsed.version !== 1) {
|
|
741
|
+
throw new Error(`unsupported Cursor hooks version at ${path} (expected version 1)`);
|
|
742
|
+
}
|
|
743
|
+
if (parsed.hooks !== void 0) {
|
|
744
|
+
if (!plainObject(parsed.hooks)) throw new Error(`Cursor hooks must be an object: ${path}`);
|
|
745
|
+
for (const [event, entries] of Object.entries(parsed.hooks)) {
|
|
746
|
+
if (!Array.isArray(entries) || !entries.every(plainObject)) {
|
|
747
|
+
throw new Error(`Cursor hook list must contain objects: ${event} in ${path}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return parsed;
|
|
752
|
+
}
|
|
753
|
+
function commandFor(registration, scope) {
|
|
754
|
+
return stableHookCommand(
|
|
755
|
+
registration.bin,
|
|
756
|
+
`--agent cursor --event ${registration.event} --scope ${scope}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
var OWNED_COMMANDS = new Set(
|
|
760
|
+
["project", "user"].flatMap(
|
|
761
|
+
(scope) => REGISTRATIONS2.map((registration) => commandFor(registration, scope))
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
function jsoncUnsafeCommand(command) {
|
|
765
|
+
return command.replace('case "$1" in [/]* ', 'case "$1" in /*').replace("*[/][/]*", "*//*").replace("*[/].[/]*", "*/./*").replace("*[/]..[/]*", "*/../*").replace("*[/].", "*/.").replace("*[/]..", "*/..").replace("?*[/]", "?*/");
|
|
766
|
+
}
|
|
767
|
+
var JSONC_UNSAFE_OWNED_COMMANDS = new Set([...OWNED_COMMANDS].map(jsoncUnsafeCommand));
|
|
768
|
+
function hookFor(registration, scope) {
|
|
769
|
+
return {
|
|
770
|
+
command: commandFor(registration, scope),
|
|
771
|
+
...registration.matcher ? { matcher: registration.matcher } : {},
|
|
772
|
+
...registration.timeout ? { timeout: registration.timeout } : {}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function exactHook(entry, registration, scope) {
|
|
776
|
+
return JSON.stringify(entry) === JSON.stringify(hookFor(registration, scope));
|
|
777
|
+
}
|
|
778
|
+
function hasExactCursorHandler(settings, event, bin, scope) {
|
|
779
|
+
const registration = REGISTRATIONS2.find(
|
|
780
|
+
(candidate) => candidate.event === event && candidate.bin === bin
|
|
781
|
+
);
|
|
782
|
+
return registration ? (settings.hooks?.[event] ?? []).some((entry) => exactHook(entry, registration, scope)) : false;
|
|
783
|
+
}
|
|
784
|
+
var FOOTER = {
|
|
785
|
+
type: "command",
|
|
786
|
+
command: stableHookCommand(STATUSLINE_BIN2),
|
|
787
|
+
updateIntervalMs: 1e3,
|
|
788
|
+
timeoutMs: 2e3
|
|
789
|
+
};
|
|
790
|
+
var JSONC_UNSAFE_FOOTER = {
|
|
791
|
+
...FOOTER,
|
|
792
|
+
command: jsoncUnsafeCommand(FOOTER.command)
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// src/hooks/cursor-coexistence.ts
|
|
796
|
+
var CURSOR_EVENTS2 = /* @__PURE__ */ new Set([
|
|
797
|
+
"sessionStart",
|
|
798
|
+
"beforeSubmitPrompt",
|
|
799
|
+
"preToolUse",
|
|
800
|
+
"postToolUse",
|
|
801
|
+
"postToolUseFailure",
|
|
802
|
+
"afterAgentResponse",
|
|
803
|
+
"subagentStop",
|
|
804
|
+
"stop",
|
|
805
|
+
"sessionEnd"
|
|
806
|
+
]);
|
|
807
|
+
function cursorEvidence(parsed) {
|
|
808
|
+
return typeof parsed.cursor_version === "string" && parsed.cursor_version.length > 0 && typeof parsed.conversation_id === "string" && parsed.conversation_id.length > 0 && typeof parsed.generation_id === "string" && parsed.generation_id.length > 0 && Array.isArray(parsed.workspace_roots) && typeof parsed.hook_event_name === "string" && CURSOR_EVENTS2.has(parsed.hook_event_name);
|
|
809
|
+
}
|
|
810
|
+
function uniqueProjectRoot(parsed) {
|
|
811
|
+
if (!Array.isArray(parsed.workspace_roots)) return;
|
|
812
|
+
const roots = new Set(
|
|
813
|
+
parsed.workspace_roots.filter((value) => typeof value === "string" && value.length > 0).map((value) => resolveRepositoryContext(value)?.repoRoot).filter((value) => !!value)
|
|
814
|
+
);
|
|
815
|
+
return roots.size === 1 ? [...roots][0] : void 0;
|
|
816
|
+
}
|
|
817
|
+
function shouldSuppressImportedCursorHandler(parsed, bin) {
|
|
818
|
+
if (!cursorEvidence(parsed)) return false;
|
|
819
|
+
const project = uniqueProjectRoot(parsed);
|
|
820
|
+
if (project) {
|
|
821
|
+
try {
|
|
822
|
+
if (hasExactCursorHandler(
|
|
823
|
+
readCursorHooks(cursorHooksPath("project", project)),
|
|
824
|
+
parsed.hook_event_name,
|
|
825
|
+
bin,
|
|
826
|
+
"project"
|
|
827
|
+
)) {
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
return hasExactCursorHandler(
|
|
835
|
+
readCursorHooks(cursorHooksPath("user")),
|
|
836
|
+
parsed.hook_event_name,
|
|
837
|
+
bin,
|
|
838
|
+
"user"
|
|
839
|
+
);
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/hooks/hook-stdin.ts
|
|
846
|
+
var DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
847
|
+
function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
|
|
848
|
+
return new Promise((resolve4, reject) => {
|
|
849
|
+
const chunks = [];
|
|
850
|
+
let bytes = 0;
|
|
851
|
+
let settled = false;
|
|
852
|
+
const finish = (error) => {
|
|
853
|
+
if (settled) return;
|
|
854
|
+
settled = true;
|
|
855
|
+
clearTimeout(timer);
|
|
856
|
+
process.stdin.removeAllListeners("data");
|
|
857
|
+
process.stdin.removeAllListeners("end");
|
|
858
|
+
process.stdin.removeAllListeners("error");
|
|
859
|
+
if (error) reject(error);
|
|
860
|
+
else resolve4(Buffer.concat(chunks, bytes).toString("utf-8"));
|
|
861
|
+
};
|
|
862
|
+
const timer = setTimeout(() => finish(new Error("stdin read timeout")), timeoutMs);
|
|
863
|
+
process.stdin.on("data", (chunk) => {
|
|
864
|
+
bytes += chunk.length;
|
|
865
|
+
if (bytes > maxBytes) {
|
|
866
|
+
finish(new Error("stdin envelope exceeds size limit"));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
chunks.push(chunk);
|
|
870
|
+
});
|
|
871
|
+
process.stdin.on("end", () => finish());
|
|
872
|
+
process.stdin.on("error", (error) => finish(error));
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/hooks/normalize.ts
|
|
877
|
+
import { createHash as createHash5 } from "crypto";
|
|
878
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
879
|
+
import { isAbsolute as isAbsolute6, resolve as resolve3 } from "path";
|
|
880
|
+
var HERMES_EVENT_MAP = {
|
|
881
|
+
on_session_start: "SessionStart",
|
|
882
|
+
on_session_end: "SessionEnd",
|
|
883
|
+
pre_llm_call: "UserPromptSubmit",
|
|
884
|
+
post_llm_call: "Stop",
|
|
885
|
+
pre_tool_call: "PreToolUse",
|
|
886
|
+
post_tool_call: "PostToolUse",
|
|
887
|
+
subagent_stop: "SubagentStop"
|
|
888
|
+
};
|
|
889
|
+
var CURSOR_EVENT_MAP = {
|
|
890
|
+
sessionStart: "SessionStart",
|
|
891
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
892
|
+
preToolUse: "PreToolUse",
|
|
893
|
+
postToolUse: "PostToolUse",
|
|
894
|
+
postToolUseFailure: "PostToolUseFailure",
|
|
895
|
+
afterAgentResponse: "AssistantResponse",
|
|
896
|
+
subagentStop: "SubagentStop",
|
|
897
|
+
stop: "Stop",
|
|
898
|
+
sessionEnd: "SessionEnd"
|
|
899
|
+
};
|
|
900
|
+
var CURSOR_TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
|
|
901
|
+
var SAFE_CORRELATION_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
902
|
+
var CURSOR_LOCAL_ONLY_FIELDS = /* @__PURE__ */ new Set([
|
|
903
|
+
"conversation_id",
|
|
904
|
+
"generation_id",
|
|
905
|
+
"user_email",
|
|
906
|
+
"account_email",
|
|
907
|
+
"transcript_path",
|
|
908
|
+
"transcript",
|
|
909
|
+
"thought",
|
|
910
|
+
"thoughts",
|
|
911
|
+
"thinking",
|
|
912
|
+
"tool_output"
|
|
913
|
+
]);
|
|
914
|
+
function requiredId(value, field) {
|
|
915
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
916
|
+
throw new TypeError(`Cursor hook envelope is missing ${field}`);
|
|
917
|
+
}
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
920
|
+
function cursorToolUseId(value) {
|
|
921
|
+
if (SAFE_CORRELATION_ID_RE.test(value)) return value;
|
|
922
|
+
return `cursor:tool:v1:${createHash5("sha256").update(value).digest("hex")}`;
|
|
923
|
+
}
|
|
924
|
+
function workspaceCandidates(value) {
|
|
925
|
+
if (!Array.isArray(value)) return [];
|
|
926
|
+
const roots = value.filter((item) => typeof item === "string" && item.length > 0);
|
|
927
|
+
if (roots.some((root) => !isAbsolute6(root))) {
|
|
928
|
+
throw new TypeError("Cursor workspace roots must be absolute");
|
|
929
|
+
}
|
|
930
|
+
return roots;
|
|
931
|
+
}
|
|
932
|
+
function pathIdentity(path) {
|
|
933
|
+
try {
|
|
934
|
+
return realpathSync3.native(path);
|
|
935
|
+
} catch {
|
|
936
|
+
return resolve3(path);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function resolveCursorCwd(parsed) {
|
|
940
|
+
const roots = workspaceCandidates(parsed.workspace_roots).map(pathIdentity);
|
|
941
|
+
const rootRepositories = roots.map((root) => resolveRepositoryContext(root)?.repoRoot);
|
|
942
|
+
const repositoryRoots = new Set(rootRepositories.filter((root) => !!root));
|
|
943
|
+
if (repositoryRoots.size > 1) {
|
|
944
|
+
throw new TypeError("Cursor hook envelope spans multiple repositories");
|
|
945
|
+
}
|
|
946
|
+
if (roots.length > 1 && rootRepositories.some((root) => root === void 0)) {
|
|
947
|
+
throw new TypeError("Cursor hook envelope has ambiguous workspace roots");
|
|
948
|
+
}
|
|
949
|
+
const expectedRepo = [...repositoryRoots][0];
|
|
950
|
+
const rawCwd = typeof parsed.cwd === "string" && parsed.cwd.length > 0 ? parsed.cwd : void 0;
|
|
951
|
+
if (rawCwd && !isAbsolute6(rawCwd)) {
|
|
952
|
+
throw new TypeError("Cursor hook cwd must be absolute");
|
|
953
|
+
}
|
|
954
|
+
const envelopeCwd = rawCwd ? pathIdentity(rawCwd) : void 0;
|
|
955
|
+
const input = typeof parsed.tool_input === "object" && parsed.tool_input !== null && !Array.isArray(parsed.tool_input) ? parsed.tool_input : void 0;
|
|
956
|
+
const rawToolCwd = parsed.tool_name === "Shell" && typeof input?.working_directory === "string" ? input.working_directory : void 0;
|
|
957
|
+
const base = envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
|
|
958
|
+
if (rawToolCwd && !isAbsolute6(rawToolCwd) && !base) {
|
|
959
|
+
throw new TypeError("relative Cursor tool cwd has no workspace base");
|
|
960
|
+
}
|
|
961
|
+
const toolCwd = rawToolCwd ? pathIdentity(isAbsolute6(rawToolCwd) ? rawToolCwd : resolve3(base, rawToolCwd)) : void 0;
|
|
962
|
+
const selected = toolCwd ?? envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
|
|
963
|
+
if (!selected) {
|
|
964
|
+
throw new TypeError("Cursor hook envelope has no unambiguous workspace");
|
|
965
|
+
}
|
|
966
|
+
const selectedRepo = resolveRepositoryContext(selected)?.repoRoot;
|
|
967
|
+
if (expectedRepo && selectedRepo !== expectedRepo) {
|
|
968
|
+
throw new TypeError("Cursor hook cwd conflicts with workspace repository");
|
|
969
|
+
}
|
|
970
|
+
return selected;
|
|
971
|
+
}
|
|
972
|
+
function normalizeCursorEnvelope(parsed) {
|
|
973
|
+
const rawEvent = requiredId(parsed.hook_event_name, "hook_event_name");
|
|
974
|
+
const event = CURSOR_EVENT_MAP[rawEvent];
|
|
975
|
+
if (!event) throw new TypeError(`unsupported Cursor hook event: ${rawEvent}`);
|
|
976
|
+
const sessionId = requiredId(parsed.conversation_id, "conversation_id");
|
|
977
|
+
const turnId = requiredId(parsed.generation_id, "generation_id");
|
|
978
|
+
if (parsed.session_id !== void 0 && parsed.session_id !== sessionId) {
|
|
979
|
+
throw new TypeError("Cursor conversation_id conflicts with session_id");
|
|
980
|
+
}
|
|
981
|
+
if (parsed.turn_id !== void 0 && parsed.turn_id !== turnId) {
|
|
982
|
+
throw new TypeError("Cursor generation_id conflicts with turn_id");
|
|
983
|
+
}
|
|
984
|
+
const rawToolUseId = CURSOR_TOOL_EVENTS.has(event) ? requiredId(parsed.tool_use_id, "tool_use_id") : void 0;
|
|
985
|
+
const normalized = {
|
|
986
|
+
...Object.fromEntries(
|
|
987
|
+
Object.entries(parsed).filter(([key]) => !CURSOR_LOCAL_ONLY_FIELDS.has(key))
|
|
988
|
+
),
|
|
989
|
+
hook_event_name: event,
|
|
990
|
+
session_id: sessionId,
|
|
991
|
+
turn_id: turnId,
|
|
992
|
+
cwd: resolveCursorCwd(parsed),
|
|
993
|
+
...rawToolUseId === void 0 ? {} : { tool_use_id: cursorToolUseId(rawToolUseId) }
|
|
994
|
+
};
|
|
995
|
+
if (typeof parsed.tool_output === "string") {
|
|
996
|
+
try {
|
|
997
|
+
normalized.tool_response = JSON.parse(parsed.tool_output);
|
|
998
|
+
} catch {
|
|
999
|
+
normalized.tool_response = parsed.tool_output;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return normalized;
|
|
1003
|
+
}
|
|
1004
|
+
function normalizeEnvelope(parsed, agent) {
|
|
1005
|
+
if (agent === "cursor") {
|
|
1006
|
+
return normalizeCursorEnvelope(parsed);
|
|
1007
|
+
}
|
|
1008
|
+
if (agent === "codex") {
|
|
1009
|
+
const event = parsed.hook_event_name;
|
|
1010
|
+
if ((event === "PreToolUse" || event === "PostToolUse") && (typeof parsed.tool_use_id !== "string" || parsed.tool_use_id.length === 0)) {
|
|
1011
|
+
const stableToolIdentity = JSON.stringify([
|
|
1012
|
+
parsed.session_id ?? null,
|
|
1013
|
+
parsed.turn_id ?? null,
|
|
1014
|
+
parsed.tool_name ?? null,
|
|
1015
|
+
parsed.tool_input ?? null
|
|
1016
|
+
]);
|
|
1017
|
+
return {
|
|
1018
|
+
...parsed,
|
|
1019
|
+
tool_use_id: `codex:fallback:v1:${createHash5("sha256").update(stableToolIdentity).digest("hex")}`
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return parsed;
|
|
1023
|
+
}
|
|
1024
|
+
if (agent !== "hermes") {
|
|
1025
|
+
return parsed;
|
|
1026
|
+
}
|
|
1027
|
+
const raw = parsed.hook_event_name;
|
|
1028
|
+
if (typeof raw === "string" && raw in HERMES_EVENT_MAP) {
|
|
1029
|
+
return { ...parsed, hook_event_name: HERMES_EVENT_MAP[raw] };
|
|
1030
|
+
}
|
|
1031
|
+
return parsed;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/hooks/session-end.ts
|
|
1035
|
+
var STDIN_TIMEOUT_MS = 1e3;
|
|
1036
|
+
var DAEMON_TIMEOUT_MS = 250;
|
|
1037
|
+
function emit() {
|
|
1038
|
+
process.stdout.write("{}\n");
|
|
1039
|
+
}
|
|
1040
|
+
async function main() {
|
|
1041
|
+
const agent = parseAgent(process.argv);
|
|
1042
|
+
let raw;
|
|
1043
|
+
try {
|
|
1044
|
+
raw = await readHookStdin(STDIN_TIMEOUT_MS);
|
|
1045
|
+
} catch {
|
|
1046
|
+
emit();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
let envelope;
|
|
1050
|
+
try {
|
|
1051
|
+
const incoming = JSON.parse(raw);
|
|
1052
|
+
if (agent !== "cursor" && shouldSuppressImportedCursorHandler(incoming, "prim-session-end")) {
|
|
1053
|
+
emit();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
envelope = normalizeEnvelope(incoming, agent);
|
|
1057
|
+
} catch {
|
|
1058
|
+
emit();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (envelope.hook_event_name !== "SessionEnd") {
|
|
1062
|
+
emit();
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
|
|
1066
|
+
emit();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
await daemonRequest(
|
|
1070
|
+
"session_end",
|
|
1071
|
+
{ sessionId: envelope.session_id },
|
|
1072
|
+
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
1073
|
+
);
|
|
1074
|
+
emit();
|
|
1075
|
+
}
|
|
1076
|
+
main().catch(() => {
|
|
1077
|
+
emit();
|
|
1078
|
+
});
|