@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.90
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 +295 -37
- package/SKILL.md +269 -0
- package/dist/chunk-2JRX7ZR4.js +247 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-6IWNH776.js +77 -0
- package/dist/chunk-C7T7B45R.js +1400 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-MAZWKJGL.js +18548 -0
- package/dist/chunk-PMRDE2DA.js +310 -0
- package/dist/chunk-PPQYQEIR.js +815 -0
- package/dist/chunk-R26A7TWY.js +36 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-UZPBB7V4.js +505 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -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 +25846 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +25284 -0
- package/dist/hook-runtime/hooks/prim-hook.js +25870 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +28180 -0
- package/dist/hook-runtime/index.js +50516 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +188 -0
- package/dist/hooks/post-rewrite.js +201 -0
- package/dist/hooks/post-tool-use.js +263 -0
- package/dist/hooks/pre-commit.js +16 -101
- package/dist/hooks/pre-tool-use.js +367 -0
- package/dist/hooks/prim-hook.js +207 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +289 -0
- package/dist/index.js +9749 -425
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,495 @@
|
|
|
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((resolve) => {
|
|
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
|
+
resolve(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 === "hermes") {
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
return "claude_code";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/hooks/normalize.ts
|
|
399
|
+
import { createHash as createHash3 } from "crypto";
|
|
400
|
+
var HERMES_EVENT_MAP = {
|
|
401
|
+
on_session_start: "SessionStart",
|
|
402
|
+
on_session_end: "SessionEnd",
|
|
403
|
+
pre_llm_call: "UserPromptSubmit",
|
|
404
|
+
post_llm_call: "Stop",
|
|
405
|
+
pre_tool_call: "PreToolUse",
|
|
406
|
+
post_tool_call: "PostToolUse",
|
|
407
|
+
subagent_stop: "SubagentStop"
|
|
408
|
+
};
|
|
409
|
+
function normalizeEnvelope(parsed, agent) {
|
|
410
|
+
if (agent === "codex") {
|
|
411
|
+
const event = parsed.hook_event_name;
|
|
412
|
+
if ((event === "PreToolUse" || event === "PostToolUse") && (typeof parsed.tool_use_id !== "string" || parsed.tool_use_id.length === 0)) {
|
|
413
|
+
const stableToolIdentity = JSON.stringify([
|
|
414
|
+
parsed.session_id ?? null,
|
|
415
|
+
parsed.turn_id ?? null,
|
|
416
|
+
parsed.tool_name ?? null,
|
|
417
|
+
parsed.tool_input ?? null
|
|
418
|
+
]);
|
|
419
|
+
return {
|
|
420
|
+
...parsed,
|
|
421
|
+
tool_use_id: `codex:fallback:v1:${createHash3("sha256").update(stableToolIdentity).digest("hex")}`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return parsed;
|
|
425
|
+
}
|
|
426
|
+
if (agent !== "hermes") {
|
|
427
|
+
return parsed;
|
|
428
|
+
}
|
|
429
|
+
const raw = parsed.hook_event_name;
|
|
430
|
+
if (typeof raw === "string" && raw in HERMES_EVENT_MAP) {
|
|
431
|
+
return { ...parsed, hook_event_name: HERMES_EVENT_MAP[raw] };
|
|
432
|
+
}
|
|
433
|
+
return parsed;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/hooks/session-end.ts
|
|
437
|
+
var STDIN_TIMEOUT_MS = 1e3;
|
|
438
|
+
var DAEMON_TIMEOUT_MS = 250;
|
|
439
|
+
function readStdin() {
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
const chunks = [];
|
|
442
|
+
const timer = setTimeout(() => {
|
|
443
|
+
reject(new Error("stdin read timeout"));
|
|
444
|
+
}, STDIN_TIMEOUT_MS);
|
|
445
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
446
|
+
process.stdin.on("end", () => {
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
449
|
+
});
|
|
450
|
+
process.stdin.on("error", (err) => {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
reject(err);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function emit() {
|
|
457
|
+
process.stdout.write("{}\n");
|
|
458
|
+
}
|
|
459
|
+
async function main() {
|
|
460
|
+
const agent = parseAgent(process.argv);
|
|
461
|
+
let raw;
|
|
462
|
+
try {
|
|
463
|
+
raw = await readStdin();
|
|
464
|
+
} catch {
|
|
465
|
+
emit();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let envelope;
|
|
469
|
+
try {
|
|
470
|
+
envelope = normalizeEnvelope(
|
|
471
|
+
JSON.parse(raw),
|
|
472
|
+
agent
|
|
473
|
+
);
|
|
474
|
+
} catch {
|
|
475
|
+
emit();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (envelope.hook_event_name !== "SessionEnd") {
|
|
479
|
+
emit();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (typeof envelope.session_id !== "string" || envelope.session_id.length === 0) {
|
|
483
|
+
emit();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await daemonRequest(
|
|
487
|
+
"session_end",
|
|
488
|
+
{ sessionId: envelope.session_id },
|
|
489
|
+
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
490
|
+
);
|
|
491
|
+
emit();
|
|
492
|
+
}
|
|
493
|
+
main().catch(() => {
|
|
494
|
+
emit();
|
|
495
|
+
});
|