@fml-inc/fml 0.8.2 → 0.8.3
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/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +4 -4
- package/dist/hook-handler.js +76 -45
- package/dist/observer-server.js +1 -1
- package/dist/pi/extension.js +2774 -98
- package/package.json +1 -1
package/dist/pi/extension.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/pi/extension.ts
|
|
2
|
-
import { randomUUID as
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3
|
+
import fs4 from "node:fs";
|
|
4
|
+
import os2 from "node:os";
|
|
5
|
+
import path7 from "node:path";
|
|
6
6
|
|
|
7
7
|
// ../../packages/agent-targets/src/install/pi/index.ts
|
|
8
8
|
import fs from "node:fs";
|
|
@@ -149,125 +149,2741 @@ var PI_EXTENSION_INSTALLER = Object.freeze({
|
|
|
149
149
|
}
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
// src/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
// src/hooks/context.ts
|
|
153
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
154
|
+
import path6 from "node:path";
|
|
155
|
+
|
|
156
|
+
// ../../packages/agent-targets/src/hooks/normalize.ts
|
|
157
|
+
import { createHash } from "node:crypto";
|
|
158
|
+
|
|
159
|
+
// ../../packages/agent-targets/src/hooks/types.ts
|
|
160
|
+
var HOOK_TARGETS = Object.freeze([
|
|
161
|
+
"claude",
|
|
162
|
+
"codex",
|
|
163
|
+
"gemini",
|
|
164
|
+
"grok",
|
|
165
|
+
"pi"
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
// ../../packages/agent-targets/src/catalog/hook-events.json
|
|
169
|
+
var hook_events_default = {
|
|
170
|
+
claude: [
|
|
171
|
+
"SessionStart",
|
|
172
|
+
"Setup",
|
|
173
|
+
"InstructionsLoaded",
|
|
174
|
+
"UserPromptSubmit",
|
|
175
|
+
"UserPromptExpansion",
|
|
176
|
+
"PreToolUse",
|
|
177
|
+
"PermissionRequest",
|
|
178
|
+
"PostToolUse",
|
|
179
|
+
"PostToolUseFailure",
|
|
180
|
+
"PostToolBatch",
|
|
181
|
+
"PermissionDenied",
|
|
182
|
+
"Notification",
|
|
183
|
+
"SubagentStart",
|
|
184
|
+
"SubagentStop",
|
|
185
|
+
"TaskCreated",
|
|
186
|
+
"TaskCompleted",
|
|
187
|
+
"Stop",
|
|
188
|
+
"StopFailure",
|
|
189
|
+
"TeammateIdle",
|
|
190
|
+
"ConfigChange",
|
|
191
|
+
"CwdChanged",
|
|
192
|
+
"PreCompact",
|
|
193
|
+
"PostCompact",
|
|
194
|
+
"SessionEnd",
|
|
195
|
+
"Elicitation",
|
|
196
|
+
"ElicitationResult"
|
|
197
|
+
],
|
|
198
|
+
codex: [
|
|
199
|
+
"SessionStart",
|
|
200
|
+
"UserPromptSubmit",
|
|
201
|
+
"PreToolUse",
|
|
202
|
+
"PermissionRequest",
|
|
203
|
+
"PostToolUse",
|
|
204
|
+
"Stop"
|
|
205
|
+
],
|
|
206
|
+
gemini: [
|
|
207
|
+
"SessionStart",
|
|
208
|
+
"BeforeAgent",
|
|
209
|
+
"BeforeTool",
|
|
210
|
+
"AfterTool",
|
|
211
|
+
"AfterAgent"
|
|
212
|
+
],
|
|
213
|
+
grok: [
|
|
214
|
+
"SessionStart",
|
|
215
|
+
"UserPromptSubmit",
|
|
216
|
+
"PreToolUse",
|
|
217
|
+
"PostToolUse",
|
|
218
|
+
"PostToolUseFailure",
|
|
219
|
+
"PermissionDenied",
|
|
220
|
+
"Stop",
|
|
221
|
+
"StopFailure",
|
|
222
|
+
"Notification",
|
|
223
|
+
"SubagentStart",
|
|
224
|
+
"SubagentStop",
|
|
225
|
+
"PreCompact",
|
|
226
|
+
"PostCompact",
|
|
227
|
+
"SessionEnd"
|
|
228
|
+
],
|
|
229
|
+
pi: [
|
|
230
|
+
"SessionStart",
|
|
231
|
+
"UserPromptSubmit",
|
|
232
|
+
"TurnStart",
|
|
233
|
+
"PreToolUse",
|
|
234
|
+
"PostToolUse",
|
|
235
|
+
"PostToolUseFailure",
|
|
236
|
+
"Stop",
|
|
237
|
+
"Notification",
|
|
238
|
+
"ConfigChange",
|
|
239
|
+
"PreCompact",
|
|
240
|
+
"PostCompact",
|
|
241
|
+
"SessionEnd"
|
|
242
|
+
]
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// ../../packages/agent-targets/src/catalog/hook-events.ts
|
|
246
|
+
var CLAUDE_HOOK_EVENTS = Object.freeze([
|
|
247
|
+
...hook_events_default.claude
|
|
248
|
+
]);
|
|
249
|
+
var CODEX_HOOK_EVENTS = Object.freeze([
|
|
250
|
+
...hook_events_default.codex
|
|
251
|
+
]);
|
|
252
|
+
var GEMINI_HOOK_EVENTS = Object.freeze([
|
|
253
|
+
...hook_events_default.gemini
|
|
254
|
+
]);
|
|
255
|
+
var GROK_HOOK_EVENTS = Object.freeze([
|
|
256
|
+
...hook_events_default.grok
|
|
257
|
+
]);
|
|
258
|
+
var PI_HOOK_EVENTS = Object.freeze([...hook_events_default.pi]);
|
|
259
|
+
|
|
260
|
+
// ../../packages/agent-targets/src/hooks/read-command.ts
|
|
261
|
+
import path2 from "node:path";
|
|
262
|
+
var SHELL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
263
|
+
"bash",
|
|
264
|
+
"exec_command",
|
|
265
|
+
"shell"
|
|
266
|
+
]);
|
|
267
|
+
var COMMAND_SEPARATORS = /* @__PURE__ */ new Set([
|
|
268
|
+
";",
|
|
269
|
+
"&",
|
|
270
|
+
"&&",
|
|
271
|
+
"|",
|
|
272
|
+
"||",
|
|
273
|
+
"<",
|
|
274
|
+
"<<",
|
|
275
|
+
">",
|
|
276
|
+
">>",
|
|
277
|
+
">&1",
|
|
278
|
+
">&2",
|
|
279
|
+
"1>",
|
|
280
|
+
"1>>",
|
|
281
|
+
"1>&1",
|
|
282
|
+
"1>&2",
|
|
283
|
+
"2>",
|
|
284
|
+
"2>>",
|
|
285
|
+
"2>&1",
|
|
286
|
+
"2>&2"
|
|
287
|
+
]);
|
|
288
|
+
function tokenizeShellLikeCommand(command) {
|
|
289
|
+
const tokens = [];
|
|
290
|
+
let current = "";
|
|
291
|
+
let quote = null;
|
|
292
|
+
let escaping = false;
|
|
293
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
294
|
+
const character = command[index];
|
|
295
|
+
if (escaping) {
|
|
296
|
+
current += character;
|
|
297
|
+
escaping = false;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (character === "\\") {
|
|
301
|
+
escaping = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (quote) {
|
|
305
|
+
if (character === quote) quote = null;
|
|
306
|
+
else current += character;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (character === "'" || character === '"') {
|
|
310
|
+
quote = character;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (current === "" && (character === "1" || character === "2") && command[index + 1] === ">") {
|
|
314
|
+
if (command[index + 2] === ">") {
|
|
315
|
+
tokens.push(`${character}>>`);
|
|
316
|
+
index += 2;
|
|
317
|
+
} else if (command[index + 2] === "&" && (command[index + 3] === "1" || command[index + 3] === "2")) {
|
|
318
|
+
tokens.push(`${character}>&${command[index + 3]}`);
|
|
319
|
+
index += 3;
|
|
320
|
+
} else {
|
|
321
|
+
tokens.push(`${character}>`);
|
|
322
|
+
index += 1;
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (/[;&|<>]/.test(character)) {
|
|
327
|
+
if (current) {
|
|
328
|
+
tokens.push(current);
|
|
329
|
+
current = "";
|
|
330
|
+
}
|
|
331
|
+
const next = command[index + 1];
|
|
332
|
+
if (character === ">" && next === "&" && (command[index + 2] === "1" || command[index + 2] === "2")) {
|
|
333
|
+
tokens.push(`>&${command[index + 2]}`);
|
|
334
|
+
index += 2;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (character === "&" && next === "&" || character === "|" && next === "|" || character === "<" && next === "<" || character === ">" && next === ">") {
|
|
338
|
+
tokens.push(`${character}${next}`);
|
|
339
|
+
index += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
tokens.push(character);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (/\s/.test(character)) {
|
|
346
|
+
if (current) {
|
|
347
|
+
tokens.push(current);
|
|
348
|
+
current = "";
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
current += character;
|
|
353
|
+
}
|
|
354
|
+
if (quote || escaping) return [];
|
|
355
|
+
if (current) tokens.push(current);
|
|
356
|
+
return tokens;
|
|
160
357
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
358
|
+
function splitCommandSegments(tokens) {
|
|
359
|
+
const segments = [];
|
|
360
|
+
let current = [];
|
|
361
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
362
|
+
const token = tokens[index];
|
|
363
|
+
if (COMMAND_SEPARATORS.has(token)) {
|
|
364
|
+
if (current.length > 0) {
|
|
365
|
+
segments.push(current);
|
|
366
|
+
current = [];
|
|
367
|
+
}
|
|
368
|
+
if (token === "<" || token === "<<") index += 1;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
current.push(token);
|
|
167
372
|
}
|
|
168
|
-
|
|
373
|
+
if (current.length > 0) segments.push(current);
|
|
374
|
+
return segments;
|
|
169
375
|
}
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
|
|
376
|
+
function stripSimpleOptions(args, flagsWithValues = /* @__PURE__ */ new Set(), patternFlags = /* @__PURE__ */ new Set()) {
|
|
377
|
+
const remaining = [];
|
|
378
|
+
let consumedPatternFlag = false;
|
|
379
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
380
|
+
const argument = args[index];
|
|
381
|
+
if (argument === "--") {
|
|
382
|
+
remaining.push(...args.slice(index + 1));
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (argument.startsWith("-") && argument.length > 1) {
|
|
386
|
+
if (flagsWithValues.has(argument)) {
|
|
387
|
+
if (patternFlags.has(argument)) consumedPatternFlag = true;
|
|
388
|
+
if (!argument.includes("=")) index += 1;
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
remaining.push(argument);
|
|
393
|
+
}
|
|
394
|
+
return { args: remaining, consumedPatternFlag };
|
|
173
395
|
}
|
|
174
|
-
function
|
|
396
|
+
function readCommandFileArgs(args, flagsWithValues, patternFlags = flagsWithValues) {
|
|
397
|
+
const stripped = stripSimpleOptions(args, flagsWithValues, patternFlags);
|
|
398
|
+
return stripped.consumedPatternFlag ? stripped.args : stripped.args.slice(1);
|
|
399
|
+
}
|
|
400
|
+
function isPlausibleReadPath(argument) {
|
|
401
|
+
if (!argument || argument === "-" || argument.startsWith("-")) return false;
|
|
402
|
+
if (COMMAND_SEPARATORS.has(argument)) return false;
|
|
403
|
+
if (/[\0*?[{}`$]/.test(argument)) return false;
|
|
404
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(argument)) return false;
|
|
405
|
+
return path2.posix.isAbsolute(argument) || path2.win32.isAbsolute(argument) || argument.startsWith("~/") || argument.startsWith("./") || argument.startsWith("../") || argument.includes("/") || /\.[A-Za-z0-9][A-Za-z0-9_-]*$/.test(argument) || /^[A-Z][A-Za-z0-9_.-]*$/.test(argument);
|
|
406
|
+
}
|
|
407
|
+
function filePathFromSegment(tokens) {
|
|
408
|
+
const commandTokens = [...tokens];
|
|
409
|
+
while (commandTokens[0]?.includes("=") && /^[A-Za-z_][A-Za-z0-9_]*=/.test(commandTokens[0])) {
|
|
410
|
+
commandTokens.shift();
|
|
411
|
+
}
|
|
412
|
+
const commandName = commandTokens.shift()?.split("/").pop();
|
|
413
|
+
if (!commandName) return null;
|
|
414
|
+
let args;
|
|
415
|
+
switch (commandName) {
|
|
416
|
+
case "bat":
|
|
417
|
+
case "batcat":
|
|
418
|
+
case "cat":
|
|
419
|
+
case "file":
|
|
420
|
+
case "less":
|
|
421
|
+
case "more":
|
|
422
|
+
case "nl":
|
|
423
|
+
case "stat":
|
|
424
|
+
case "wc":
|
|
425
|
+
args = stripSimpleOptions(commandTokens).args;
|
|
426
|
+
break;
|
|
427
|
+
case "head":
|
|
428
|
+
case "tail":
|
|
429
|
+
args = stripSimpleOptions(commandTokens, /* @__PURE__ */ new Set(["-n", "-c"])).args;
|
|
430
|
+
break;
|
|
431
|
+
case "sed":
|
|
432
|
+
if (commandTokens.some(
|
|
433
|
+
(argument) => argument.startsWith("-i") || argument === "--in-place" || argument.startsWith("--in-place=")
|
|
434
|
+
)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
args = readCommandFileArgs(commandTokens, /* @__PURE__ */ new Set(["-e", "-f"]));
|
|
438
|
+
break;
|
|
439
|
+
case "awk":
|
|
440
|
+
args = readCommandFileArgs(
|
|
441
|
+
commandTokens,
|
|
442
|
+
/* @__PURE__ */ new Set(["-f", "-v"]),
|
|
443
|
+
/* @__PURE__ */ new Set(["-f"])
|
|
444
|
+
);
|
|
445
|
+
break;
|
|
446
|
+
case "grep":
|
|
447
|
+
case "rg":
|
|
448
|
+
args = readCommandFileArgs(
|
|
449
|
+
commandTokens,
|
|
450
|
+
/* @__PURE__ */ new Set(["-A", "-B", "-C", "-e", "-f", "-g", "-m", "-t"]),
|
|
451
|
+
/* @__PURE__ */ new Set(["-e", "-f"])
|
|
452
|
+
);
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return args.find(isPlausibleReadPath) ?? null;
|
|
458
|
+
}
|
|
459
|
+
function extractReadFilePathFromCommand(command) {
|
|
460
|
+
const tokens = tokenizeShellLikeCommand(command);
|
|
461
|
+
if (tokens.length === 0) return null;
|
|
462
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
463
|
+
if (tokens[index] === "<" && isPlausibleReadPath(tokens[index + 1])) {
|
|
464
|
+
return tokens[index + 1];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const segment of splitCommandSegments(tokens)) {
|
|
468
|
+
const filePath = filePathFromSegment(segment);
|
|
469
|
+
if (filePath) return filePath;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ../../packages/agent-targets/src/hooks/normalize.ts
|
|
475
|
+
var MAX_EXTRACTED_TEXT_BYTES = 64 * 1024;
|
|
476
|
+
function isRecord(value) {
|
|
477
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
478
|
+
}
|
|
479
|
+
function firstString(record, keys) {
|
|
480
|
+
for (const key of keys) {
|
|
481
|
+
const value = record[key];
|
|
482
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
483
|
+
}
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
function extractedText(value) {
|
|
487
|
+
if (typeof value === "string") return value;
|
|
488
|
+
if (value === void 0 || value === null) return void 0;
|
|
489
|
+
try {
|
|
490
|
+
return JSON.stringify(value);
|
|
491
|
+
} catch {
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function firstText(record, keys) {
|
|
496
|
+
for (const key of keys) {
|
|
497
|
+
const value = extractedText(record[key]);
|
|
498
|
+
if (value !== void 0 && value.length > 0) return value;
|
|
499
|
+
}
|
|
500
|
+
return void 0;
|
|
501
|
+
}
|
|
502
|
+
function utf8Prefix(value, maxBytes) {
|
|
503
|
+
let low = 0;
|
|
504
|
+
let high = value.length;
|
|
505
|
+
while (low < high) {
|
|
506
|
+
const middle = Math.ceil((low + high) / 2);
|
|
507
|
+
if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maxBytes) {
|
|
508
|
+
low = middle;
|
|
509
|
+
} else {
|
|
510
|
+
high = middle - 1;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
let end = low;
|
|
514
|
+
if (end > 0 && end < value.length && value.charCodeAt(end - 1) >= 55296 && value.charCodeAt(end - 1) <= 56319 && value.charCodeAt(end) >= 56320 && value.charCodeAt(end) <= 57343) {
|
|
515
|
+
end -= 1;
|
|
516
|
+
}
|
|
517
|
+
return value.slice(0, end);
|
|
518
|
+
}
|
|
519
|
+
function boundedExtractedText(value, field) {
|
|
520
|
+
if (value === void 0) return void 0;
|
|
521
|
+
const originalBytes = Buffer.byteLength(value, "utf8");
|
|
522
|
+
if (originalBytes <= MAX_EXTRACTED_TEXT_BYTES) return value;
|
|
523
|
+
const digest = createHash("sha256").update(value).digest("hex");
|
|
524
|
+
const marker = `
|
|
525
|
+
[fml:${field}:truncated originalBytes=${originalBytes} sha256=${digest}]`;
|
|
526
|
+
return `${utf8Prefix(
|
|
527
|
+
value,
|
|
528
|
+
MAX_EXTRACTED_TEXT_BYTES - Buffer.byteLength(marker, "utf8")
|
|
529
|
+
)}${marker}`;
|
|
530
|
+
}
|
|
531
|
+
function nestedRecord(record, keys) {
|
|
532
|
+
for (const key of keys) {
|
|
533
|
+
const value = record[key];
|
|
534
|
+
if (isRecord(value)) return value;
|
|
535
|
+
}
|
|
536
|
+
return void 0;
|
|
537
|
+
}
|
|
538
|
+
function timestampMs(payload, fallback) {
|
|
539
|
+
for (const key of ["timestamp_ms", "timestampMs", "timestamp"]) {
|
|
540
|
+
const value = payload[key];
|
|
541
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
542
|
+
const milliseconds = Math.round(
|
|
543
|
+
value < 1e10 ? value * 1e3 : value
|
|
544
|
+
);
|
|
545
|
+
if (Number.isSafeInteger(milliseconds) && milliseconds > 0) {
|
|
546
|
+
return milliseconds;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (typeof value === "string") {
|
|
550
|
+
const parsed = Date.parse(value);
|
|
551
|
+
if (Number.isSafeInteger(parsed) && parsed >= 0) return parsed;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return fallback;
|
|
555
|
+
}
|
|
556
|
+
function geminiEventType(eventType) {
|
|
557
|
+
switch (eventType) {
|
|
558
|
+
case "BeforeAgent":
|
|
559
|
+
case "BeforeModel":
|
|
560
|
+
return "UserPromptSubmit";
|
|
561
|
+
case "BeforeTool":
|
|
562
|
+
return "PreToolUse";
|
|
563
|
+
case "AfterTool":
|
|
564
|
+
return "PostToolUse";
|
|
565
|
+
case "AfterAgent":
|
|
566
|
+
return "Stop";
|
|
567
|
+
default:
|
|
568
|
+
return eventType;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
var GROK_EVENT_TYPES = Object.freeze({
|
|
572
|
+
session_start: "SessionStart",
|
|
573
|
+
user_prompt_submit: "UserPromptSubmit",
|
|
574
|
+
pre_tool_use: "PreToolUse",
|
|
575
|
+
post_tool_use: "PostToolUse",
|
|
576
|
+
post_tool_use_failure: "PostToolUseFailure",
|
|
577
|
+
permission_denied: "PermissionDenied",
|
|
578
|
+
stop: "Stop",
|
|
579
|
+
stop_failure: "StopFailure",
|
|
580
|
+
notification: "Notification",
|
|
581
|
+
subagent_start: "SubagentStart",
|
|
582
|
+
subagent_stop: "SubagentStop",
|
|
583
|
+
pre_compact: "PreCompact",
|
|
584
|
+
post_compact: "PostCompact",
|
|
585
|
+
session_end: "SessionEnd"
|
|
586
|
+
});
|
|
587
|
+
function grokEventType(eventType) {
|
|
588
|
+
return Object.hasOwn(GROK_EVENT_TYPES, eventType) ? GROK_EVENT_TYPES[eventType] ?? eventType : eventType;
|
|
589
|
+
}
|
|
590
|
+
function geminiUserPrompt(payload) {
|
|
591
|
+
const request = payload.llm_request;
|
|
592
|
+
if (!isRecord(request) || !Array.isArray(request.messages)) return void 0;
|
|
593
|
+
const userMessage = [...request.messages].reverse().find(
|
|
594
|
+
(message) => isRecord(message) && message.role === "user"
|
|
595
|
+
);
|
|
596
|
+
if (!userMessage) return void 0;
|
|
597
|
+
if (typeof userMessage.content === "string") {
|
|
598
|
+
return userMessage.content || void 0;
|
|
599
|
+
}
|
|
600
|
+
if (!Array.isArray(userMessage.content)) return void 0;
|
|
601
|
+
const text = userMessage.content.filter(
|
|
602
|
+
(part) => isRecord(part) && part.type === "text" && typeof part.text === "string"
|
|
603
|
+
).map((part) => part.text).join("\n");
|
|
604
|
+
return text || void 0;
|
|
605
|
+
}
|
|
606
|
+
function normalizedEvent(target, payload, context) {
|
|
607
|
+
const sessionId = firstString(payload, [
|
|
608
|
+
"session_id",
|
|
609
|
+
"sessionId",
|
|
610
|
+
"conversation_id",
|
|
611
|
+
"conversationId"
|
|
612
|
+
]);
|
|
613
|
+
const rawEventType = firstString(payload, [
|
|
614
|
+
"hook_event_name",
|
|
615
|
+
"hookEventName",
|
|
616
|
+
"event_name",
|
|
617
|
+
"eventName",
|
|
618
|
+
"event"
|
|
619
|
+
]);
|
|
620
|
+
if (!sessionId || !rawEventType) return null;
|
|
621
|
+
const eventType = target === "gemini" ? geminiEventType(rawEventType) : target === "grok" ? grokEventType(rawEventType) : rawEventType;
|
|
622
|
+
const toolInput = nestedRecord(payload, ["tool_input", "toolInput"]);
|
|
623
|
+
const cwd = firstString(payload, [
|
|
624
|
+
"cwd",
|
|
625
|
+
"working_directory",
|
|
626
|
+
"workingDirectory",
|
|
627
|
+
"workspaceRoot"
|
|
628
|
+
]) ?? context.cwd;
|
|
629
|
+
const command = boundedExtractedText(
|
|
630
|
+
firstText(payload, ["command"]) ?? (toolInput ? firstText(toolInput, ["command", "cmd"]) : void 0),
|
|
631
|
+
"command"
|
|
632
|
+
);
|
|
633
|
+
const toolName = firstString(payload, ["tool_name", "toolName", "name"]);
|
|
634
|
+
const filePath = firstString(payload, ["file_path", "filePath", "path"]) ?? (toolInput ? firstString(toolInput, ["file_path", "filePath", "path"]) : void 0) ?? (command && toolName && SHELL_TOOL_NAMES.has(toolName.toLowerCase()) ? extractReadFilePathFromCommand(command) ?? void 0 : void 0);
|
|
635
|
+
const userPrompt = boundedExtractedText(
|
|
636
|
+
firstText(payload, ["prompt", "user_prompt", "userPrompt"]) ?? (target === "gemini" ? geminiUserPrompt(payload) : void 0),
|
|
637
|
+
"user_prompt"
|
|
638
|
+
);
|
|
639
|
+
const plan = boundedExtractedText(firstText(payload, ["plan"]), "plan");
|
|
640
|
+
const allowedPrompts = boundedExtractedText(
|
|
641
|
+
firstText(payload, ["allowed_prompts", "allowedPrompts"]),
|
|
642
|
+
"allowed_prompts"
|
|
643
|
+
);
|
|
644
|
+
const toolResult = boundedExtractedText(
|
|
645
|
+
firstText(payload, [
|
|
646
|
+
"tool_result",
|
|
647
|
+
"toolResult",
|
|
648
|
+
"tool_response",
|
|
649
|
+
"toolResponse"
|
|
650
|
+
]),
|
|
651
|
+
"tool_result"
|
|
652
|
+
);
|
|
653
|
+
const nativeEventId = firstString(payload, [
|
|
654
|
+
"hook_id",
|
|
655
|
+
"hookId",
|
|
656
|
+
"event_id",
|
|
657
|
+
"eventId",
|
|
658
|
+
"tool_use_id",
|
|
659
|
+
"toolUseId",
|
|
660
|
+
"tool_call_id",
|
|
661
|
+
"toolCallId",
|
|
662
|
+
"prompt_id",
|
|
663
|
+
"promptId"
|
|
664
|
+
]) ?? (eventType === "UserPromptSubmit" || eventType === "Stop" ? firstString(payload, ["turn_id", "turnId"]) : void 0);
|
|
665
|
+
return {
|
|
666
|
+
sessionId,
|
|
667
|
+
eventType,
|
|
668
|
+
timestampMs: timestampMs(payload, context.nowMs),
|
|
669
|
+
...nativeEventId ? { nativeEventId } : {},
|
|
670
|
+
...cwd ? { cwd } : {},
|
|
671
|
+
...toolName ? { toolName } : {},
|
|
672
|
+
...userPrompt ? { userPrompt } : {},
|
|
673
|
+
...filePath ? { filePath } : {},
|
|
674
|
+
...command ? { command } : {},
|
|
675
|
+
...plan ? { plan } : {},
|
|
676
|
+
...allowedPrompts ? { allowedPrompts } : {},
|
|
677
|
+
...toolResult ? { toolResult } : {}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function capability(targetId, nativeEvents) {
|
|
681
|
+
return Object.freeze({
|
|
682
|
+
targetId,
|
|
683
|
+
nativeEvents,
|
|
684
|
+
normalize: (payload, context) => normalizedEvent(targetId, payload, context)
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
var HOOK_NORMALIZATION_CAPABILITIES = Object.freeze([
|
|
688
|
+
capability("claude", CLAUDE_HOOK_EVENTS),
|
|
689
|
+
capability("codex", CODEX_HOOK_EVENTS),
|
|
690
|
+
capability("gemini", GEMINI_HOOK_EVENTS),
|
|
691
|
+
capability("grok", GROK_HOOK_EVENTS),
|
|
692
|
+
capability("pi", PI_HOOK_EVENTS)
|
|
693
|
+
]);
|
|
694
|
+
|
|
695
|
+
// src/auth/token-store.ts
|
|
696
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
697
|
+
import { execFileSync } from "node:child_process";
|
|
698
|
+
import fs3 from "node:fs";
|
|
699
|
+
import path5 from "node:path";
|
|
700
|
+
|
|
701
|
+
// src/config.ts
|
|
702
|
+
import fs2 from "node:fs";
|
|
703
|
+
import path4 from "node:path";
|
|
704
|
+
|
|
705
|
+
// src/dirs.ts
|
|
706
|
+
import os from "node:os";
|
|
707
|
+
import path3 from "node:path";
|
|
708
|
+
function isPresent(value) {
|
|
709
|
+
return value !== void 0 && value.trim().length > 0;
|
|
710
|
+
}
|
|
711
|
+
function normalizeAbsolutePath(value, variableName, pathApi) {
|
|
712
|
+
if (!pathApi.isAbsolute(value)) {
|
|
713
|
+
throw new Error(`${variableName} must be an absolute path: ${value}`);
|
|
714
|
+
}
|
|
715
|
+
return pathApi.normalize(value);
|
|
716
|
+
}
|
|
717
|
+
function explicitDirectory(value, variableName, pathApi) {
|
|
718
|
+
if (!isPresent(value)) return void 0;
|
|
719
|
+
return normalizeAbsolutePath(value, variableName, pathApi);
|
|
720
|
+
}
|
|
721
|
+
function platformBaseDirectory(value, pathApi) {
|
|
722
|
+
if (!isPresent(value) || !pathApi.isAbsolute(value)) return void 0;
|
|
723
|
+
return pathApi.normalize(value);
|
|
724
|
+
}
|
|
725
|
+
function resolveFmlDirectories(options = {}) {
|
|
726
|
+
const platform = options.platform ?? process.platform;
|
|
727
|
+
const env = options.env ?? process.env;
|
|
728
|
+
const pathApi = platform === "win32" ? path3.win32 : path3.posix;
|
|
729
|
+
const homeDir = normalizeAbsolutePath(
|
|
730
|
+
options.homeDir ?? os.homedir(),
|
|
731
|
+
"home directory",
|
|
732
|
+
pathApi
|
|
733
|
+
);
|
|
734
|
+
const explicitDataDir = explicitDirectory(
|
|
735
|
+
env.FML_DATA_DIR,
|
|
736
|
+
"FML_DATA_DIR",
|
|
737
|
+
pathApi
|
|
738
|
+
);
|
|
739
|
+
const explicitStateDir = explicitDirectory(
|
|
740
|
+
env.FML_STATE_DIR,
|
|
741
|
+
"FML_STATE_DIR",
|
|
742
|
+
pathApi
|
|
743
|
+
);
|
|
744
|
+
const explicitLogDir = explicitDirectory(
|
|
745
|
+
env.FML_LOG_DIR,
|
|
746
|
+
"FML_LOG_DIR",
|
|
747
|
+
pathApi
|
|
748
|
+
);
|
|
749
|
+
let defaultDataDir;
|
|
750
|
+
let defaultStateDir;
|
|
751
|
+
let nativeLogDir;
|
|
752
|
+
switch (platform) {
|
|
753
|
+
case "darwin": {
|
|
754
|
+
defaultDataDir = pathApi.join(
|
|
755
|
+
homeDir,
|
|
756
|
+
"Library",
|
|
757
|
+
"Application Support",
|
|
758
|
+
"fml"
|
|
759
|
+
);
|
|
760
|
+
defaultStateDir = defaultDataDir;
|
|
761
|
+
nativeLogDir = pathApi.join(homeDir, "Library", "Logs", "fml");
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "win32": {
|
|
765
|
+
const roamingAppData = platformBaseDirectory(env.APPDATA, pathApi) ?? pathApi.join(homeDir, "AppData", "Roaming");
|
|
766
|
+
const localAppData = platformBaseDirectory(env.LOCALAPPDATA, pathApi) ?? pathApi.join(homeDir, "AppData", "Local");
|
|
767
|
+
defaultDataDir = pathApi.join(roamingAppData, "fml");
|
|
768
|
+
defaultStateDir = pathApi.join(localAppData, "fml");
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
default: {
|
|
772
|
+
const xdgDataHome = platformBaseDirectory(env.XDG_DATA_HOME, pathApi) ?? pathApi.join(homeDir, ".local", "share");
|
|
773
|
+
const xdgStateHome = platformBaseDirectory(env.XDG_STATE_HOME, pathApi) ?? pathApi.join(homeDir, ".local", "state");
|
|
774
|
+
defaultDataDir = pathApi.join(xdgDataHome, "fml");
|
|
775
|
+
defaultStateDir = pathApi.join(xdgStateHome, "fml");
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const dataDir = explicitDataDir ?? defaultDataDir;
|
|
780
|
+
const stateDir = explicitStateDir ?? explicitDataDir ?? defaultStateDir;
|
|
781
|
+
const defaultLogDir = nativeLogDir ?? (platform === "win32" ? pathApi.join(defaultDataDir, "logs") : pathApi.join(defaultStateDir, "log"));
|
|
782
|
+
const explicitObserverDir = explicitDirectory(
|
|
783
|
+
env.FML_OBSERVER_DIR,
|
|
784
|
+
"FML_OBSERVER_DIR",
|
|
785
|
+
pathApi
|
|
786
|
+
);
|
|
787
|
+
const legacyPanopticonHome = explicitDirectory(
|
|
788
|
+
env.PANOPTICON_HOME,
|
|
789
|
+
"PANOPTICON_HOME",
|
|
790
|
+
pathApi
|
|
791
|
+
);
|
|
792
|
+
const fmlHome = explicitDirectory(env.FML_HOME, "FML_HOME", pathApi);
|
|
793
|
+
return {
|
|
794
|
+
dataDir,
|
|
795
|
+
stateDir,
|
|
796
|
+
logDir: explicitLogDir ?? defaultLogDir,
|
|
797
|
+
observerDir: explicitObserverDir ?? legacyPanopticonHome ?? (fmlHome ? pathApi.join(fmlHome, "panopticon") : pathApi.join(homeDir, ".fml", "panopticon"))
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function observerPort() {
|
|
801
|
+
const configured = process.env.FML_OBSERVER_PORT ?? process.env.PANOPTICON_PORT;
|
|
802
|
+
if (configured) {
|
|
803
|
+
const parsed = Number.parseInt(configured, 10);
|
|
804
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
|
|
805
|
+
return parsed;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return 4318 + (process.getuid?.() ?? 0) % 100;
|
|
809
|
+
}
|
|
810
|
+
var DIRECTORIES = resolveFmlDirectories();
|
|
811
|
+
var FML_DATA_DIR = DIRECTORIES.dataDir;
|
|
812
|
+
var FML_STATE_DIR = DIRECTORIES.stateDir;
|
|
813
|
+
var FML_LOG_DIR = DIRECTORIES.logDir;
|
|
814
|
+
var FML_OBSERVER_DIR = DIRECTORIES.observerDir;
|
|
815
|
+
var FML_OBSERVER_PORT = observerPort();
|
|
816
|
+
var FML_OBSERVER_STATE_FILE = path3.join(
|
|
817
|
+
FML_OBSERVER_DIR,
|
|
818
|
+
"observer.pid.json"
|
|
819
|
+
);
|
|
820
|
+
var FML_OBSERVER_LOCK_FILE = path3.join(
|
|
821
|
+
FML_OBSERVER_DIR,
|
|
822
|
+
"observer.lock"
|
|
823
|
+
);
|
|
824
|
+
var FML_OBSERVER_AUTH_TOKEN_FILE = path3.join(
|
|
825
|
+
FML_OBSERVER_DIR,
|
|
826
|
+
"auth-token"
|
|
827
|
+
);
|
|
828
|
+
var FML_OBSERVER_ENDPOINT_FILE = path3.join(
|
|
829
|
+
FML_OBSERVER_DIR,
|
|
830
|
+
"endpoint.json"
|
|
831
|
+
);
|
|
832
|
+
var FML_OBSERVER_LOG_FILE = path3.join(FML_LOG_DIR, "observer.log");
|
|
833
|
+
var FML_INSTALLATION_ID_FILE = path3.join(
|
|
834
|
+
FML_STATE_DIR,
|
|
835
|
+
"installation_id"
|
|
836
|
+
);
|
|
837
|
+
var FML_PANOPTICON_DB_FILE = path3.join(FML_STATE_DIR, "panopticon.db");
|
|
838
|
+
var FML_PANOPTICON_DB_OBSERVER_STATE_FILE = `${FML_PANOPTICON_DB_FILE}.observer.pid.json`;
|
|
839
|
+
var FML_PANOPTICON_DB_OBSERVER_LOCK_FILE = `${FML_PANOPTICON_DB_FILE}.observer.lock`;
|
|
840
|
+
var FML_LEGACY_OBSERVER_DB_FILE = path3.join(
|
|
841
|
+
FML_OBSERVER_DIR,
|
|
842
|
+
"observer.db"
|
|
843
|
+
);
|
|
844
|
+
var FML_HOOK_INBOX_DIR = path3.join(FML_OBSERVER_DIR, "hook-inbox");
|
|
845
|
+
var FML_HOOK_CONTEXT_STATE_DIR = path3.join(
|
|
846
|
+
FML_OBSERVER_DIR,
|
|
847
|
+
"hook-context-state"
|
|
848
|
+
);
|
|
849
|
+
var FML_HOOK_CAPTURE_DISABLED_FILE = path3.join(
|
|
850
|
+
FML_OBSERVER_DIR,
|
|
851
|
+
"hook-capture.disabled"
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
// src/config.ts
|
|
855
|
+
var DEFAULT_PROD_URL = "https://trustworthy-chihuahua-382.convex.cloud";
|
|
856
|
+
var DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(
|
|
857
|
+
".convex.cloud",
|
|
858
|
+
".convex.site"
|
|
859
|
+
);
|
|
860
|
+
var DEFAULT_TARGET_NAME = "fml";
|
|
861
|
+
var ENV_CONFIG_PATH = path4.join(FML_DATA_DIR, "env.json");
|
|
862
|
+
var ENV_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
863
|
+
function isValidEnvName(envName) {
|
|
864
|
+
return ENV_NAME_RE.test(envName);
|
|
865
|
+
}
|
|
866
|
+
function normalizeConvexUrl(value) {
|
|
867
|
+
const url = new URL(value);
|
|
868
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
869
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") {
|
|
870
|
+
throw new Error(
|
|
871
|
+
"FML environment URL must be HTTPS (HTTP is allowed only for loopback) with no path, credentials, query, or fragment"
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return value.replace(/\/$/, "").replace(".convex.site", ".convex.cloud");
|
|
875
|
+
}
|
|
876
|
+
function readEnvConfig() {
|
|
175
877
|
try {
|
|
176
|
-
const parsed = JSON.parse(
|
|
878
|
+
const parsed = JSON.parse(
|
|
879
|
+
fs2.readFileSync(ENV_CONFIG_PATH, "utf8")
|
|
880
|
+
);
|
|
177
881
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
178
|
-
|
|
882
|
+
throw new Error("invalid environment config");
|
|
179
883
|
}
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
|
|
884
|
+
const config = parsed;
|
|
885
|
+
if (typeof config.active !== "string" || !isValidEnvName(config.active)) {
|
|
886
|
+
throw new Error("invalid active environment");
|
|
183
887
|
}
|
|
184
|
-
|
|
888
|
+
const environments = {};
|
|
889
|
+
if (config.environments && typeof config.environments === "object" && !Array.isArray(config.environments)) {
|
|
890
|
+
for (const [name, environment] of Object.entries(
|
|
891
|
+
config.environments
|
|
892
|
+
)) {
|
|
893
|
+
if (!isValidEnvName(name) || !environment || typeof environment !== "object" || Array.isArray(environment)) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const convexUrl = environment.convexUrl;
|
|
897
|
+
if (typeof convexUrl !== "string") continue;
|
|
898
|
+
try {
|
|
899
|
+
environments[name] = { convexUrl: normalizeConvexUrl(convexUrl) };
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
schemaVersion: 1,
|
|
906
|
+
active: config.active,
|
|
907
|
+
environments
|
|
908
|
+
};
|
|
185
909
|
} catch {
|
|
186
|
-
return
|
|
910
|
+
return {
|
|
911
|
+
schemaVersion: 1,
|
|
912
|
+
active: DEFAULT_TARGET_NAME,
|
|
913
|
+
environments: {
|
|
914
|
+
[DEFAULT_TARGET_NAME]: { convexUrl: DEFAULT_PROD_URL }
|
|
915
|
+
}
|
|
916
|
+
};
|
|
187
917
|
}
|
|
188
918
|
}
|
|
189
|
-
function
|
|
919
|
+
function resolveEnvConvexUrl(envName) {
|
|
920
|
+
const configured = readEnvConfig().environments?.[envName]?.convexUrl;
|
|
921
|
+
if (configured) return configured;
|
|
922
|
+
if (envName === DEFAULT_TARGET_NAME) return DEFAULT_PROD_URL;
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
function getActiveEnv() {
|
|
926
|
+
const name = readEnvConfig().active;
|
|
927
|
+
const override = process.env.FML_CONVEX_URL;
|
|
928
|
+
return {
|
|
929
|
+
name,
|
|
930
|
+
convexUrl: override ? normalizeConvexUrl(override) : resolveEnvConvexUrl(name)
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function authStorePathFor(envName) {
|
|
934
|
+
return path4.join(FML_DATA_DIR, `auth.${envName}.json`);
|
|
935
|
+
}
|
|
936
|
+
var CONVEX_URL = (process.env.FML_CONVEX_URL ?? getActiveEnv().convexUrl ?? DEFAULT_PROD_URL).replace(/\/$/, "");
|
|
937
|
+
var WORKOS_API_URL = "https://api.workos.com";
|
|
938
|
+
function getSiteUrlForConvexUrl(convexUrl) {
|
|
939
|
+
const explicit = process.env.CONVEX_SITE_URL;
|
|
940
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
941
|
+
return (convexUrl ?? DEFAULT_PROD_URL).replace(".convex.cloud", ".convex.site").replace(/\/$/, "");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/sentry.ts
|
|
945
|
+
var sentry = null;
|
|
946
|
+
var client = null;
|
|
947
|
+
var Sentry = {
|
|
948
|
+
captureException: (err) => sentry?.captureException(err),
|
|
949
|
+
captureMessage: (msg) => sentry?.captureMessage(msg),
|
|
950
|
+
flush: async (timeoutMs) => {
|
|
951
|
+
try {
|
|
952
|
+
await client?.flush(timeoutMs);
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// src/auth/token-store.ts
|
|
959
|
+
var TOKEN_REFRESH_WINDOW_MS = 6e4;
|
|
960
|
+
var OAUTH_REFRESH_LOCK_POLL_MS = 50;
|
|
961
|
+
var OAUTH_REFRESH_LOCK_STALE_MS = 45e3;
|
|
962
|
+
var OAUTH_REFRESH_RECOVERY_STALE_MS = 45e3;
|
|
963
|
+
var OAUTH_REFRESH_LOCK_WAIT_MS = 6e4;
|
|
964
|
+
var OAUTH_REFRESH_REQUEST_TIMEOUT_MS = 3e4;
|
|
965
|
+
var PROCESS_IDENTITY_STALE_MS = 45e3;
|
|
966
|
+
var PROCESS_IDENTITY_RECHECK_MS = 5e3;
|
|
967
|
+
var PROCESS_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("@fml-inc/fml:auth-process-instance");
|
|
968
|
+
var PROCESS_IDENTITY_KEY = /* @__PURE__ */ Symbol.for("@fml-inc/fml:auth-process-identity");
|
|
969
|
+
var processGlobals = globalThis;
|
|
970
|
+
var existingProcessInstanceId = processGlobals[PROCESS_INSTANCE_KEY];
|
|
971
|
+
var CURRENT_PROCESS_INSTANCE_ID = typeof existingProcessInstanceId === "string" ? existingProcessInstanceId : randomUUID2();
|
|
972
|
+
processGlobals[PROCESS_INSTANCE_KEY] = CURRENT_PROCESS_INSTANCE_ID;
|
|
973
|
+
function getCurrentProcessIdentity() {
|
|
974
|
+
let cache = processGlobals[PROCESS_IDENTITY_KEY];
|
|
975
|
+
if (!cache) {
|
|
976
|
+
cache = { resolved: false };
|
|
977
|
+
processGlobals[PROCESS_IDENTITY_KEY] = cache;
|
|
978
|
+
}
|
|
979
|
+
if (!cache.resolved) {
|
|
980
|
+
cache.value = getProcessIdentity(process.pid) ?? void 0;
|
|
981
|
+
cache.resolved = true;
|
|
982
|
+
}
|
|
983
|
+
return cache.value;
|
|
984
|
+
}
|
|
985
|
+
function storePathFor(envName) {
|
|
986
|
+
return authStorePathFor(envName ?? getActiveEnv().name);
|
|
987
|
+
}
|
|
988
|
+
function resolveEnvName(envName) {
|
|
989
|
+
return envName ?? getActiveEnv().name;
|
|
990
|
+
}
|
|
991
|
+
function ensureDir(envName) {
|
|
992
|
+
const dir = path5.dirname(storePathFor(envName));
|
|
993
|
+
fs3.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
190
994
|
try {
|
|
191
|
-
|
|
995
|
+
fs3.chmodSync(dir, 448);
|
|
192
996
|
} catch {
|
|
193
|
-
return null;
|
|
194
997
|
}
|
|
195
998
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const authToken = readObserverAuthToken(path2.join(directory, "auth-token"));
|
|
200
|
-
if (!endpoint || !authToken) return;
|
|
201
|
-
const host = endpoint.host === "::1" ? "[::1]" : endpoint.host;
|
|
999
|
+
function writeSecretFile(filePath, data) {
|
|
1000
|
+
const temporaryFile = `${filePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1001
|
+
let descriptor = null;
|
|
202
1002
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1003
|
+
descriptor = fs3.openSync(temporaryFile, "wx", 384);
|
|
1004
|
+
fs3.writeFileSync(descriptor, data, { encoding: "utf8" });
|
|
1005
|
+
fs3.fsyncSync(descriptor);
|
|
1006
|
+
fs3.closeSync(descriptor);
|
|
1007
|
+
descriptor = null;
|
|
1008
|
+
fs3.renameSync(temporaryFile, filePath);
|
|
1009
|
+
try {
|
|
1010
|
+
fs3.chmodSync(filePath, 384);
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (descriptor !== null) {
|
|
1015
|
+
try {
|
|
1016
|
+
fs3.closeSync(descriptor);
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
fs3.unlinkSync(temporaryFile);
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function readTokens(envName) {
|
|
1027
|
+
let raw;
|
|
1028
|
+
try {
|
|
1029
|
+
raw = fs3.readFileSync(storePathFor(envName), "utf-8");
|
|
1030
|
+
} catch {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
return parseStoredAuthShape(JSON.parse(raw));
|
|
222
1035
|
} catch {
|
|
1036
|
+
return null;
|
|
223
1037
|
}
|
|
224
1038
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1039
|
+
var CorruptStoredCredentialsError = class extends Error {
|
|
1040
|
+
};
|
|
1041
|
+
function parseStoredAuthShape(parsed) {
|
|
1042
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
const auth = parsed;
|
|
1046
|
+
const user = auth.user;
|
|
1047
|
+
if (typeof auth.accessToken !== "string" || typeof auth.refreshToken !== "string" || typeof auth.expiresAt !== "number" || !Number.isFinite(auth.expiresAt) || !user || typeof user.id !== "string" || typeof user.email !== "string" || typeof user.name !== "string" || auth.orgId !== void 0 && typeof auth.orgId !== "string" || auth.orgSlug !== void 0 && typeof auth.orgSlug !== "string" || auth.workosClientId !== void 0 && typeof auth.workosClientId !== "string" || auth.tokenType !== void 0 && auth.tokenType !== "oauth" && auth.tokenType !== "service" || auth.refreshInvalidAt !== void 0 && (typeof auth.refreshInvalidAt !== "number" || !Number.isFinite(auth.refreshInvalidAt))) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
return auth;
|
|
229
1051
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
))
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1052
|
+
function readTokensForReconciliation(envName) {
|
|
1053
|
+
let raw;
|
|
1054
|
+
try {
|
|
1055
|
+
raw = fs3.readFileSync(storePathFor(envName), "utf8");
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error.code === "ENOENT") return null;
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
let parsed;
|
|
1061
|
+
try {
|
|
1062
|
+
parsed = JSON.parse(raw);
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
throw new CorruptStoredCredentialsError(
|
|
1065
|
+
"Stored credentials are not valid JSON",
|
|
1066
|
+
{ cause: error }
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
const auth = parseStoredAuthShape(parsed);
|
|
1070
|
+
if (!auth) {
|
|
1071
|
+
throw new CorruptStoredCredentialsError(
|
|
1072
|
+
"Stored credentials have an invalid shape"
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return auth;
|
|
239
1076
|
}
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const id = record.id ?? record.name ?? record.model;
|
|
244
|
-
const provider = record.provider ?? record.providerId;
|
|
245
|
-
return [provider, id].filter((value) => typeof value === "string" && value.length > 0).join(":") || void 0;
|
|
1077
|
+
function writeTokensUnlocked(auth, envName) {
|
|
1078
|
+
ensureDir(envName);
|
|
1079
|
+
writeSecretFile(storePathFor(envName), JSON.stringify(auth, null, 2));
|
|
246
1080
|
}
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
1081
|
+
function credentialHash(value) {
|
|
1082
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1083
|
+
}
|
|
1084
|
+
function describeError(error) {
|
|
1085
|
+
return error instanceof Error ? error.message : String(error);
|
|
1086
|
+
}
|
|
1087
|
+
function getSyncDestinationAuthBinding(envName) {
|
|
1088
|
+
const resolvedEnvName = resolveEnvName(envName);
|
|
1089
|
+
const envToken = process.env.FML_TOKEN;
|
|
1090
|
+
if (envToken?.startsWith("fml_srt_")) {
|
|
1091
|
+
return {
|
|
1092
|
+
destinationIdentity: `service-refresh:${credentialHash(envToken)}`
|
|
1093
|
+
};
|
|
257
1094
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1095
|
+
if (envToken?.startsWith("fml_st_")) {
|
|
1096
|
+
return {
|
|
1097
|
+
destinationIdentity: `service-access:${credentialHash(envToken)}`
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
const stored = readTokens(resolvedEnvName);
|
|
1101
|
+
if (!stored) return null;
|
|
1102
|
+
if (stored.tokenType === "service") {
|
|
1103
|
+
return {
|
|
1104
|
+
destinationIdentity: `service-refresh:${credentialHash(stored.refreshToken)}`
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
const destinationIdentity = `oauth-user:${stored.user.id}`;
|
|
1108
|
+
if (stored.orgId && stored.orgSlug) {
|
|
1109
|
+
return {
|
|
1110
|
+
destinationIdentity,
|
|
1111
|
+
organizationId: stored.orgId,
|
|
1112
|
+
organizationSlug: stored.orgSlug
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
return { destinationIdentity, organizationSelectionRequired: true };
|
|
1116
|
+
}
|
|
1117
|
+
function getSyncDestinationIdentity(envName) {
|
|
1118
|
+
return getSyncDestinationAuthBinding(envName)?.destinationIdentity ?? null;
|
|
1119
|
+
}
|
|
1120
|
+
var serviceTokenCaches = /* @__PURE__ */ new Map();
|
|
1121
|
+
var serviceRefreshPromises = /* @__PURE__ */ new Map();
|
|
1122
|
+
var oauthRefreshPromises = /* @__PURE__ */ new Map();
|
|
1123
|
+
var cacheKey = (envName) => envName;
|
|
1124
|
+
function waitForTokenRefresh(promise, signal) {
|
|
1125
|
+
if (!signal) return promise;
|
|
1126
|
+
if (signal.aborted) return Promise.resolve(null);
|
|
1127
|
+
return new Promise((resolve, reject) => {
|
|
1128
|
+
const onAbort = () => {
|
|
1129
|
+
cleanup();
|
|
1130
|
+
resolve(null);
|
|
1131
|
+
};
|
|
1132
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1133
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1134
|
+
void promise.then(
|
|
1135
|
+
(value) => {
|
|
1136
|
+
cleanup();
|
|
1137
|
+
resolve(value);
|
|
1138
|
+
},
|
|
1139
|
+
(error) => {
|
|
1140
|
+
cleanup();
|
|
1141
|
+
reject(error);
|
|
1142
|
+
}
|
|
1143
|
+
);
|
|
262
1144
|
});
|
|
263
|
-
|
|
264
|
-
|
|
1145
|
+
}
|
|
1146
|
+
function getOrStartTokenRefresh(flights, key, forceRefresh, refresh, signal) {
|
|
1147
|
+
const existing = flights.get(key);
|
|
1148
|
+
if (existing && (!forceRefresh || existing.forceRefresh)) {
|
|
1149
|
+
return waitForTokenRefresh(existing.promise, signal);
|
|
1150
|
+
}
|
|
1151
|
+
const operation = existing ? existing.promise.then(refresh, refresh) : refresh();
|
|
1152
|
+
let flight;
|
|
1153
|
+
const promise = operation.finally(() => {
|
|
1154
|
+
if (flights.get(key) === flight) flights.delete(key);
|
|
265
1155
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1156
|
+
flight = { forceRefresh, promise };
|
|
1157
|
+
flights.set(key, flight);
|
|
1158
|
+
return waitForTokenRefresh(promise, signal);
|
|
1159
|
+
}
|
|
1160
|
+
function refreshLockPath(envName) {
|
|
1161
|
+
return `${storePathFor(envName)}.refresh.lock`;
|
|
1162
|
+
}
|
|
1163
|
+
function pendingOAuthRefreshPrefix(envName) {
|
|
1164
|
+
return `${path5.basename(storePathFor(envName))}.refresh-result.`;
|
|
1165
|
+
}
|
|
1166
|
+
function parsePendingOAuthRefresh(raw) {
|
|
1167
|
+
try {
|
|
1168
|
+
const parsed = JSON.parse(raw);
|
|
1169
|
+
const refreshed = parsed.refreshed;
|
|
1170
|
+
if (parsed.version !== 1 || typeof parsed.previousRefreshTokenHash !== "string" || !/^[a-f0-9]{64}$/.test(parsed.previousRefreshTokenHash) || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || !refreshed || typeof refreshed.accessToken !== "string" || typeof refreshed.refreshToken !== "string" || typeof refreshed.expiresAt !== "number" || !Number.isFinite(refreshed.expiresAt) || !refreshed.user || typeof refreshed.user.id !== "string" || typeof refreshed.user.email !== "string" || typeof refreshed.user.name !== "string") {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
return parsed;
|
|
1174
|
+
} catch {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
function stagePendingOAuthRefresh(previousRefreshToken, refreshed, envName) {
|
|
1179
|
+
ensureDir(envName);
|
|
1180
|
+
const resultPath = `${storePathFor(envName)}.refresh-result.${Date.now()}.${randomUUID2()}.json`;
|
|
1181
|
+
const pending = {
|
|
1182
|
+
version: 1,
|
|
1183
|
+
previousRefreshTokenHash: credentialHash(previousRefreshToken),
|
|
1184
|
+
refreshed,
|
|
1185
|
+
createdAtMs: Date.now()
|
|
1186
|
+
};
|
|
1187
|
+
writeSecretFile(resultPath, `${JSON.stringify(pending)}
|
|
1188
|
+
`);
|
|
1189
|
+
return resultPath;
|
|
1190
|
+
}
|
|
1191
|
+
function removePendingOAuthRefresh(resultPath) {
|
|
1192
|
+
try {
|
|
1193
|
+
fs3.unlinkSync(resultPath);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
if (error.code !== "ENOENT") {
|
|
1196
|
+
console.error(
|
|
1197
|
+
`[fml] Auth: unable to remove a committed refresh result: ${error instanceof Error ? error.message : String(error)}`
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function reconcilePendingOAuthRefreshes(envName, unreadableStorePolicy = "preserve-staged") {
|
|
1203
|
+
const storePath = storePathFor(envName);
|
|
1204
|
+
const directory = path5.dirname(storePath);
|
|
1205
|
+
const prefix = pendingOAuthRefreshPrefix(envName);
|
|
1206
|
+
let resultPaths;
|
|
1207
|
+
try {
|
|
1208
|
+
resultPaths = fs3.readdirSync(directory).filter((entry) => entry.startsWith(prefix) && entry.endsWith(".json")).map((entry) => path5.join(directory, entry));
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (error.code === "ENOENT") return;
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
const pendingResults = resultPaths.map((resultPath) => {
|
|
1214
|
+
let pending = null;
|
|
1215
|
+
try {
|
|
1216
|
+
pending = parsePendingOAuthRefresh(fs3.readFileSync(resultPath, "utf8"));
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
if (error.code !== "ENOENT") throw error;
|
|
1219
|
+
}
|
|
1220
|
+
return { resultPath, pending };
|
|
1221
|
+
}).sort(
|
|
1222
|
+
(left, right) => (left.pending?.createdAtMs ?? 0) - (right.pending?.createdAtMs ?? 0)
|
|
1223
|
+
);
|
|
1224
|
+
for (const { resultPath, pending } of pendingResults) {
|
|
1225
|
+
if (!pending) {
|
|
1226
|
+
removePendingOAuthRefresh(resultPath);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
let current;
|
|
1230
|
+
try {
|
|
1231
|
+
current = readTokensForReconciliation(envName);
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
if (unreadableStorePolicy === "discard-staged" && error instanceof CorruptStoredCredentialsError) {
|
|
1234
|
+
console.error(
|
|
1235
|
+
`[fml] Auth: discarding a staged refresh result because the credential store is corrupt and is being replaced: ${error.message}`
|
|
1236
|
+
);
|
|
1237
|
+
removePendingOAuthRefresh(resultPath);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
if (current?.refreshToken === pending.refreshed.refreshToken || current && credentialHash(current.refreshToken) === pending.previousRefreshTokenHash) {
|
|
1243
|
+
if (current.refreshToken !== pending.refreshed.refreshToken) {
|
|
1244
|
+
writeTokensUnlocked(
|
|
1245
|
+
{
|
|
1246
|
+
...pending.refreshed,
|
|
1247
|
+
orgId: current.orgId,
|
|
1248
|
+
orgSlug: current.orgSlug
|
|
1249
|
+
},
|
|
1250
|
+
envName
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
removePendingOAuthRefresh(resultPath);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function parseRefreshLockOwner(raw) {
|
|
1258
|
+
try {
|
|
1259
|
+
const parsed = JSON.parse(raw);
|
|
1260
|
+
if (parsed.version !== 1 || typeof parsed.ownerId !== "string" || parsed.ownerId.length === 0 || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processInstanceId !== void 0 && typeof parsed.processInstanceId !== "string" || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || parsed.releasedAtMs !== void 0 && (typeof parsed.releasedAtMs !== "number" || !Number.isFinite(parsed.releasedAtMs))) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
return parsed;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function getProcessIdentity(pid) {
|
|
1269
|
+
try {
|
|
1270
|
+
if (process.platform === "linux") {
|
|
1271
|
+
const stat = fs3.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1272
|
+
const fieldsAfterCommand = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
1273
|
+
const startTimeTicks = fieldsAfterCommand[19];
|
|
1274
|
+
return startTimeTicks ? `linux:${startTimeTicks}` : null;
|
|
1275
|
+
}
|
|
1276
|
+
if (process.platform === "darwin") {
|
|
1277
|
+
const startedAt = execFileSync(
|
|
1278
|
+
"ps",
|
|
1279
|
+
["-p", String(pid), "-o", "lstart="],
|
|
1280
|
+
{
|
|
1281
|
+
encoding: "utf8",
|
|
1282
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1283
|
+
timeout: 2e3
|
|
1284
|
+
}
|
|
1285
|
+
).trim();
|
|
1286
|
+
return startedAt ? `darwin:${startedAt}` : null;
|
|
1287
|
+
}
|
|
1288
|
+
if (process.platform === "win32") {
|
|
1289
|
+
const startedAt = execFileSync(
|
|
1290
|
+
"powershell.exe",
|
|
1291
|
+
[
|
|
1292
|
+
"-NoProfile",
|
|
1293
|
+
"-NonInteractive",
|
|
1294
|
+
"-Command",
|
|
1295
|
+
`(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`
|
|
1296
|
+
],
|
|
1297
|
+
{
|
|
1298
|
+
encoding: "utf8",
|
|
1299
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1300
|
+
timeout: 2e3
|
|
1301
|
+
}
|
|
1302
|
+
).trim();
|
|
1303
|
+
return startedAt ? `win32:${startedAt}` : null;
|
|
1304
|
+
}
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
function parseRecoveryMarkerOwner(raw) {
|
|
1310
|
+
try {
|
|
1311
|
+
const parsed = JSON.parse(raw);
|
|
1312
|
+
if (parsed.version !== 1 || typeof parsed.ownerId !== "string" || parsed.ownerId.length === 0 || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processInstanceId !== void 0 && typeof parsed.processInstanceId !== "string" || typeof parsed.createdAtMs !== "number" || !Number.isFinite(parsed.createdAtMs) || parsed.releasedAtMs !== void 0 && (typeof parsed.releasedAtMs !== "number" || !Number.isFinite(parsed.releasedAtMs))) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
return parsed;
|
|
1316
|
+
} catch {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function snapshotLeaseFile(filePath) {
|
|
1321
|
+
let stat;
|
|
1322
|
+
try {
|
|
1323
|
+
stat = fs3.lstatSync(filePath);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (error.code === "ENOENT") return null;
|
|
1326
|
+
throw error;
|
|
1327
|
+
}
|
|
1328
|
+
if (stat.isDirectory()) {
|
|
1329
|
+
try {
|
|
1330
|
+
return {
|
|
1331
|
+
stat,
|
|
1332
|
+
raw: fs3.readFileSync(path5.join(filePath, "owner.json"), "utf8")
|
|
1333
|
+
};
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
if (error.code === "ENOENT") {
|
|
1336
|
+
return { stat, raw: "" };
|
|
1337
|
+
}
|
|
1338
|
+
throw error;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
return { stat, raw: fs3.readFileSync(filePath, "utf8") };
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error.code === "ENOENT") return null;
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function snapshotsMatch(observed, current) {
|
|
1349
|
+
return current.stat.dev === observed.stat.dev && current.stat.ino === observed.stat.ino && current.stat.mtimeMs === observed.stat.mtimeMs && current.raw === observed.raw;
|
|
1350
|
+
}
|
|
1351
|
+
function restoreMovedLease(movedPath, originalPath) {
|
|
1352
|
+
let movedStat;
|
|
1353
|
+
try {
|
|
1354
|
+
movedStat = fs3.lstatSync(movedPath);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
if (error.code === "ENOENT") return;
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
if (!movedStat.isFile()) return;
|
|
1360
|
+
try {
|
|
1361
|
+
fs3.linkSync(movedPath, originalPath);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
const code = error.code;
|
|
1364
|
+
if (code === "EEXIST" || code === "ENOENT") return;
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
try {
|
|
1368
|
+
fs3.unlinkSync(movedPath);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
if (error.code !== "ENOENT") throw error;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function pathMatchesClaim(filePath, claimedStat, ownerId, parseOwner) {
|
|
1374
|
+
const current = snapshotLeaseFile(filePath);
|
|
1375
|
+
return Boolean(
|
|
1376
|
+
current && current.stat.dev === claimedStat.dev && current.stat.ino === claimedStat.ino && parseOwner(current.raw)?.ownerId === ownerId
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
function markClaimReleased(filePath, claimedStat, owner, parseOwner) {
|
|
1380
|
+
let descriptor;
|
|
1381
|
+
try {
|
|
1382
|
+
descriptor = fs3.openSync(filePath, "r+");
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
if (error.code === "ENOENT") return false;
|
|
1385
|
+
throw error;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const currentStat = fs3.fstatSync(descriptor);
|
|
1389
|
+
if (currentStat.dev !== claimedStat.dev || currentStat.ino !== claimedStat.ino) {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
const currentOwner = parseOwner(fs3.readFileSync(descriptor, "utf8"));
|
|
1393
|
+
if (currentOwner?.ownerId !== owner.ownerId) return false;
|
|
1394
|
+
const serialized = Buffer.from(
|
|
1395
|
+
`${JSON.stringify({ ...owner, releasedAtMs: Date.now() })}
|
|
1396
|
+
`,
|
|
1397
|
+
"utf8"
|
|
1398
|
+
);
|
|
1399
|
+
fs3.ftruncateSync(descriptor, 0);
|
|
1400
|
+
let offset = 0;
|
|
1401
|
+
while (offset < serialized.length) {
|
|
1402
|
+
offset += fs3.writeSync(
|
|
1403
|
+
descriptor,
|
|
1404
|
+
serialized,
|
|
1405
|
+
offset,
|
|
1406
|
+
serialized.length - offset,
|
|
1407
|
+
offset
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
fs3.fsyncSync(descriptor);
|
|
1411
|
+
return true;
|
|
1412
|
+
} finally {
|
|
1413
|
+
fs3.closeSync(descriptor);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function releaseOwnedLease(filePath, claimedStat, owner, parseOwner, description) {
|
|
1417
|
+
const releasedPath = `${filePath}.released.${randomUUID2()}`;
|
|
1418
|
+
let moved = false;
|
|
1419
|
+
try {
|
|
1420
|
+
if (!markClaimReleased(filePath, claimedStat, owner, parseOwner)) return;
|
|
1421
|
+
try {
|
|
1422
|
+
fs3.renameSync(filePath, releasedPath);
|
|
1423
|
+
moved = true;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (error.code === "ENOENT") return;
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1428
|
+
const released = snapshotLeaseFile(releasedPath);
|
|
1429
|
+
const releasedOwner = released ? parseOwner(released.raw) : null;
|
|
1430
|
+
if (!released || released.stat.dev !== claimedStat.dev || released.stat.ino !== claimedStat.ino || releasedOwner?.ownerId !== owner.ownerId || releasedOwner.releasedAtMs === void 0) {
|
|
1431
|
+
restoreMovedLease(releasedPath, filePath);
|
|
1432
|
+
moved = false;
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
fs3.rmSync(releasedPath, {
|
|
1436
|
+
recursive: released.stat.isDirectory(),
|
|
1437
|
+
force: true
|
|
1438
|
+
});
|
|
1439
|
+
moved = false;
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
let restorationError;
|
|
1442
|
+
if (moved) {
|
|
1443
|
+
try {
|
|
1444
|
+
restoreMovedLease(releasedPath, filePath);
|
|
1445
|
+
} catch (restoreError) {
|
|
1446
|
+
restorationError = restoreError;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
console.error(
|
|
1450
|
+
`[fml] Auth: unable to remove released ${description}: ${error instanceof Error ? error.message : String(error)}${restorationError ? `; unable to restore it: ${restorationError instanceof Error ? restorationError.message : String(restorationError)}` : ""}`
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
function sleep(milliseconds, signal) {
|
|
1455
|
+
if (!signal) {
|
|
1456
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1457
|
+
}
|
|
1458
|
+
if (signal.aborted) {
|
|
1459
|
+
return Promise.reject(
|
|
1460
|
+
signal.reason ?? new DOMException("The operation was aborted", "AbortError")
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
return new Promise((resolve, reject) => {
|
|
1464
|
+
const timeout = setTimeout(() => {
|
|
1465
|
+
cleanup();
|
|
1466
|
+
resolve();
|
|
1467
|
+
}, milliseconds);
|
|
1468
|
+
const onAbort = () => {
|
|
1469
|
+
clearTimeout(timeout);
|
|
1470
|
+
cleanup();
|
|
1471
|
+
reject(
|
|
1472
|
+
signal.reason ?? new DOMException("The operation was aborted", "AbortError")
|
|
1473
|
+
);
|
|
1474
|
+
};
|
|
1475
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
1476
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function isProcessAlive(pid) {
|
|
1480
|
+
try {
|
|
1481
|
+
process.kill(pid, 0);
|
|
1482
|
+
return true;
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
return error.code === "EPERM";
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
var processIdentityChecks = /* @__PURE__ */ new Map();
|
|
1488
|
+
function ownerPidWasReused(owner, lockMtimeMs) {
|
|
1489
|
+
const now = Date.now();
|
|
1490
|
+
if (now - Math.max(owner.createdAtMs, lockMtimeMs) < PROCESS_IDENTITY_STALE_MS)
|
|
1491
|
+
return false;
|
|
1492
|
+
if (owner.pid === process.pid && owner.processInstanceId !== void 0) {
|
|
1493
|
+
return owner.processInstanceId !== CURRENT_PROCESS_INSTANCE_ID;
|
|
1494
|
+
}
|
|
1495
|
+
if (!owner.processIdentity) return false;
|
|
1496
|
+
const cached = processIdentityChecks.get(owner.ownerId);
|
|
1497
|
+
let currentIdentity;
|
|
1498
|
+
if (cached && now - cached.checkedAtMs < PROCESS_IDENTITY_RECHECK_MS) {
|
|
1499
|
+
currentIdentity = cached.identity;
|
|
1500
|
+
} else {
|
|
1501
|
+
currentIdentity = getProcessIdentity(owner.pid);
|
|
1502
|
+
processIdentityChecks.set(owner.ownerId, {
|
|
1503
|
+
checkedAtMs: now,
|
|
1504
|
+
identity: currentIdentity
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
return currentIdentity !== null && currentIdentity !== owner.processIdentity;
|
|
1508
|
+
}
|
|
1509
|
+
function writeClaimToDescriptor(descriptor, serializedOwner) {
|
|
1510
|
+
const claimedStat = fs3.fstatSync(descriptor);
|
|
1511
|
+
fs3.writeFileSync(descriptor, serializedOwner, { encoding: "utf8" });
|
|
1512
|
+
fs3.fsyncSync(descriptor);
|
|
1513
|
+
return claimedStat;
|
|
1514
|
+
}
|
|
1515
|
+
function recoverAbandonedRecoveryMarker(recoveryPath) {
|
|
1516
|
+
const observed = snapshotLeaseFile(recoveryPath);
|
|
1517
|
+
if (!observed) return;
|
|
1518
|
+
const owner = parseRecoveryMarkerOwner(observed.raw);
|
|
1519
|
+
const ownerAlive = owner ? isProcessAlive(owner.pid) : false;
|
|
1520
|
+
const reclaimable = owner ? owner.releasedAtMs !== void 0 || !ownerAlive || ownerAlive && ownerPidWasReused(owner, observed.stat.mtimeMs) : Date.now() - observed.stat.mtimeMs > OAUTH_REFRESH_RECOVERY_STALE_MS;
|
|
1521
|
+
if (!reclaimable) return;
|
|
1522
|
+
const abandonedPath = `${recoveryPath}.stale.${randomUUID2()}`;
|
|
1523
|
+
try {
|
|
1524
|
+
fs3.renameSync(recoveryPath, abandonedPath);
|
|
1525
|
+
const moved = snapshotLeaseFile(abandonedPath);
|
|
1526
|
+
if (!moved || !snapshotsMatch(observed, moved)) {
|
|
1527
|
+
restoreMovedLease(abandonedPath, recoveryPath);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
fs3.rmSync(abandonedPath, {
|
|
1531
|
+
recursive: moved.stat.isDirectory(),
|
|
1532
|
+
force: true
|
|
1533
|
+
});
|
|
1534
|
+
} catch (error) {
|
|
1535
|
+
if (error.code !== "ENOENT") throw error;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function acquireRecoveryMarker(recoveryPath) {
|
|
1539
|
+
const ownerId = randomUUID2();
|
|
1540
|
+
const owner = {
|
|
1541
|
+
version: 1,
|
|
1542
|
+
ownerId,
|
|
1543
|
+
pid: process.pid,
|
|
1544
|
+
processIdentity: getCurrentProcessIdentity(),
|
|
1545
|
+
processInstanceId: CURRENT_PROCESS_INSTANCE_ID,
|
|
1546
|
+
createdAtMs: Date.now()
|
|
1547
|
+
};
|
|
1548
|
+
let descriptor;
|
|
1549
|
+
try {
|
|
1550
|
+
descriptor = fs3.openSync(recoveryPath, "wx", 384);
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
if (error.code === "EEXIST") return null;
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
let claimedStat;
|
|
1556
|
+
try {
|
|
1557
|
+
claimedStat = writeClaimToDescriptor(
|
|
1558
|
+
descriptor,
|
|
1559
|
+
`${JSON.stringify(owner)}
|
|
1560
|
+
`
|
|
1561
|
+
);
|
|
1562
|
+
} finally {
|
|
1563
|
+
fs3.closeSync(descriptor);
|
|
1564
|
+
}
|
|
1565
|
+
const isOwned = () => pathMatchesClaim(
|
|
1566
|
+
recoveryPath,
|
|
1567
|
+
claimedStat,
|
|
1568
|
+
ownerId,
|
|
1569
|
+
parseRecoveryMarkerOwner
|
|
1570
|
+
);
|
|
1571
|
+
if (!isOwned()) return null;
|
|
1572
|
+
let released = false;
|
|
1573
|
+
return {
|
|
1574
|
+
isOwned,
|
|
1575
|
+
release: () => {
|
|
1576
|
+
if (released) return;
|
|
1577
|
+
released = true;
|
|
1578
|
+
if (isOwned()) {
|
|
1579
|
+
releaseOwnedLease(
|
|
1580
|
+
recoveryPath,
|
|
1581
|
+
claimedStat,
|
|
1582
|
+
owner,
|
|
1583
|
+
parseRecoveryMarkerOwner,
|
|
1584
|
+
"credential-lock recovery marker"
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function recoverStaleRefreshLock(lockPath) {
|
|
1591
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
1592
|
+
const recovery = acquireRecoveryMarker(recoveryPath);
|
|
1593
|
+
if (!recovery) return;
|
|
1594
|
+
try {
|
|
1595
|
+
const observed = snapshotLeaseFile(lockPath);
|
|
1596
|
+
if (!observed) return;
|
|
1597
|
+
let reclaimable;
|
|
1598
|
+
const owner = parseRefreshLockOwner(observed.raw);
|
|
1599
|
+
if (owner) {
|
|
1600
|
+
const alive = isProcessAlive(owner.pid);
|
|
1601
|
+
reclaimable = owner.releasedAtMs !== void 0 || !alive || alive && ownerPidWasReused(owner, observed.stat.mtimeMs);
|
|
1602
|
+
} else {
|
|
1603
|
+
reclaimable = Date.now() - observed.stat.mtimeMs > OAUTH_REFRESH_LOCK_STALE_MS;
|
|
1604
|
+
}
|
|
1605
|
+
if (!reclaimable) return;
|
|
1606
|
+
if (!recovery.isOwned()) return;
|
|
1607
|
+
const abandonedPath = `${lockPath}.stale.${randomUUID2()}`;
|
|
1608
|
+
try {
|
|
1609
|
+
fs3.renameSync(lockPath, abandonedPath);
|
|
1610
|
+
const moved = snapshotLeaseFile(abandonedPath);
|
|
1611
|
+
if (!moved || !snapshotsMatch(observed, moved)) {
|
|
1612
|
+
restoreMovedLease(abandonedPath, lockPath);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
fs3.rmSync(abandonedPath, {
|
|
1616
|
+
recursive: moved.stat.isDirectory(),
|
|
1617
|
+
force: true
|
|
1618
|
+
});
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
if (error.code !== "ENOENT") throw error;
|
|
1621
|
+
}
|
|
1622
|
+
} finally {
|
|
1623
|
+
recovery.release();
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
async function acquireRefreshLock(envName, signal) {
|
|
1627
|
+
try {
|
|
1628
|
+
ensureDir(envName);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
console.error(
|
|
1631
|
+
`[fml] Auth: unable to prepare refresh lock: ${error instanceof Error ? error.message : String(error)}`
|
|
1632
|
+
);
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
const lockPath = refreshLockPath(envName);
|
|
1636
|
+
const recoveryPath = `${lockPath}.recovery`;
|
|
1637
|
+
const deadline = Date.now() + OAUTH_REFRESH_LOCK_WAIT_MS;
|
|
1638
|
+
while (Date.now() < deadline) {
|
|
1639
|
+
if (signal?.aborted) {
|
|
1640
|
+
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
1641
|
+
}
|
|
1642
|
+
if (fs3.existsSync(recoveryPath)) {
|
|
1643
|
+
try {
|
|
1644
|
+
recoverAbandonedRecoveryMarker(recoveryPath);
|
|
1645
|
+
} catch (recoveryError) {
|
|
1646
|
+
console.error(
|
|
1647
|
+
`[fml] Auth: unable to recover refresh recovery marker: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
1648
|
+
);
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
let descriptor;
|
|
1655
|
+
try {
|
|
1656
|
+
descriptor = fs3.openSync(lockPath, "wx", 384);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
if (error.code !== "EEXIST") {
|
|
1659
|
+
console.error(
|
|
1660
|
+
`[fml] Auth: unable to acquire refresh lock: ${error instanceof Error ? error.message : String(error)}`
|
|
1661
|
+
);
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
recoverStaleRefreshLock(lockPath);
|
|
1666
|
+
} catch (recoveryError) {
|
|
1667
|
+
console.error(
|
|
1668
|
+
`[fml] Auth: unable to recover refresh lock: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`
|
|
1669
|
+
);
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
const ownerId = randomUUID2();
|
|
1676
|
+
const owner = {
|
|
1677
|
+
version: 1,
|
|
1678
|
+
ownerId,
|
|
1679
|
+
pid: process.pid,
|
|
1680
|
+
processIdentity: getCurrentProcessIdentity(),
|
|
1681
|
+
processInstanceId: CURRENT_PROCESS_INSTANCE_ID,
|
|
1682
|
+
createdAtMs: Date.now()
|
|
1683
|
+
};
|
|
1684
|
+
let claimedStat;
|
|
1685
|
+
try {
|
|
1686
|
+
claimedStat = writeClaimToDescriptor(
|
|
1687
|
+
descriptor,
|
|
1688
|
+
`${JSON.stringify(owner)}
|
|
1689
|
+
`
|
|
1690
|
+
);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
fs3.closeSync(descriptor);
|
|
1693
|
+
console.error(
|
|
1694
|
+
`[fml] Auth: unable to initialize refresh lock: ${error instanceof Error ? error.message : String(error)}`
|
|
1695
|
+
);
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
fs3.closeSync(descriptor);
|
|
1699
|
+
const isOwned = () => pathMatchesClaim(lockPath, claimedStat, ownerId, parseRefreshLockOwner);
|
|
1700
|
+
const releaseClaim = () => releaseOwnedLease(
|
|
1701
|
+
lockPath,
|
|
1702
|
+
claimedStat,
|
|
1703
|
+
owner,
|
|
1704
|
+
parseRefreshLockOwner,
|
|
1705
|
+
"credential lock"
|
|
1706
|
+
);
|
|
1707
|
+
if (signal?.aborted) {
|
|
1708
|
+
releaseClaim();
|
|
1709
|
+
throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
1710
|
+
}
|
|
1711
|
+
let entryBlocked;
|
|
1712
|
+
try {
|
|
1713
|
+
entryBlocked = fs3.existsSync(recoveryPath) || !isOwned();
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
console.error(
|
|
1716
|
+
`[fml] Auth: unable to verify refresh lock ownership: ${describeError(error)}`
|
|
1717
|
+
);
|
|
1718
|
+
try {
|
|
1719
|
+
if (isOwned()) releaseClaim();
|
|
1720
|
+
} catch {
|
|
1721
|
+
}
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
if (entryBlocked) {
|
|
1725
|
+
try {
|
|
1726
|
+
await sleep(OAUTH_REFRESH_LOCK_POLL_MS, signal);
|
|
1727
|
+
} finally {
|
|
1728
|
+
try {
|
|
1729
|
+
if (isOwned()) releaseClaim();
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
console.error(
|
|
1732
|
+
`[fml] Auth: unable to release a blocked refresh lock claim: ${describeError(error)}`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
let released = false;
|
|
1739
|
+
return {
|
|
1740
|
+
isOwned,
|
|
1741
|
+
release: () => {
|
|
1742
|
+
if (released) return;
|
|
1743
|
+
released = true;
|
|
1744
|
+
let owned = false;
|
|
1745
|
+
try {
|
|
1746
|
+
owned = isOwned();
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
console.error(
|
|
1749
|
+
`[fml] Auth: unable to verify credential lock ownership during release: ${describeError(error)}`
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
if (owned) releaseClaim();
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
console.error(
|
|
1757
|
+
(() => {
|
|
1758
|
+
try {
|
|
1759
|
+
const owner = snapshotLeaseFile(lockPath);
|
|
1760
|
+
const parsedOwner = owner ? parseRefreshLockOwner(owner.raw) : null;
|
|
1761
|
+
return parsedOwner && isProcessAlive(parsedOwner.pid) ? `[fml] Auth: credential lock is still owned by live process ${parsedOwner.pid}; refusing an unsafe takeover` : "[fml] Auth: timed out waiting for another process to refresh credentials";
|
|
1762
|
+
} catch {
|
|
1763
|
+
return "[fml] Auth: timed out waiting for another process to refresh credentials";
|
|
1764
|
+
}
|
|
1765
|
+
})()
|
|
1766
|
+
);
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
function isLockOwnedSafely(lock) {
|
|
1770
|
+
try {
|
|
1771
|
+
return lock.isOwned();
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
console.error(
|
|
1774
|
+
`[fml] Auth: unable to verify credential lock ownership: ${describeError(error)}`
|
|
1775
|
+
);
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
async function withRefreshLock(envName, mutation, options) {
|
|
1780
|
+
const lock = await acquireRefreshLock(envName, options?.signal);
|
|
1781
|
+
if (!lock) {
|
|
1782
|
+
throw new Error("Unable to acquire the credential mutation lock");
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
if (!isLockOwnedSafely(lock)) {
|
|
1786
|
+
throw new Error("Credential mutation lock ownership was lost");
|
|
1787
|
+
}
|
|
1788
|
+
reconcilePendingOAuthRefreshes(envName, options?.unreadableStorePolicy);
|
|
1789
|
+
if (!isLockOwnedSafely(lock)) {
|
|
1790
|
+
throw new Error("Credential mutation lock ownership was lost");
|
|
1791
|
+
}
|
|
1792
|
+
const result = await mutation(lock);
|
|
1793
|
+
if (!isLockOwnedSafely(lock)) {
|
|
1794
|
+
throw new Error("Credential mutation lock ownership was lost");
|
|
1795
|
+
}
|
|
1796
|
+
return result;
|
|
1797
|
+
} finally {
|
|
1798
|
+
lock.release();
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function refreshServiceToken(refreshToken2, envName, siteUrl, signal) {
|
|
1802
|
+
try {
|
|
1803
|
+
if (signal?.aborted) return null;
|
|
1804
|
+
const convexUrl = (envName ? resolveEnvConvexUrl(envName) : null) ?? CONVEX_URL;
|
|
1805
|
+
const requestTimeout = AbortSignal.timeout(
|
|
1806
|
+
OAUTH_REFRESH_REQUEST_TIMEOUT_MS
|
|
1807
|
+
);
|
|
1808
|
+
const response = await fetch(
|
|
1809
|
+
`${siteUrl ?? getSiteUrlForConvexUrl(convexUrl)}/api/tokens/refresh`,
|
|
1810
|
+
{
|
|
1811
|
+
method: "POST",
|
|
1812
|
+
headers: {
|
|
1813
|
+
"Content-Type": "application/json",
|
|
1814
|
+
Authorization: `Bearer ${refreshToken2}`
|
|
1815
|
+
},
|
|
1816
|
+
signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
|
|
1817
|
+
}
|
|
1818
|
+
);
|
|
1819
|
+
if (!response.ok) {
|
|
1820
|
+
console.error(
|
|
1821
|
+
`[fml] Service token refresh failed: HTTP ${response.status}`
|
|
1822
|
+
);
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
const data = await response.json();
|
|
1826
|
+
if (!data.ok || !data.accessToken) {
|
|
1827
|
+
console.error(`[fml] Service token refresh error: ${data.error}`);
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
serviceTokenCaches.set(cacheKey(envName), {
|
|
1831
|
+
accessToken: data.accessToken,
|
|
1832
|
+
expiresAt: data.expiresAt
|
|
1833
|
+
});
|
|
1834
|
+
return data.accessToken;
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
if (signal?.aborted) return null;
|
|
1837
|
+
Sentry.captureException(err);
|
|
1838
|
+
console.error(`[fml] Service token refresh error: ${err}`);
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
async function getValidToken(opts) {
|
|
1843
|
+
if (opts?.signal?.aborted) return null;
|
|
1844
|
+
const envName = resolveEnvName(opts?.env);
|
|
1845
|
+
const forceRefresh = opts?.forceRefresh === true;
|
|
1846
|
+
const key = cacheKey(envName);
|
|
1847
|
+
const envToken = process.env.FML_TOKEN;
|
|
1848
|
+
if (envToken?.startsWith("fml_srt_")) {
|
|
1849
|
+
const cached = serviceTokenCaches.get(key);
|
|
1850
|
+
if (!forceRefresh && cached && cached.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
|
|
1851
|
+
return cached.accessToken;
|
|
1852
|
+
}
|
|
1853
|
+
return await getOrStartTokenRefresh(
|
|
1854
|
+
serviceRefreshPromises,
|
|
1855
|
+
key,
|
|
1856
|
+
forceRefresh,
|
|
1857
|
+
() => refreshServiceToken(envToken, envName, void 0, opts?.signal),
|
|
1858
|
+
opts?.signal
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
if (envToken?.startsWith("fml_st_")) return envToken;
|
|
1862
|
+
if (envToken) {
|
|
1863
|
+
console.error(
|
|
1864
|
+
"[fml] Auth: unsupported FML_TOKEN format. Expected fml_srt_* or fml_st_*."
|
|
1865
|
+
);
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
const stored = readTokens(envName);
|
|
1869
|
+
if (!stored) {
|
|
1870
|
+
console.error(
|
|
1871
|
+
envName ? `[fml] Auth: no stored tokens for env "${envName}"` : "[fml] Auth: no stored tokens"
|
|
1872
|
+
);
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
if (!forceRefresh && stored.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
|
|
1876
|
+
return stored.accessToken;
|
|
1877
|
+
}
|
|
1878
|
+
if (stored.tokenType === "service") {
|
|
1879
|
+
return await getOrStartTokenRefresh(
|
|
1880
|
+
serviceRefreshPromises,
|
|
1881
|
+
key,
|
|
1882
|
+
forceRefresh,
|
|
1883
|
+
() => withRefreshLock(
|
|
1884
|
+
envName,
|
|
1885
|
+
async (lock) => {
|
|
1886
|
+
const current = readTokens(envName);
|
|
1887
|
+
if (!current) return null;
|
|
1888
|
+
if (!forceRefresh && current.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
|
|
1889
|
+
return current.accessToken;
|
|
1890
|
+
}
|
|
1891
|
+
if (current.tokenType !== "service") {
|
|
1892
|
+
return current.accessToken;
|
|
1893
|
+
}
|
|
1894
|
+
if (!lock.isOwned()) return null;
|
|
1895
|
+
const newAccessToken = await refreshServiceToken(
|
|
1896
|
+
current.refreshToken,
|
|
1897
|
+
envName,
|
|
1898
|
+
void 0,
|
|
1899
|
+
opts?.signal
|
|
1900
|
+
);
|
|
1901
|
+
if (!lock.isOwned()) return null;
|
|
1902
|
+
const cached = serviceTokenCaches.get(key);
|
|
1903
|
+
if (newAccessToken && cached) {
|
|
1904
|
+
writeTokensUnlocked(
|
|
1905
|
+
{
|
|
1906
|
+
...current,
|
|
1907
|
+
accessToken: newAccessToken,
|
|
1908
|
+
expiresAt: cached.expiresAt
|
|
1909
|
+
},
|
|
1910
|
+
envName
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
return newAccessToken;
|
|
1914
|
+
},
|
|
1915
|
+
{ signal: opts?.signal }
|
|
1916
|
+
).catch((error) => {
|
|
1917
|
+
if (opts?.signal?.aborted) return null;
|
|
1918
|
+
console.error(
|
|
1919
|
+
`[fml] Auth: unable to refresh stored service credentials: ${error instanceof Error ? error.message : String(error)}`
|
|
1920
|
+
);
|
|
1921
|
+
return null;
|
|
1922
|
+
}),
|
|
1923
|
+
opts?.signal
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
if (stored.refreshInvalidAt) {
|
|
1927
|
+
console.error(
|
|
1928
|
+
"[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
|
|
1929
|
+
);
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
return await getOrStartTokenRefresh(
|
|
1933
|
+
oauthRefreshPromises,
|
|
1934
|
+
key,
|
|
1935
|
+
forceRefresh,
|
|
1936
|
+
() => refreshTokenWithLock(envName, forceRefresh, opts?.signal),
|
|
1937
|
+
opts?.signal
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
async function refreshTokenWithLock(envName, forceRefresh = false, signal) {
|
|
1941
|
+
const lock = await acquireRefreshLock(envName, signal);
|
|
1942
|
+
if (!lock) return null;
|
|
1943
|
+
try {
|
|
1944
|
+
if (!isLockOwnedSafely(lock)) return null;
|
|
1945
|
+
try {
|
|
1946
|
+
reconcilePendingOAuthRefreshes(envName);
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
console.error(
|
|
1949
|
+
`[fml] Auth: unable to reconcile staged refresh results: ${describeError(error)}`
|
|
1950
|
+
);
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
if (!isLockOwnedSafely(lock)) return null;
|
|
1954
|
+
const current = readTokens(envName);
|
|
1955
|
+
if (!current) {
|
|
1956
|
+
console.error("[fml] Auth: credentials disappeared during refresh");
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
if (!forceRefresh && current.expiresAt > Date.now() + TOKEN_REFRESH_WINDOW_MS) {
|
|
1960
|
+
return current.accessToken;
|
|
1961
|
+
}
|
|
1962
|
+
if (current.refreshInvalidAt) {
|
|
1963
|
+
console.error(
|
|
1964
|
+
"[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
|
|
1965
|
+
);
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
if (current.tokenType === "service") {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
if (!isLockOwnedSafely(lock)) return null;
|
|
1972
|
+
return await refreshToken(current, lock, envName, signal);
|
|
1973
|
+
} finally {
|
|
1974
|
+
lock.release();
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function refreshToken(stored, lock, envName, signal) {
|
|
1978
|
+
if (!stored.workosClientId) {
|
|
1979
|
+
console.error(
|
|
1980
|
+
"[fml] Auth: token expired, missing workosClientId \u2014 run `fml login`"
|
|
1981
|
+
);
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
if (signal?.aborted) return null;
|
|
1986
|
+
if (!lock.isOwned()) return null;
|
|
1987
|
+
const requestTimeout = AbortSignal.timeout(
|
|
1988
|
+
OAUTH_REFRESH_REQUEST_TIMEOUT_MS
|
|
1989
|
+
);
|
|
1990
|
+
const response = await fetch(
|
|
1991
|
+
`${WORKOS_API_URL}/user_management/authenticate`,
|
|
1992
|
+
{
|
|
1993
|
+
method: "POST",
|
|
1994
|
+
headers: { "Content-Type": "application/json" },
|
|
1995
|
+
body: JSON.stringify({
|
|
1996
|
+
grant_type: "refresh_token",
|
|
1997
|
+
client_id: stored.workosClientId,
|
|
1998
|
+
refresh_token: stored.refreshToken
|
|
1999
|
+
}),
|
|
2000
|
+
signal: signal ? AbortSignal.any([signal, requestTimeout]) : requestTimeout
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
if (!response.ok) {
|
|
2004
|
+
if (!lock.isOwned()) return null;
|
|
2005
|
+
let oauthError = null;
|
|
2006
|
+
try {
|
|
2007
|
+
const errorBody = await response.json();
|
|
2008
|
+
if (errorBody && typeof errorBody === "object" && "error" in errorBody && typeof errorBody.error === "string") {
|
|
2009
|
+
oauthError = errorBody.error;
|
|
2010
|
+
}
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
if (oauthError === "invalid_grant") {
|
|
2014
|
+
const current2 = readTokens(envName);
|
|
2015
|
+
if (lock.isOwned() && current2?.refreshToken === stored.refreshToken) {
|
|
2016
|
+
writeTokensUnlocked(
|
|
2017
|
+
{ ...current2, refreshInvalidAt: Date.now() },
|
|
2018
|
+
envName
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
console.error(
|
|
2022
|
+
"[fml] Auth: refresh token is no longer valid \u2014 run `fml login`"
|
|
2023
|
+
);
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
console.error(
|
|
2027
|
+
`[fml] Auth: refresh failed (HTTP ${response.status}) \u2014 token preserved`
|
|
2028
|
+
);
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
const data = await response.json();
|
|
2032
|
+
const userName = [data.user.first_name, data.user.last_name].filter(Boolean).join(" ");
|
|
2033
|
+
const refreshed = {
|
|
2034
|
+
accessToken: data.access_token,
|
|
2035
|
+
refreshToken: data.refresh_token,
|
|
2036
|
+
expiresAt: Date.now() + (data.expires_in ?? 300) * 1e3,
|
|
2037
|
+
user: {
|
|
2038
|
+
id: data.user.id,
|
|
2039
|
+
email: data.user.email,
|
|
2040
|
+
name: userName || stored.user.name
|
|
2041
|
+
},
|
|
2042
|
+
orgId: stored.orgId,
|
|
2043
|
+
orgSlug: stored.orgSlug,
|
|
2044
|
+
workosClientId: stored.workosClientId
|
|
2045
|
+
};
|
|
2046
|
+
const pendingPath = stagePendingOAuthRefresh(
|
|
2047
|
+
stored.refreshToken,
|
|
2048
|
+
refreshed,
|
|
2049
|
+
envName
|
|
2050
|
+
);
|
|
2051
|
+
const current = readTokens(envName);
|
|
2052
|
+
if (current?.refreshToken === stored.refreshToken && lock.isOwned()) {
|
|
2053
|
+
writeTokensUnlocked(
|
|
2054
|
+
{
|
|
2055
|
+
...refreshed,
|
|
2056
|
+
orgId: current.orgId,
|
|
2057
|
+
orgSlug: current.orgSlug
|
|
2058
|
+
},
|
|
2059
|
+
envName
|
|
2060
|
+
);
|
|
2061
|
+
removePendingOAuthRefresh(pendingPath);
|
|
2062
|
+
return refreshed.accessToken;
|
|
2063
|
+
}
|
|
2064
|
+
if (!current) return null;
|
|
2065
|
+
if (current.refreshToken !== stored.refreshToken) {
|
|
2066
|
+
return current.accessToken;
|
|
2067
|
+
}
|
|
2068
|
+
return refreshed.accessToken;
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
if (signal?.aborted) return null;
|
|
2071
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2072
|
+
console.error(
|
|
2073
|
+
`[fml] Auth: refresh failed (network error: ${msg}) \u2014 token preserved`
|
|
2074
|
+
);
|
|
2075
|
+
Sentry.captureException(err);
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/repo.ts
|
|
2081
|
+
import { execFile, execFileSync as execFileSync2 } from "node:child_process";
|
|
2082
|
+
var REPO_CACHE = /* @__PURE__ */ new Map();
|
|
2083
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
2084
|
+
function cachedRepo(cwd) {
|
|
2085
|
+
const entry = REPO_CACHE.get(cwd);
|
|
2086
|
+
if (!entry) return { hit: false };
|
|
2087
|
+
if (entry.expiresAtMs <= Date.now()) {
|
|
2088
|
+
REPO_CACHE.delete(cwd);
|
|
2089
|
+
return { hit: false };
|
|
2090
|
+
}
|
|
2091
|
+
REPO_CACHE.delete(cwd);
|
|
2092
|
+
REPO_CACHE.set(cwd, entry);
|
|
2093
|
+
return { hit: true, value: entry.value };
|
|
2094
|
+
}
|
|
2095
|
+
function git(cwd, args, timeoutMs) {
|
|
2096
|
+
try {
|
|
2097
|
+
return execFileSync2("git", ["-C", cwd, ...args], {
|
|
2098
|
+
encoding: "utf8",
|
|
2099
|
+
timeout: timeoutMs,
|
|
2100
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2101
|
+
windowsHide: true
|
|
2102
|
+
}).trim() || null;
|
|
2103
|
+
} catch {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function repositorySlug(remote) {
|
|
2108
|
+
const ssh = remote.match(/^[^@]+@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
2109
|
+
if (ssh?.[1]) return ssh[1];
|
|
2110
|
+
try {
|
|
2111
|
+
const url = new URL(remote);
|
|
2112
|
+
const slug = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
2113
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(slug) ? slug : null;
|
|
2114
|
+
} catch {
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function resolveRepositorySlugFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
2119
|
+
const cached = cachedRepo(cwd);
|
|
2120
|
+
if (cached.hit) return cached.value?.repo ?? null;
|
|
2121
|
+
const remote = git(cwd, ["remote", "get-url", "origin"], timeoutMs);
|
|
2122
|
+
return remote ? repositorySlug(remote) : null;
|
|
2123
|
+
}
|
|
2124
|
+
function resolveRepositoryRootFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
2125
|
+
return git(cwd, ["rev-parse", "--show-toplevel"], timeoutMs);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/hooks/sync-loop.ts
|
|
2129
|
+
var MAX_RETRY_INTERVAL_MS = 5 * 6e4;
|
|
2130
|
+
var MAX_RETRY_AFTER_MS = 24 * 60 * 6e4;
|
|
2131
|
+
var MAX_HOOK_EVENT_ROW_JSON_BYTES = 2 * 1024 * 1024;
|
|
2132
|
+
var MAX_SESSION_ROW_JSON_BYTES = 2 * 1024 * 1024;
|
|
2133
|
+
var MAX_BATCH_JSON_BYTES = 3 * 1024 * 1024;
|
|
2134
|
+
var MAX_EXTRACTED_TEXT_BYTES2 = 64 * 1024;
|
|
2135
|
+
var MAX_CONTEXT_TEXT_BYTES = 16 * 1024;
|
|
2136
|
+
var MAX_REJECTION_REASON_BYTES = 3 * 1024;
|
|
2137
|
+
function activeHookDestinationSnapshot() {
|
|
2138
|
+
const environment = getActiveEnv();
|
|
2139
|
+
const destinationIdentity = getSyncDestinationIdentity(environment.name);
|
|
2140
|
+
if (!destinationIdentity) {
|
|
2141
|
+
return { kind: "unauthenticated", environment: environment.name };
|
|
2142
|
+
}
|
|
2143
|
+
return {
|
|
2144
|
+
kind: "ready",
|
|
2145
|
+
binding: {
|
|
2146
|
+
environment: environment.name,
|
|
2147
|
+
targetUrl: getSiteUrlForConvexUrl(environment.convexUrl),
|
|
2148
|
+
destinationIdentity
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
function getActiveHookDestinationBinding() {
|
|
2153
|
+
const snapshot = activeHookDestinationSnapshot();
|
|
2154
|
+
return snapshot.kind === "ready" ? snapshot.binding : null;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/hooks/context.ts
|
|
2158
|
+
var HOOK_CONTEXT_ENDPOINT = "/v1/panopticon/hook-context";
|
|
2159
|
+
var HOOK_CONTEXT_TIMEOUT_MS = 2e3;
|
|
2160
|
+
var MAX_ADDITIONAL_CONTEXT_CHARS = 3700;
|
|
2161
|
+
var MAX_RECEIPT_ITEMS = 5;
|
|
2162
|
+
var MAX_RECEIPT_LABEL_CHARS = 48;
|
|
2163
|
+
var MAX_RECEIPT_SUMMARY_CHARS = 240;
|
|
2164
|
+
var MAX_RECEIPT_PRIORITY = 100;
|
|
2165
|
+
var MAX_RECEIPT_TIMESTAMP_MS = 864e13;
|
|
2166
|
+
var MAX_RENDERED_RECEIPT_ITEMS = 3;
|
|
2167
|
+
var MODEL_CONTEXT_GUIDANCE = "If this FML context materially informs your work, naturally mention FML, the specific context used, and how it informed your work in the final response. Otherwise, do not mention it. Do not claim causal impact or time savings.";
|
|
2168
|
+
var CONTEXT_GUIDANCE_SEPARATOR = "\n\n";
|
|
2169
|
+
var STATE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2170
|
+
var INJECTION_ID_VERSION = "v1";
|
|
2171
|
+
var EDIT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
2172
|
+
"apply_patch",
|
|
2173
|
+
"edit",
|
|
2174
|
+
"multiedit",
|
|
2175
|
+
"notebookedit",
|
|
2176
|
+
"write",
|
|
2177
|
+
"write_file"
|
|
2178
|
+
]);
|
|
2179
|
+
var READ_TOOL_NAMES = /* @__PURE__ */ new Set(["read", "read_file"]);
|
|
2180
|
+
function hookContextFileOperation(toolName) {
|
|
2181
|
+
const normalizedToolName = toolName.toLowerCase();
|
|
2182
|
+
if (EDIT_TOOL_NAMES.has(normalizedToolName)) return "edit";
|
|
2183
|
+
if (READ_TOOL_NAMES.has(normalizedToolName) || SHELL_TOOL_NAMES.has(normalizedToolName)) {
|
|
2184
|
+
return "read";
|
|
2185
|
+
}
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
function extractHookContextFileTrigger(toolName, input) {
|
|
2189
|
+
const fileOperation = hookContextFileOperation(toolName);
|
|
2190
|
+
if (!fileOperation || !input || typeof input !== "object" || Array.isArray(input)) {
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
2193
|
+
const record = input;
|
|
2194
|
+
const directPath = record.file_path ?? record.filePath ?? record.path;
|
|
2195
|
+
if (typeof directPath === "string" && directPath.length > 0) {
|
|
2196
|
+
return { filePath: directPath, fileOperation };
|
|
2197
|
+
}
|
|
2198
|
+
if (!SHELL_TOOL_NAMES.has(toolName.toLowerCase())) return null;
|
|
2199
|
+
const command = record.command ?? record.cmd;
|
|
2200
|
+
if (typeof command !== "string") return null;
|
|
2201
|
+
const filePath = extractReadFilePathFromCommand(command);
|
|
2202
|
+
return filePath ? { filePath, fileOperation } : null;
|
|
2203
|
+
}
|
|
2204
|
+
function sameDestination(left, right) {
|
|
2205
|
+
return left.environment === right.environment && left.targetUrl === right.targetUrl && left.destinationIdentity === right.destinationIdentity;
|
|
2206
|
+
}
|
|
2207
|
+
function hookContextInjectionId(target, syncId) {
|
|
2208
|
+
const digest = createHash3("sha256").update(`${INJECTION_ID_VERSION}\0${target}\0${syncId}`).digest("base64url");
|
|
2209
|
+
return `${INJECTION_ID_VERSION}_${digest}`;
|
|
2210
|
+
}
|
|
2211
|
+
function canonicalHookContextFilePath(filePath, cwd, repositoryRoot) {
|
|
2212
|
+
const windows = [filePath, cwd, repositoryRoot].some(
|
|
2213
|
+
(value) => value !== void 0 && value !== null && /^[A-Za-z]:[\\/]/.test(value)
|
|
2214
|
+
);
|
|
2215
|
+
const pathApi = windows ? path6.win32 : path6.posix;
|
|
2216
|
+
const normalized = pathApi.normalize(filePath);
|
|
2217
|
+
const resolved = pathApi.isAbsolute(normalized) ? normalized : cwd && pathApi.isAbsolute(cwd) ? pathApi.resolve(cwd, normalized) : normalized;
|
|
2218
|
+
if (repositoryRoot && pathApi.isAbsolute(resolved)) {
|
|
2219
|
+
const relative2 = pathApi.relative(
|
|
2220
|
+
pathApi.normalize(repositoryRoot),
|
|
2221
|
+
resolved
|
|
2222
|
+
);
|
|
2223
|
+
if (relative2 && relative2 !== ".." && !relative2.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative2)) {
|
|
2224
|
+
return relative2.replace(/\\/g, "/");
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (!pathApi.isAbsolute(resolved)) {
|
|
2228
|
+
return resolved.replace(/^[.][\\/]/, "").replace(/\\/g, "/");
|
|
2229
|
+
}
|
|
2230
|
+
if (!cwd || pathApi.isAbsolute(cwd) !== pathApi.isAbsolute(resolved)) {
|
|
2231
|
+
return resolved.replace(/\\/g, "/");
|
|
2232
|
+
}
|
|
2233
|
+
const relative = pathApi.relative(pathApi.normalize(cwd), resolved);
|
|
2234
|
+
if (relative && relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative)) {
|
|
2235
|
+
return relative.replace(/\\/g, "/");
|
|
2236
|
+
}
|
|
2237
|
+
return resolved.replace(/\\/g, "/");
|
|
2238
|
+
}
|
|
2239
|
+
var RECEIPT_LABELS = /* @__PURE__ */ new Set([
|
|
2240
|
+
"Recent work",
|
|
2241
|
+
"Relevant history",
|
|
2242
|
+
"File history",
|
|
2243
|
+
"Related files"
|
|
2244
|
+
]);
|
|
2245
|
+
function isSanitizedInlineText(value, maxChars) {
|
|
2246
|
+
return value.length > 0 && value.length <= maxChars && !/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u.test(
|
|
2247
|
+
value
|
|
2248
|
+
) && value === value.replace(/\s+/g, " ").trim();
|
|
2249
|
+
}
|
|
2250
|
+
function isReceiptItem(value) {
|
|
2251
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2252
|
+
const item = value;
|
|
2253
|
+
return typeof item.label === "string" && RECEIPT_LABELS.has(item.label) && isSanitizedInlineText(item.label, MAX_RECEIPT_LABEL_CHARS) && typeof item.summary === "string" && isSanitizedInlineText(item.summary, MAX_RECEIPT_SUMMARY_CHARS) && typeof item.priority === "number" && Number.isSafeInteger(item.priority) && item.priority >= 0 && item.priority <= MAX_RECEIPT_PRIORITY && (item.occurredAtMs === void 0 || typeof item.occurredAtMs === "number" && Number.isSafeInteger(item.occurredAtMs) && item.occurredAtMs >= 0 && item.occurredAtMs <= MAX_RECEIPT_TIMESTAMP_MS);
|
|
2254
|
+
}
|
|
2255
|
+
function receiptItemsFromResponse(value) {
|
|
2256
|
+
if (value === void 0) return [];
|
|
2257
|
+
if (!Array.isArray(value) || value.length > MAX_RECEIPT_ITEMS) return null;
|
|
2258
|
+
if (!value.every(isReceiptItem)) return null;
|
|
2259
|
+
return value;
|
|
2260
|
+
}
|
|
2261
|
+
var RECEIPT_LINE_WIDTH = 88;
|
|
2262
|
+
var RECEIPT_SEGMENTER = new Intl.Segmenter(void 0, {
|
|
2263
|
+
granularity: "grapheme"
|
|
2264
|
+
});
|
|
2265
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2266
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || // Supplementary CJK-adjacent scripts and Japanese kana are rendered
|
|
2267
|
+
// double-width by terminals. Keep these deliberately conservative:
|
|
2268
|
+
// wrapping early is safer than emitting a line beyond the receipt bound.
|
|
2269
|
+
codePoint >= 94176 && codePoint <= 94207 || codePoint >= 94208 && codePoint <= 100351 || codePoint >= 100352 && codePoint <= 101887 || codePoint >= 110592 && codePoint <= 111359 || codePoint >= 127488 && codePoint <= 127569 || codePoint >= 131072 && codePoint <= 262141);
|
|
2270
|
+
}
|
|
2271
|
+
function isDefaultIgnorableOrMark(value) {
|
|
2272
|
+
return /^[\p{Mark}\p{Default_Ignorable_Code_Point}]+$/u.test(value);
|
|
2273
|
+
}
|
|
2274
|
+
function isEmojiGrapheme(value) {
|
|
2275
|
+
return new RegExp("\\p{Extended_Pictographic}", "u").test(value) || /[\u{1F1E6}-\u{1F1FF}]/u.test(value) || value.includes("\u20E3");
|
|
2276
|
+
}
|
|
2277
|
+
function receiptDisplayWidth(value) {
|
|
2278
|
+
let width = 0;
|
|
2279
|
+
for (const { segment } of RECEIPT_SEGMENTER.segment(value)) {
|
|
2280
|
+
if (isDefaultIgnorableOrMark(segment)) continue;
|
|
2281
|
+
if (isEmojiGrapheme(segment)) {
|
|
2282
|
+
width += 2;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
const codePoints = [...segment].map(
|
|
2286
|
+
(character) => character.codePointAt(0)
|
|
2287
|
+
);
|
|
2288
|
+
width += codePoints.some(
|
|
2289
|
+
(codePoint) => codePoint !== void 0 && isFullWidthCodePoint(codePoint)
|
|
2290
|
+
) ? 2 : 1;
|
|
2291
|
+
}
|
|
2292
|
+
return width;
|
|
2293
|
+
}
|
|
2294
|
+
function takeGraphemesToWidth(value, maxWidth) {
|
|
2295
|
+
let rendered = "";
|
|
2296
|
+
let renderedWidth = 0;
|
|
2297
|
+
for (const { segment } of RECEIPT_SEGMENTER.segment(value)) {
|
|
2298
|
+
const segmentWidth = receiptDisplayWidth(segment);
|
|
2299
|
+
if (renderedWidth + segmentWidth > maxWidth) break;
|
|
2300
|
+
rendered += segment;
|
|
2301
|
+
renderedWidth += segmentWidth;
|
|
2302
|
+
}
|
|
2303
|
+
return rendered;
|
|
2304
|
+
}
|
|
2305
|
+
function splitAtDisplayWidth(value, maxWidth) {
|
|
2306
|
+
let renderedWidth = 0;
|
|
2307
|
+
let end = 0;
|
|
2308
|
+
let lastWhitespaceEnd = 0;
|
|
2309
|
+
for (const { segment, index } of RECEIPT_SEGMENTER.segment(value)) {
|
|
2310
|
+
const segmentWidth = receiptDisplayWidth(segment);
|
|
2311
|
+
if (renderedWidth + segmentWidth > maxWidth) break;
|
|
2312
|
+
renderedWidth += segmentWidth;
|
|
2313
|
+
end = index + segment.length;
|
|
2314
|
+
if (/^\s$/u.test(segment)) lastWhitespaceEnd = end;
|
|
2315
|
+
}
|
|
2316
|
+
if (end === value.length) return { head: value, tail: "" };
|
|
2317
|
+
if (lastWhitespaceEnd > 0) {
|
|
2318
|
+
return {
|
|
2319
|
+
head: value.slice(0, lastWhitespaceEnd).trimEnd(),
|
|
2320
|
+
tail: value.slice(lastWhitespaceEnd).trimStart()
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
return { head: value.slice(0, end), tail: value.slice(end) };
|
|
2324
|
+
}
|
|
2325
|
+
function isSafeInlineCode(value) {
|
|
2326
|
+
if (!/^[A-Za-z0-9@._/$-]+$/u.test(value)) return false;
|
|
2327
|
+
const segments = value.split("/");
|
|
2328
|
+
if (segments.some(
|
|
2329
|
+
(segment) => segment === "" || segment === "." || segment === ".."
|
|
2330
|
+
)) {
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
return value.includes("/") || /^[^.]+\.[A-Za-z0-9_-]+$/u.test(value) || /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
|
|
2334
|
+
}
|
|
2335
|
+
function removeMarkdownEmphasis(value) {
|
|
2336
|
+
return value.replace(/(\*\*|__)(.+?)\1/gu, "$2").replace(/~~(.+?)~~/gu, "$1").replace(/(?<!\w)\*([^*]+)\*(?!\w)/gu, "$1").replace(/(?<!\w)_([^_]+)_(?!\w)/gu, "$1");
|
|
2337
|
+
}
|
|
2338
|
+
var UNSAFE_RECEIPT_CHARACTERS = /[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;
|
|
2339
|
+
var HTML_CHARACTER_REFERENCE = /&(?:#(?:[xX][0-9A-Fa-f]+|\d+)|[A-Za-z][A-Za-z0-9]+);/gu;
|
|
2340
|
+
function renderReceiptPlainText(value) {
|
|
2341
|
+
return removeMarkdownEmphasis(value).replace(HTML_CHARACTER_REFERENCE, "character reference").replace(/\\([\\`*_{}\[\]<>()[\]#+\-=!|])/gu, "$1").replace(/`/gu, "");
|
|
2342
|
+
}
|
|
2343
|
+
function renderReceiptSummary(summary) {
|
|
2344
|
+
const withoutLinks = summary.replace(UNSAFE_RECEIPT_CHARACTERS, "").replace(/!?\[([^\]]*)\]\([^)]*\)/gu, "$1").replace(/!?\[([^\]]*)\]\[[^\]]*\]/gu, "$1").replace(/<https?:\/\/[^>\s]+>/gu, "link").replace(/https?:\/\/[^\s<>()]+/gu, "link").replace(/<[^>]*>/gu, "");
|
|
2345
|
+
let rendered = "";
|
|
2346
|
+
let cursor = 0;
|
|
2347
|
+
for (const match of withoutLinks.matchAll(/`([^`\r\n]+)`/gu)) {
|
|
2348
|
+
const matchIndex = match.index ?? 0;
|
|
2349
|
+
rendered += renderReceiptPlainText(withoutLinks.slice(cursor, matchIndex));
|
|
2350
|
+
const content = match[1] ?? "";
|
|
2351
|
+
rendered += isSafeInlineCode(content) ? `\`${content}\`` : renderReceiptPlainText(content);
|
|
2352
|
+
cursor = matchIndex + match[0].length;
|
|
2353
|
+
}
|
|
2354
|
+
return `${rendered}${renderReceiptPlainText(withoutLinks.slice(cursor))}`.replace(/\s+/gu, " ").trim();
|
|
2355
|
+
}
|
|
2356
|
+
function truncateInlineCodeToken(token, maxWidth) {
|
|
2357
|
+
const content = token.slice(1, -1);
|
|
2358
|
+
const ellipsisWidth = receiptDisplayWidth("\u2026");
|
|
2359
|
+
const contentWidth = maxWidth - 2 - ellipsisWidth;
|
|
2360
|
+
if (contentWidth < 1) return "";
|
|
2361
|
+
return `\`${takeGraphemesToWidth(content, contentWidth)}\u2026\``;
|
|
2362
|
+
}
|
|
2363
|
+
function splitInlineCodeToken(token) {
|
|
2364
|
+
const match = /^(`[^`]+`)([.,;:!?)}\]]*)$/u.exec(token);
|
|
2365
|
+
if (!match) return null;
|
|
2366
|
+
return {
|
|
2367
|
+
inlineCode: match[1] ?? "",
|
|
2368
|
+
trailingPunctuation: match[2] ?? ""
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function wrapReceiptDetail(label, detail) {
|
|
2372
|
+
const firstPrefix = `\u2502 ${label}: `;
|
|
2373
|
+
const continuationPrefix = "\u2502 ";
|
|
2374
|
+
const lines = [];
|
|
2375
|
+
let remaining = detail;
|
|
2376
|
+
let prefix = firstPrefix;
|
|
2377
|
+
while (remaining) {
|
|
2378
|
+
const available = RECEIPT_LINE_WIDTH - receiptDisplayWidth(prefix);
|
|
2379
|
+
if (receiptDisplayWidth(remaining) <= available) {
|
|
2380
|
+
lines.push(`${prefix}${remaining}`);
|
|
2381
|
+
break;
|
|
2382
|
+
}
|
|
2383
|
+
const tokenEnd = remaining.search(/\s/u);
|
|
2384
|
+
const token = tokenEnd === -1 ? remaining : remaining.slice(0, tokenEnd);
|
|
2385
|
+
const inlineCodeToken = splitInlineCodeToken(token);
|
|
2386
|
+
const inlineCodeWidth = inlineCodeToken ? receiptDisplayWidth(inlineCodeToken.inlineCode) + receiptDisplayWidth(inlineCodeToken.trailingPunctuation) : 0;
|
|
2387
|
+
if (inlineCodeToken && inlineCodeWidth > available) {
|
|
2388
|
+
const codeWidth = available - receiptDisplayWidth(inlineCodeToken.trailingPunctuation);
|
|
2389
|
+
if (codeWidth >= 3) {
|
|
2390
|
+
lines.push(
|
|
2391
|
+
`${prefix}${truncateInlineCodeToken(
|
|
2392
|
+
inlineCodeToken.inlineCode,
|
|
2393
|
+
codeWidth
|
|
2394
|
+
)}${inlineCodeToken.trailingPunctuation}`
|
|
2395
|
+
);
|
|
2396
|
+
remaining = tokenEnd === -1 ? "" : remaining.slice(tokenEnd).trimStart();
|
|
2397
|
+
} else {
|
|
2398
|
+
lines.push(
|
|
2399
|
+
`${prefix}${truncateInlineCodeToken(
|
|
2400
|
+
inlineCodeToken.inlineCode,
|
|
2401
|
+
available
|
|
2402
|
+
)}`
|
|
2403
|
+
);
|
|
2404
|
+
remaining = `${inlineCodeToken.trailingPunctuation}${tokenEnd === -1 ? "" : ` ${remaining.slice(tokenEnd).trimStart()}`}`;
|
|
2405
|
+
}
|
|
2406
|
+
} else {
|
|
2407
|
+
const { head, tail } = splitAtDisplayWidth(remaining, available);
|
|
2408
|
+
if (!head) return [];
|
|
2409
|
+
lines.push(`${prefix}${head}`);
|
|
2410
|
+
remaining = tail;
|
|
2411
|
+
}
|
|
2412
|
+
prefix = continuationPrefix;
|
|
2413
|
+
}
|
|
2414
|
+
return lines;
|
|
2415
|
+
}
|
|
2416
|
+
function formatReceiptTime(occurredAtMs, nowMs) {
|
|
2417
|
+
const elapsedMs = nowMs - occurredAtMs;
|
|
2418
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
2419
|
+
if (elapsedMs >= 0 && elapsedMs < 30 * dayMs) {
|
|
2420
|
+
const days = Math.floor(elapsedMs / dayMs);
|
|
2421
|
+
if (days === 0) return "today";
|
|
2422
|
+
if (days === 1) return "yesterday";
|
|
2423
|
+
if (days < 7) return `${days} days ago`;
|
|
2424
|
+
const weeks = Math.floor(days / 7);
|
|
2425
|
+
return `${weeks} week${weeks === 1 ? "" : "s"} ago`;
|
|
2426
|
+
}
|
|
2427
|
+
try {
|
|
2428
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
2429
|
+
month: "short",
|
|
2430
|
+
day: "numeric",
|
|
2431
|
+
year: "numeric"
|
|
2432
|
+
}).format(new Date(occurredAtMs));
|
|
2433
|
+
} catch {
|
|
2434
|
+
return "";
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
function renderHookContextReceipt(receiptItems, nowMs) {
|
|
2438
|
+
try {
|
|
2439
|
+
const displayedDetails = [];
|
|
2440
|
+
for (const { item } of receiptItems.map((item2, index) => ({ item: item2, index })).sort(
|
|
2441
|
+
(left, right) => right.item.priority - left.item.priority || left.index - right.index
|
|
2442
|
+
)) {
|
|
2443
|
+
const summary = renderReceiptSummary(item.summary);
|
|
2444
|
+
if (!summary) continue;
|
|
2445
|
+
const time = item.occurredAtMs === void 0 ? "" : formatReceiptTime(item.occurredAtMs, nowMs);
|
|
2446
|
+
const detailLines = wrapReceiptDetail(
|
|
2447
|
+
item.label,
|
|
2448
|
+
`${summary}${time ? ` (${time})` : ""}`
|
|
2449
|
+
);
|
|
2450
|
+
if (detailLines.length === 0) continue;
|
|
2451
|
+
displayedDetails.push(detailLines);
|
|
2452
|
+
if (displayedDetails.length === MAX_RENDERED_RECEIPT_ITEMS) break;
|
|
2453
|
+
}
|
|
2454
|
+
const displayedItemCount = displayedDetails.length;
|
|
2455
|
+
const remainingItemCount = receiptItems.length - displayedItemCount;
|
|
2456
|
+
return receiptItems.length > 0 ? [
|
|
2457
|
+
"",
|
|
2458
|
+
"\u256D\u2500 FML provided context:",
|
|
2459
|
+
...displayedDetails.flat(),
|
|
2460
|
+
...remainingItemCount > 0 ? [
|
|
2461
|
+
`\u2502 +${remainingItemCount} more context item${remainingItemCount === 1 ? "" : "s"}`
|
|
2462
|
+
] : [],
|
|
2463
|
+
"\u2570\u2500"
|
|
2464
|
+
].join("\n") : null;
|
|
2465
|
+
} catch {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
function appendModelContextGuidance(additionalContext) {
|
|
2470
|
+
return `${additionalContext}${CONTEXT_GUIDANCE_SEPARATOR}${MODEL_CONTEXT_GUIDANCE}`;
|
|
2471
|
+
}
|
|
2472
|
+
async function requestServerHookContextInjection(request, destination, opts = {}) {
|
|
2473
|
+
const deadlineSignal = AbortSignal.timeout(
|
|
2474
|
+
opts.timeoutMs ?? HOOK_CONTEXT_TIMEOUT_MS
|
|
2475
|
+
);
|
|
2476
|
+
try {
|
|
2477
|
+
const token = await getValidToken({
|
|
2478
|
+
env: destination.environment,
|
|
2479
|
+
signal: deadlineSignal
|
|
2480
|
+
});
|
|
2481
|
+
if (!token) return null;
|
|
2482
|
+
const currentDestination = getActiveHookDestinationBinding();
|
|
2483
|
+
if (!currentDestination || !sameDestination(destination, currentDestination)) {
|
|
2484
|
+
return null;
|
|
2485
|
+
}
|
|
2486
|
+
const response = await (opts.fetch ?? fetch)(
|
|
2487
|
+
`${destination.targetUrl.replace(/\/+$/, "")}${HOOK_CONTEXT_ENDPOINT}`,
|
|
2488
|
+
{
|
|
2489
|
+
method: "POST",
|
|
2490
|
+
headers: {
|
|
2491
|
+
Authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`,
|
|
2492
|
+
"Content-Type": "application/json"
|
|
2493
|
+
},
|
|
2494
|
+
body: JSON.stringify(request),
|
|
2495
|
+
signal: deadlineSignal
|
|
2496
|
+
}
|
|
2497
|
+
);
|
|
2498
|
+
if (!response.ok) return null;
|
|
2499
|
+
const body = await response.json();
|
|
2500
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
2501
|
+
const responseBody = body;
|
|
2502
|
+
const additionalContext = responseBody.additionalContext;
|
|
2503
|
+
if (additionalContext === null) {
|
|
2504
|
+
return { additionalContext: null, receipt: null };
|
|
2505
|
+
}
|
|
2506
|
+
if (typeof additionalContext !== "string" || additionalContext.length === 0 || additionalContext.length > MAX_ADDITIONAL_CONTEXT_CHARS) {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
const receiptItems = receiptItemsFromResponse(responseBody.receiptItems);
|
|
2510
|
+
return {
|
|
2511
|
+
additionalContext: appendModelContextGuidance(additionalContext),
|
|
2512
|
+
// A malformed receipt is presentation-only and must not suppress model
|
|
2513
|
+
// context. The renderer also fails open independently.
|
|
2514
|
+
receipt: receiptItems === null ? null : renderHookContextReceipt(receiptItems, request.nowMs)
|
|
2515
|
+
};
|
|
2516
|
+
} catch {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/pi/extension.ts
|
|
2522
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 500;
|
|
2523
|
+
var DEFAULT_SHUTDOWN_FLUSH_TIMEOUT_MS = 5e3;
|
|
2524
|
+
var PI_CONTEXT_RECEIPT_ENTRY_TYPE = "fml-context-receipt";
|
|
2525
|
+
var PI_CONTEXT_STATE_ENTRY_TYPE = "fml-context-state";
|
|
2526
|
+
var pendingPosts = /* @__PURE__ */ new Set();
|
|
2527
|
+
function positiveInteger(value, fallback) {
|
|
2528
|
+
if (!value) return fallback;
|
|
2529
|
+
const parsed = Number.parseInt(value, 10);
|
|
2530
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
2531
|
+
}
|
|
2532
|
+
function absoluteEnvPath(value) {
|
|
2533
|
+
if (!value?.trim()) return void 0;
|
|
2534
|
+
const trimmed = value.trim();
|
|
2535
|
+
if (trimmed === "~") return os2.homedir();
|
|
2536
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
2537
|
+
return path7.join(os2.homedir(), trimmed.slice(2));
|
|
2538
|
+
}
|
|
2539
|
+
return path7.isAbsolute(trimmed) ? path7.normalize(trimmed) : void 0;
|
|
2540
|
+
}
|
|
2541
|
+
function observerDirectory() {
|
|
2542
|
+
const fmlHome = absoluteEnvPath(process.env.FML_HOME);
|
|
2543
|
+
return absoluteEnvPath(process.env.FML_OBSERVER_DIR) ?? absoluteEnvPath(process.env.PANOPTICON_HOME) ?? (fmlHome ? path7.join(fmlHome, "panopticon") : path7.join(os2.homedir(), ".fml", "panopticon"));
|
|
2544
|
+
}
|
|
2545
|
+
function readObserverEndpoint(filePath) {
|
|
2546
|
+
try {
|
|
2547
|
+
const parsed = JSON.parse(fs4.readFileSync(filePath, "utf8"));
|
|
2548
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2549
|
+
return null;
|
|
2550
|
+
}
|
|
2551
|
+
const record = parsed;
|
|
2552
|
+
if (record.host !== "127.0.0.1" && record.host !== "::1" && record.host !== "localhost" || typeof record.port !== "number" || !Number.isInteger(record.port) || record.port <= 0 || record.port > 65535) {
|
|
2553
|
+
return null;
|
|
2554
|
+
}
|
|
2555
|
+
return { host: record.host, port: record.port };
|
|
2556
|
+
} catch {
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function readObserverAuthToken(filePath) {
|
|
2561
|
+
try {
|
|
2562
|
+
return fs4.readFileSync(filePath, "utf8").trim() || null;
|
|
2563
|
+
} catch {
|
|
2564
|
+
return null;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
async function deliver(event) {
|
|
2568
|
+
const directory = observerDirectory();
|
|
2569
|
+
const endpoint = readObserverEndpoint(path7.join(directory, "endpoint.json"));
|
|
2570
|
+
const authToken = readObserverAuthToken(path7.join(directory, "auth-token"));
|
|
2571
|
+
if (!endpoint || !authToken) return;
|
|
2572
|
+
const host = endpoint.host === "::1" ? "[::1]" : endpoint.host;
|
|
2573
|
+
try {
|
|
2574
|
+
const response = await fetch(`http://${host}:${endpoint.port}/hooks`, {
|
|
2575
|
+
method: "POST",
|
|
2576
|
+
headers: {
|
|
2577
|
+
Authorization: `Bearer ${authToken}`,
|
|
2578
|
+
"Content-Type": "application/json"
|
|
2579
|
+
},
|
|
2580
|
+
body: JSON.stringify({
|
|
2581
|
+
target: "pi",
|
|
2582
|
+
payload: event,
|
|
2583
|
+
...event.cwd ? { cwd: event.cwd } : {}
|
|
2584
|
+
}),
|
|
2585
|
+
signal: AbortSignal.timeout(
|
|
2586
|
+
positiveInteger(
|
|
2587
|
+
process.env.FML_PI_REQUEST_TIMEOUT_MS,
|
|
2588
|
+
DEFAULT_REQUEST_TIMEOUT_MS
|
|
2589
|
+
)
|
|
2590
|
+
)
|
|
2591
|
+
});
|
|
2592
|
+
await response.arrayBuffer();
|
|
2593
|
+
} catch {
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function post(event) {
|
|
2597
|
+
const request = deliver(event);
|
|
2598
|
+
pendingPosts.add(request);
|
|
2599
|
+
void request.finally(() => pendingPosts.delete(request));
|
|
2600
|
+
}
|
|
2601
|
+
async function flushPendingPosts(timeoutMs = positiveInteger(
|
|
2602
|
+
process.env.FML_PI_SHUTDOWN_FLUSH_TIMEOUT_MS,
|
|
2603
|
+
DEFAULT_SHUTDOWN_FLUSH_TIMEOUT_MS
|
|
2604
|
+
)) {
|
|
2605
|
+
if (pendingPosts.size === 0) return;
|
|
2606
|
+
await Promise.race([
|
|
2607
|
+
Promise.allSettled([...pendingPosts]),
|
|
2608
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
2609
|
+
]);
|
|
2610
|
+
}
|
|
2611
|
+
function modelLabel(model) {
|
|
2612
|
+
if (!model || typeof model !== "object") return void 0;
|
|
2613
|
+
const record = model;
|
|
2614
|
+
const id = record.id ?? record.name ?? record.model;
|
|
2615
|
+
const provider = record.provider ?? record.providerId;
|
|
2616
|
+
return [provider, id].filter((value) => typeof value === "string" && value.length > 0).join(":") || void 0;
|
|
2617
|
+
}
|
|
2618
|
+
function isPiContextReceiptEntry(value) {
|
|
2619
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2620
|
+
const entry = value;
|
|
2621
|
+
return typeof entry.sessionId === "string" && typeof entry.injectionId === "string" && typeof entry.receipt === "string";
|
|
2622
|
+
}
|
|
2623
|
+
function isPiContextStateEntry(value) {
|
|
2624
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2625
|
+
const entry = value;
|
|
2626
|
+
if (typeof entry.sessionId !== "string") return false;
|
|
2627
|
+
if (entry.kind === "session-start") return true;
|
|
2628
|
+
if (entry.kind === "prompt") {
|
|
2629
|
+
return typeof entry.count === "number" && Number.isSafeInteger(entry.count);
|
|
2630
|
+
}
|
|
2631
|
+
if (entry.kind === "file") return typeof entry.filePath === "string";
|
|
2632
|
+
return entry.kind === "file-pending" && typeof entry.filePath === "string" && (entry.fileOperation === "read" || entry.fileOperation === "edit") && typeof entry.injectionId === "string" && typeof entry.nowMs === "number" && Number.isSafeInteger(entry.nowMs);
|
|
2633
|
+
}
|
|
2634
|
+
var RECEIPT_GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, {
|
|
2635
|
+
granularity: "grapheme"
|
|
2636
|
+
});
|
|
2637
|
+
function wrapReceiptLine(text, width) {
|
|
2638
|
+
if (width < 1) return [""];
|
|
2639
|
+
let remaining = text.trim();
|
|
2640
|
+
if (!remaining) return [""];
|
|
2641
|
+
const lines = [];
|
|
2642
|
+
while (remaining) {
|
|
2643
|
+
let line = "";
|
|
2644
|
+
let lineWidth = 0;
|
|
2645
|
+
let end = 0;
|
|
2646
|
+
let whitespaceEnd = 0;
|
|
2647
|
+
for (const { segment, index } of RECEIPT_GRAPHEME_SEGMENTER.segment(
|
|
2648
|
+
remaining
|
|
2649
|
+
)) {
|
|
2650
|
+
const segmentWidth = receiptDisplayWidth(segment);
|
|
2651
|
+
if (segmentWidth > width) {
|
|
2652
|
+
if (line) break;
|
|
2653
|
+
line = "?";
|
|
2654
|
+
end = index + segment.length;
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
if (lineWidth + segmentWidth > width) break;
|
|
2658
|
+
line += segment;
|
|
2659
|
+
lineWidth += segmentWidth;
|
|
2660
|
+
end = index + segment.length;
|
|
2661
|
+
if (/^\s$/u.test(segment)) whitespaceEnd = end;
|
|
2662
|
+
}
|
|
2663
|
+
if (end === remaining.length) {
|
|
2664
|
+
lines.push(line.trimEnd());
|
|
2665
|
+
break;
|
|
2666
|
+
}
|
|
2667
|
+
if (whitespaceEnd > 0) {
|
|
2668
|
+
lines.push(remaining.slice(0, whitespaceEnd).trimEnd());
|
|
2669
|
+
remaining = remaining.slice(whitespaceEnd).trimStart();
|
|
2670
|
+
} else {
|
|
2671
|
+
lines.push(line);
|
|
2672
|
+
remaining = remaining.slice(end);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return lines;
|
|
2676
|
+
}
|
|
2677
|
+
var PiReceiptComponent = class {
|
|
2678
|
+
constructor(receipt) {
|
|
2679
|
+
this.receipt = receipt;
|
|
2680
|
+
}
|
|
2681
|
+
invalidate() {
|
|
2682
|
+
}
|
|
2683
|
+
render(width) {
|
|
2684
|
+
if (width < 1) return [""];
|
|
2685
|
+
const lines = [];
|
|
2686
|
+
for (const sourceLine of this.receipt.replace(/^\n/u, "").split("\n")) {
|
|
2687
|
+
if (!sourceLine.startsWith("\u2502")) {
|
|
2688
|
+
lines.push(...wrapReceiptLine(sourceLine, width));
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
const sourceContent = sourceLine.slice(1);
|
|
2692
|
+
const sourceIsContinuation = sourceContent.startsWith(" ");
|
|
2693
|
+
const firstPrefix = width === 1 ? "\u2502" : sourceIsContinuation ? "\u2502 " : "\u2502 ";
|
|
2694
|
+
const continuationPrefix = width === 1 ? "\u2502" : "\u2502 ";
|
|
2695
|
+
const content = sourceContent.trimStart();
|
|
2696
|
+
const contentWidth = width - receiptDisplayWidth(continuationPrefix);
|
|
2697
|
+
if (contentWidth < 1) {
|
|
2698
|
+
lines.push(firstPrefix.slice(0, width));
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
const wrappedLines = wrapReceiptLine(content, contentWidth);
|
|
2702
|
+
wrappedLines.forEach((wrapped, index) => {
|
|
2703
|
+
lines.push(
|
|
2704
|
+
`${index === 0 ? firstPrefix : continuationPrefix}${wrapped}`
|
|
2705
|
+
);
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
return lines;
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
function fmlPiExtension(pi) {
|
|
2712
|
+
let sessionId = randomUUID3();
|
|
2713
|
+
let initializedSessionId = null;
|
|
2714
|
+
let cwd;
|
|
2715
|
+
let repository = null;
|
|
2716
|
+
let promptCount = 0;
|
|
2717
|
+
let sessionStartInjected = false;
|
|
2718
|
+
let interactive = false;
|
|
2719
|
+
const emittedFilePaths = /* @__PURE__ */ new Set();
|
|
2720
|
+
const pendingFilePaths = /* @__PURE__ */ new Set();
|
|
2721
|
+
const receiptInjectionIds = /* @__PURE__ */ new Set();
|
|
2722
|
+
const requestTimes = /* @__PURE__ */ new Map();
|
|
2723
|
+
const recordedPendingFileIds = /* @__PURE__ */ new Set();
|
|
2724
|
+
const sessionStartsInFlight = /* @__PURE__ */ new Set();
|
|
2725
|
+
pi.registerEntryRenderer(
|
|
2726
|
+
PI_CONTEXT_RECEIPT_ENTRY_TYPE,
|
|
2727
|
+
(entry) => new PiReceiptComponent(
|
|
2728
|
+
isPiContextReceiptEntry(entry.data) ? entry.data.receipt : ""
|
|
2729
|
+
)
|
|
2730
|
+
);
|
|
2731
|
+
function appendState(entry) {
|
|
2732
|
+
try {
|
|
2733
|
+
pi.appendEntry(PI_CONTEXT_STATE_ENTRY_TYPE, entry);
|
|
2734
|
+
} catch {
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
function showReceipt(injectionId, injection) {
|
|
2738
|
+
if (!interactive || !injection.receipt || receiptInjectionIds.has(injectionId)) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
try {
|
|
2742
|
+
pi.appendEntry(PI_CONTEXT_RECEIPT_ENTRY_TYPE, {
|
|
2743
|
+
sessionId,
|
|
2744
|
+
injectionId,
|
|
2745
|
+
receipt: injection.receipt
|
|
2746
|
+
});
|
|
2747
|
+
receiptInjectionIds.add(injectionId);
|
|
2748
|
+
} catch {
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
async function injectContext(event, injectionId, deliverContext) {
|
|
2752
|
+
try {
|
|
2753
|
+
if (!repository) return "unavailable";
|
|
2754
|
+
const destination = getActiveHookDestinationBinding();
|
|
2755
|
+
if (!destination) return "unavailable";
|
|
2756
|
+
const nowMs = requestTimes.get(injectionId) ?? Date.now();
|
|
2757
|
+
requestTimes.set(injectionId, nowMs);
|
|
2758
|
+
const injection = await requestServerHookContextInjection(
|
|
2759
|
+
{
|
|
2760
|
+
...event,
|
|
2761
|
+
repository,
|
|
2762
|
+
sessionId,
|
|
2763
|
+
target: "pi",
|
|
2764
|
+
injectionId,
|
|
2765
|
+
nowMs
|
|
2766
|
+
},
|
|
2767
|
+
destination
|
|
2768
|
+
);
|
|
2769
|
+
if (injection === null) return "unavailable";
|
|
2770
|
+
if (injection.additionalContext === null) {
|
|
2771
|
+
requestTimes.delete(injectionId);
|
|
2772
|
+
return "empty";
|
|
2773
|
+
}
|
|
2774
|
+
await deliverContext(injection.additionalContext);
|
|
2775
|
+
requestTimes.delete(injectionId);
|
|
2776
|
+
showReceipt(injectionId, injection);
|
|
2777
|
+
return "injected";
|
|
2778
|
+
} catch {
|
|
2779
|
+
return "unavailable";
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function resetAndRestore(entries) {
|
|
2783
|
+
repository = null;
|
|
2784
|
+
promptCount = 0;
|
|
2785
|
+
sessionStartInjected = false;
|
|
2786
|
+
emittedFilePaths.clear();
|
|
2787
|
+
pendingFilePaths.clear();
|
|
2788
|
+
receiptInjectionIds.clear();
|
|
2789
|
+
requestTimes.clear();
|
|
2790
|
+
recordedPendingFileIds.clear();
|
|
2791
|
+
for (const entry of entries) {
|
|
2792
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2793
|
+
const record = entry;
|
|
2794
|
+
if (record.type !== "custom") continue;
|
|
2795
|
+
if (record.customType === PI_CONTEXT_RECEIPT_ENTRY_TYPE) {
|
|
2796
|
+
if (isPiContextReceiptEntry(record.data) && record.data.sessionId === sessionId) {
|
|
2797
|
+
receiptInjectionIds.add(record.data.injectionId);
|
|
2798
|
+
}
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
if (record.customType !== PI_CONTEXT_STATE_ENTRY_TYPE || !isPiContextStateEntry(record.data) || record.data.sessionId !== sessionId) {
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
const state = record.data;
|
|
2805
|
+
if (state.kind === "session-start") sessionStartInjected = true;
|
|
2806
|
+
if (state.kind === "prompt")
|
|
2807
|
+
promptCount = Math.max(promptCount, state.count);
|
|
2808
|
+
if (state.kind === "file") emittedFilePaths.add(state.filePath);
|
|
2809
|
+
if (state.kind === "file-pending") {
|
|
2810
|
+
requestTimes.set(state.injectionId, state.nowMs);
|
|
2811
|
+
recordedPendingFileIds.add(state.injectionId);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
function emit(event) {
|
|
2816
|
+
post({ ...event, session_id: sessionId, source: "pi", cwd });
|
|
2817
|
+
}
|
|
2818
|
+
pi.on("session_start", async (event, context) => {
|
|
2819
|
+
const nextSessionId = context.sessionManager.getSessionId();
|
|
2820
|
+
sessionId = nextSessionId;
|
|
2821
|
+
cwd = context.cwd;
|
|
2822
|
+
interactive = context.mode === "tui";
|
|
2823
|
+
if (initializedSessionId !== sessionId) {
|
|
2824
|
+
initializedSessionId = sessionId;
|
|
2825
|
+
resetAndRestore(context.sessionManager.getEntries());
|
|
2826
|
+
}
|
|
2827
|
+
emit({ hook_event_name: "SessionStart" });
|
|
2828
|
+
try {
|
|
2829
|
+
repository = cwd ? resolveRepositorySlugFromCwd(cwd, 250) : null;
|
|
2830
|
+
if (!repository) return;
|
|
2831
|
+
} catch {
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
if (event.reason === "reload" || event.reason === "resume") return;
|
|
2835
|
+
if (sessionStartInjected || sessionStartsInFlight.has(sessionId)) return;
|
|
2836
|
+
sessionStartsInFlight.add(sessionId);
|
|
2837
|
+
try {
|
|
2838
|
+
const injectionId = hookContextInjectionId("pi", `${sessionId}:start`);
|
|
2839
|
+
const outcome = await injectContext(
|
|
2840
|
+
{ eventType: "SessionStart" },
|
|
2841
|
+
injectionId,
|
|
2842
|
+
async (additionalContext) => {
|
|
2843
|
+
pi.sendMessage({
|
|
2844
|
+
customType: "fml-context",
|
|
2845
|
+
content: additionalContext,
|
|
2846
|
+
display: false
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
);
|
|
2850
|
+
if (outcome === "injected") {
|
|
2851
|
+
sessionStartInjected = true;
|
|
2852
|
+
appendState({ sessionId, kind: "session-start" });
|
|
2853
|
+
}
|
|
2854
|
+
} catch {
|
|
2855
|
+
} finally {
|
|
2856
|
+
sessionStartsInFlight.delete(nextSessionId);
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
pi.on("input", async (event) => {
|
|
2860
|
+
emit({ hook_event_name: "UserPromptSubmit", prompt: event.text });
|
|
2861
|
+
});
|
|
2862
|
+
pi.on("before_agent_start", async (event) => {
|
|
2863
|
+
const promptIndex = promptCount;
|
|
2864
|
+
promptCount += 1;
|
|
2865
|
+
appendState({ sessionId, kind: "prompt", count: promptCount });
|
|
2866
|
+
if (promptIndex === 0) return;
|
|
2867
|
+
await injectContext(
|
|
2868
|
+
{
|
|
2869
|
+
eventType: "UserPromptSubmit",
|
|
2870
|
+
prompt: event.prompt.slice(0, 2e4)
|
|
2871
|
+
},
|
|
2872
|
+
hookContextInjectionId("pi", `${sessionId}:prompt:${promptIndex}`),
|
|
2873
|
+
async (additionalContext) => {
|
|
2874
|
+
pi.sendMessage({
|
|
2875
|
+
customType: "fml-context",
|
|
2876
|
+
content: additionalContext,
|
|
2877
|
+
display: false
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
);
|
|
2881
|
+
});
|
|
2882
|
+
pi.on("turn_start", async (event) => {
|
|
2883
|
+
emit({
|
|
2884
|
+
hook_event_name: "TurnStart",
|
|
2885
|
+
turn_index: event.turnIndex,
|
|
2886
|
+
pi_timestamp: event.timestamp
|
|
271
2887
|
});
|
|
272
2888
|
});
|
|
273
2889
|
pi.on("turn_end", async (event) => {
|
|
@@ -284,6 +2900,66 @@ function fmlPiExtension(pi) {
|
|
|
284
2900
|
tool_call_id: event.toolCallId,
|
|
285
2901
|
tool_input: event.input
|
|
286
2902
|
});
|
|
2903
|
+
try {
|
|
2904
|
+
const trigger = extractHookContextFileTrigger(
|
|
2905
|
+
event.toolName,
|
|
2906
|
+
event.input
|
|
2907
|
+
);
|
|
2908
|
+
if (!trigger || !cwd || !repository) return;
|
|
2909
|
+
const root = resolveRepositoryRootFromCwd(cwd, 250);
|
|
2910
|
+
const filePath = canonicalHookContextFilePath(
|
|
2911
|
+
trigger.filePath,
|
|
2912
|
+
cwd,
|
|
2913
|
+
root
|
|
2914
|
+
);
|
|
2915
|
+
if (emittedFilePaths.has(filePath) || pendingFilePaths.has(filePath))
|
|
2916
|
+
return;
|
|
2917
|
+
pendingFilePaths.add(filePath);
|
|
2918
|
+
try {
|
|
2919
|
+
const injectionId = hookContextInjectionId(
|
|
2920
|
+
"pi",
|
|
2921
|
+
`${sessionId}:file:${trigger.fileOperation}:${filePath}`
|
|
2922
|
+
);
|
|
2923
|
+
const nowMs = requestTimes.get(injectionId) ?? Date.now();
|
|
2924
|
+
requestTimes.set(injectionId, nowMs);
|
|
2925
|
+
if (!recordedPendingFileIds.has(injectionId)) {
|
|
2926
|
+
appendState({
|
|
2927
|
+
sessionId,
|
|
2928
|
+
kind: "file-pending",
|
|
2929
|
+
filePath,
|
|
2930
|
+
fileOperation: trigger.fileOperation,
|
|
2931
|
+
injectionId,
|
|
2932
|
+
nowMs
|
|
2933
|
+
});
|
|
2934
|
+
recordedPendingFileIds.add(injectionId);
|
|
2935
|
+
}
|
|
2936
|
+
const outcome = await injectContext(
|
|
2937
|
+
{
|
|
2938
|
+
eventType: "PreToolUse",
|
|
2939
|
+
filePath,
|
|
2940
|
+
fileOperation: trigger.fileOperation
|
|
2941
|
+
},
|
|
2942
|
+
injectionId,
|
|
2943
|
+
async (additionalContext) => {
|
|
2944
|
+
pi.sendMessage(
|
|
2945
|
+
{
|
|
2946
|
+
customType: "fml-context",
|
|
2947
|
+
content: additionalContext,
|
|
2948
|
+
display: false
|
|
2949
|
+
},
|
|
2950
|
+
{ deliverAs: "steer" }
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
);
|
|
2954
|
+
if (outcome === "empty" || outcome === "injected") {
|
|
2955
|
+
emittedFilePaths.add(filePath);
|
|
2956
|
+
appendState({ sessionId, kind: "file", filePath });
|
|
2957
|
+
}
|
|
2958
|
+
} finally {
|
|
2959
|
+
pendingFilePaths.delete(filePath);
|
|
2960
|
+
}
|
|
2961
|
+
} catch {
|
|
2962
|
+
}
|
|
287
2963
|
});
|
|
288
2964
|
pi.on("tool_result", async (event) => {
|
|
289
2965
|
emit({
|