@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.2
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/README.md +27 -16
- package/dist/{chunk-CK5MEL5J.js → chunk-3KISRT57.js} +1 -1
- package/dist/{chunk-DEMVOVVY.js → chunk-56TPYMNK.js} +244 -29
- package/dist/{chunk-T5YVLSUC.js → chunk-7V7G7NVF.js} +4 -0
- package/dist/{chunk-7DR5X7XI.js → chunk-A6HAEVJX.js} +136 -52
- package/dist/{chunk-F66IBM3Z.js → chunk-DZKYW327.js} +2 -2
- package/dist/{chunk-I4GZPTBM.js → chunk-ENW2GJNO.js} +1 -1
- package/dist/{chunk-P3QLARXE.js → chunk-R45CWWZO.js} +3 -3
- package/dist/{chunk-HUR2HCUG.js → chunk-WZLLO563.js} +2 -2
- package/dist/{chunk-JXSSPQ5B.js → chunk-YSU5CS2V.js} +1 -1
- package/dist/cli.js +1310 -1158
- package/dist/hook-handler.js +290 -40
- package/dist/mcp/server.js +7 -7
- package/dist/observer-server.js +8 -8
- package/dist/sync-cycle.js +3 -3
- package/dist/{token-store-R7WXEPLI.js → token-store-BT5THIAJ.js} +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,25 +10,33 @@ not install or run a second Panopticon package.
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
Node.js 22.15 or newer and
|
|
13
|
+
Node.js 22.15 or newer and npm are required.
|
|
14
|
+
|
|
15
|
+
On macOS, Linux, and compatible Bash environments, install with:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
17
|
-
fml install
|
|
18
|
-
fml login
|
|
19
|
-
fml org
|
|
20
|
-
fml start
|
|
18
|
+
curl -fsSL https://fml.inc/install.sh | bash
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
For Windows or manual installation, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --global @fml-inc/fml
|
|
25
|
+
fml setup
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `fml update` to upgrade the global package with npm. Stable releases stay
|
|
29
|
+
on npm's `latest` channel; prerelease versions stay on `next`.
|
|
30
|
+
|
|
31
|
+
Installing the package does not change agent configuration. Run `fml setup`
|
|
32
|
+
to register every supported FML agent integration, sign in when needed, and
|
|
33
|
+
restart the observer. It installs the agent-native FML skill, configures the
|
|
34
|
+
FML MCP server, and registers capture-only lifecycle hooks for supported
|
|
35
|
+
agents. Invoke the skill as `/fml review` in Claude, Gemini, or Grok, and as
|
|
36
|
+
`$fml review` in Codex. Review runs inside the agent from the packaged skill;
|
|
37
|
+
it is not an `fml review` CLI command. Start a new agent session after setup so
|
|
38
|
+
it picks up the integration. FML pre-approves only the isolated command hooks
|
|
39
|
+
it installs for Codex; use `/hooks` to review or disable them.
|
|
32
40
|
|
|
33
41
|
The Claude plugin is MCP-only. FML installs the workflow through each host's
|
|
34
42
|
native skill directory so it keeps the bare `fml` name without a second,
|
|
@@ -84,6 +92,7 @@ fml sync disable # Stop discovery and upload
|
|
|
84
92
|
fml sync enable # Allow discovery and upload again
|
|
85
93
|
fml sync reset # Clear watermarks and replay every local artifact from byte zero
|
|
86
94
|
fml start # Start the observer and continuously run enabled raw sync
|
|
95
|
+
fml restart # Stop and start the observer
|
|
87
96
|
fml stop # Stop the observer and disable raw sync
|
|
88
97
|
```
|
|
89
98
|
|
|
@@ -262,11 +271,13 @@ fml tools call <tool-name> --args '{"key":"value"}'
|
|
|
262
271
|
## CLI overview
|
|
263
272
|
|
|
264
273
|
```text
|
|
274
|
+
fml setup Install integrations, sign in if needed, and start FML
|
|
265
275
|
fml install Register FML agent integrations and the MCP server
|
|
266
276
|
fml uninstall Remove agent integration; add --purge to remove local state
|
|
267
|
-
fml update Update @fml-inc/fml with
|
|
277
|
+
fml update Update @fml-inc/fml with npm (stable: latest; prerelease: next)
|
|
268
278
|
|
|
269
279
|
fml start Start the local observer and continuous raw sync
|
|
280
|
+
fml restart Stop and start the local observer
|
|
270
281
|
fml stop Stop the local observer and disable raw sync
|
|
271
282
|
|
|
272
283
|
fml login Sign in to FML
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FML_HOOK_INBOX_DIR
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-A6HAEVJX.js";
|
|
4
4
|
|
|
5
5
|
// ../../packages/panopticon/src/observer/auth.ts
|
|
6
6
|
import crypto from "crypto";
|
|
@@ -154,6 +154,220 @@ var GROK_HOOK_EVENTS = Object.freeze([
|
|
|
154
154
|
...hook_events_default.grok
|
|
155
155
|
]);
|
|
156
156
|
|
|
157
|
+
// ../../packages/agent-targets/src/hooks/read-command.ts
|
|
158
|
+
import path2 from "path";
|
|
159
|
+
var SHELL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
160
|
+
"bash",
|
|
161
|
+
"exec_command",
|
|
162
|
+
"shell"
|
|
163
|
+
]);
|
|
164
|
+
var COMMAND_SEPARATORS = /* @__PURE__ */ new Set([
|
|
165
|
+
";",
|
|
166
|
+
"&",
|
|
167
|
+
"&&",
|
|
168
|
+
"|",
|
|
169
|
+
"||",
|
|
170
|
+
"<",
|
|
171
|
+
"<<",
|
|
172
|
+
">",
|
|
173
|
+
">>",
|
|
174
|
+
">&1",
|
|
175
|
+
">&2",
|
|
176
|
+
"1>",
|
|
177
|
+
"1>>",
|
|
178
|
+
"1>&1",
|
|
179
|
+
"1>&2",
|
|
180
|
+
"2>",
|
|
181
|
+
"2>>",
|
|
182
|
+
"2>&1",
|
|
183
|
+
"2>&2"
|
|
184
|
+
]);
|
|
185
|
+
function tokenizeShellLikeCommand(command) {
|
|
186
|
+
const tokens = [];
|
|
187
|
+
let current = "";
|
|
188
|
+
let quote = null;
|
|
189
|
+
let escaping = false;
|
|
190
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
191
|
+
const character = command[index];
|
|
192
|
+
if (escaping) {
|
|
193
|
+
current += character;
|
|
194
|
+
escaping = false;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (character === "\\") {
|
|
198
|
+
escaping = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (quote) {
|
|
202
|
+
if (character === quote) quote = null;
|
|
203
|
+
else current += character;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (character === "'" || character === '"') {
|
|
207
|
+
quote = character;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (current === "" && (character === "1" || character === "2") && command[index + 1] === ">") {
|
|
211
|
+
if (command[index + 2] === ">") {
|
|
212
|
+
tokens.push(`${character}>>`);
|
|
213
|
+
index += 2;
|
|
214
|
+
} else if (command[index + 2] === "&" && (command[index + 3] === "1" || command[index + 3] === "2")) {
|
|
215
|
+
tokens.push(`${character}>&${command[index + 3]}`);
|
|
216
|
+
index += 3;
|
|
217
|
+
} else {
|
|
218
|
+
tokens.push(`${character}>`);
|
|
219
|
+
index += 1;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (/[;&|<>]/.test(character)) {
|
|
224
|
+
if (current) {
|
|
225
|
+
tokens.push(current);
|
|
226
|
+
current = "";
|
|
227
|
+
}
|
|
228
|
+
const next = command[index + 1];
|
|
229
|
+
if (character === ">" && next === "&" && (command[index + 2] === "1" || command[index + 2] === "2")) {
|
|
230
|
+
tokens.push(`>&${command[index + 2]}`);
|
|
231
|
+
index += 2;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (character === "&" && next === "&" || character === "|" && next === "|" || character === "<" && next === "<" || character === ">" && next === ">") {
|
|
235
|
+
tokens.push(`${character}${next}`);
|
|
236
|
+
index += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
tokens.push(character);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (/\s/.test(character)) {
|
|
243
|
+
if (current) {
|
|
244
|
+
tokens.push(current);
|
|
245
|
+
current = "";
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
current += character;
|
|
250
|
+
}
|
|
251
|
+
if (quote || escaping) return [];
|
|
252
|
+
if (current) tokens.push(current);
|
|
253
|
+
return tokens;
|
|
254
|
+
}
|
|
255
|
+
function splitCommandSegments(tokens) {
|
|
256
|
+
const segments = [];
|
|
257
|
+
let current = [];
|
|
258
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
259
|
+
const token = tokens[index];
|
|
260
|
+
if (COMMAND_SEPARATORS.has(token)) {
|
|
261
|
+
if (current.length > 0) {
|
|
262
|
+
segments.push(current);
|
|
263
|
+
current = [];
|
|
264
|
+
}
|
|
265
|
+
if (token === "<" || token === "<<") index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
current.push(token);
|
|
269
|
+
}
|
|
270
|
+
if (current.length > 0) segments.push(current);
|
|
271
|
+
return segments;
|
|
272
|
+
}
|
|
273
|
+
function stripSimpleOptions(args, flagsWithValues = /* @__PURE__ */ new Set(), patternFlags = /* @__PURE__ */ new Set()) {
|
|
274
|
+
const remaining = [];
|
|
275
|
+
let consumedPatternFlag = false;
|
|
276
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
277
|
+
const argument = args[index];
|
|
278
|
+
if (argument === "--") {
|
|
279
|
+
remaining.push(...args.slice(index + 1));
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
if (argument.startsWith("-") && argument.length > 1) {
|
|
283
|
+
if (flagsWithValues.has(argument)) {
|
|
284
|
+
if (patternFlags.has(argument)) consumedPatternFlag = true;
|
|
285
|
+
if (!argument.includes("=")) index += 1;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
remaining.push(argument);
|
|
290
|
+
}
|
|
291
|
+
return { args: remaining, consumedPatternFlag };
|
|
292
|
+
}
|
|
293
|
+
function readCommandFileArgs(args, flagsWithValues, patternFlags = flagsWithValues) {
|
|
294
|
+
const stripped = stripSimpleOptions(args, flagsWithValues, patternFlags);
|
|
295
|
+
return stripped.consumedPatternFlag ? stripped.args : stripped.args.slice(1);
|
|
296
|
+
}
|
|
297
|
+
function isPlausibleReadPath(argument) {
|
|
298
|
+
if (!argument || argument === "-" || argument.startsWith("-")) return false;
|
|
299
|
+
if (COMMAND_SEPARATORS.has(argument)) return false;
|
|
300
|
+
if (/[\0*?[{}`$]/.test(argument)) return false;
|
|
301
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(argument)) return false;
|
|
302
|
+
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);
|
|
303
|
+
}
|
|
304
|
+
function filePathFromSegment(tokens) {
|
|
305
|
+
const commandTokens = [...tokens];
|
|
306
|
+
while (commandTokens[0]?.includes("=") && /^[A-Za-z_][A-Za-z0-9_]*=/.test(commandTokens[0])) {
|
|
307
|
+
commandTokens.shift();
|
|
308
|
+
}
|
|
309
|
+
const commandName = commandTokens.shift()?.split("/").pop();
|
|
310
|
+
if (!commandName) return null;
|
|
311
|
+
let args;
|
|
312
|
+
switch (commandName) {
|
|
313
|
+
case "bat":
|
|
314
|
+
case "batcat":
|
|
315
|
+
case "cat":
|
|
316
|
+
case "file":
|
|
317
|
+
case "less":
|
|
318
|
+
case "more":
|
|
319
|
+
case "nl":
|
|
320
|
+
case "stat":
|
|
321
|
+
case "wc":
|
|
322
|
+
args = stripSimpleOptions(commandTokens).args;
|
|
323
|
+
break;
|
|
324
|
+
case "head":
|
|
325
|
+
case "tail":
|
|
326
|
+
args = stripSimpleOptions(commandTokens, /* @__PURE__ */ new Set(["-n", "-c"])).args;
|
|
327
|
+
break;
|
|
328
|
+
case "sed":
|
|
329
|
+
if (commandTokens.some(
|
|
330
|
+
(argument) => argument.startsWith("-i") || argument === "--in-place" || argument.startsWith("--in-place=")
|
|
331
|
+
)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
args = readCommandFileArgs(commandTokens, /* @__PURE__ */ new Set(["-e", "-f"]));
|
|
335
|
+
break;
|
|
336
|
+
case "awk":
|
|
337
|
+
args = readCommandFileArgs(
|
|
338
|
+
commandTokens,
|
|
339
|
+
/* @__PURE__ */ new Set(["-f", "-v"]),
|
|
340
|
+
/* @__PURE__ */ new Set(["-f"])
|
|
341
|
+
);
|
|
342
|
+
break;
|
|
343
|
+
case "grep":
|
|
344
|
+
case "rg":
|
|
345
|
+
args = readCommandFileArgs(
|
|
346
|
+
commandTokens,
|
|
347
|
+
/* @__PURE__ */ new Set(["-A", "-B", "-C", "-e", "-f", "-g", "-m", "-t"]),
|
|
348
|
+
/* @__PURE__ */ new Set(["-e", "-f"])
|
|
349
|
+
);
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return args.find(isPlausibleReadPath) ?? null;
|
|
355
|
+
}
|
|
356
|
+
function extractReadFilePathFromCommand(command) {
|
|
357
|
+
const tokens = tokenizeShellLikeCommand(command);
|
|
358
|
+
if (tokens.length === 0) return null;
|
|
359
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
360
|
+
if (tokens[index] === "<" && isPlausibleReadPath(tokens[index + 1])) {
|
|
361
|
+
return tokens[index + 1];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const segment of splitCommandSegments(tokens)) {
|
|
365
|
+
const filePath = filePathFromSegment(segment);
|
|
366
|
+
if (filePath) return filePath;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
157
371
|
// ../../packages/agent-targets/src/hooks/normalize.ts
|
|
158
372
|
var MAX_EXTRACTED_TEXT_BYTES = 64 * 1024;
|
|
159
373
|
function isRecord(value) {
|
|
@@ -309,12 +523,12 @@ function normalizedEvent(target, payload, context) {
|
|
|
309
523
|
"workingDirectory",
|
|
310
524
|
"workspaceRoot"
|
|
311
525
|
]) ?? context.cwd;
|
|
312
|
-
const filePath = firstString(payload, ["file_path", "filePath", "path"]) ?? (toolInput ? firstString(toolInput, ["file_path", "filePath", "path"]) : void 0);
|
|
313
526
|
const command = boundedExtractedText(
|
|
314
527
|
firstText(payload, ["command"]) ?? (toolInput ? firstText(toolInput, ["command", "cmd"]) : void 0),
|
|
315
528
|
"command"
|
|
316
529
|
);
|
|
317
530
|
const toolName = firstString(payload, ["tool_name", "toolName", "name"]);
|
|
531
|
+
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);
|
|
318
532
|
const userPrompt = boundedExtractedText(
|
|
319
533
|
firstText(payload, ["prompt", "user_prompt", "userPrompt"]) ?? (target === "gemini" ? geminiUserPrompt(payload) : void 0),
|
|
320
534
|
"user_prompt"
|
|
@@ -434,7 +648,7 @@ function normalizeObserverHookCapture(request, options = {}) {
|
|
|
434
648
|
// ../../packages/panopticon/src/observer/database.ts
|
|
435
649
|
import crypto2 from "crypto";
|
|
436
650
|
import fs2 from "fs";
|
|
437
|
-
import
|
|
651
|
+
import path3 from "path";
|
|
438
652
|
var ObserverHookSpoolEntryTooLargeError = class extends Error {
|
|
439
653
|
constructor(logicalBytes, maxBytes) {
|
|
440
654
|
super(
|
|
@@ -789,7 +1003,7 @@ function openObserverDatabase(databaseFile, options = {}) {
|
|
|
789
1003
|
if (!sqlite?.DatabaseSync) {
|
|
790
1004
|
throw new Error("FML observer requires the Node.js built-in SQLite module");
|
|
791
1005
|
}
|
|
792
|
-
const databaseDirectory =
|
|
1006
|
+
const databaseDirectory = path3.dirname(databaseFile);
|
|
793
1007
|
fs2.mkdirSync(databaseDirectory, { recursive: true, mode: 448 });
|
|
794
1008
|
if (options.restrictDatabaseDirectory !== false) {
|
|
795
1009
|
enforceOwnerOnlyMode(databaseDirectory, 448);
|
|
@@ -3068,7 +3282,7 @@ function createObserverHttpServer(opts) {
|
|
|
3068
3282
|
// src/hooks/inbox.ts
|
|
3069
3283
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3070
3284
|
import fs3 from "fs";
|
|
3071
|
-
import
|
|
3285
|
+
import path4 from "path";
|
|
3072
3286
|
|
|
3073
3287
|
// src/hooks/inbox-config.ts
|
|
3074
3288
|
var BYTE_SUFFIXES = {
|
|
@@ -3210,7 +3424,7 @@ function entryFileName(id) {
|
|
|
3210
3424
|
return `${id}.json`;
|
|
3211
3425
|
}
|
|
3212
3426
|
function entryPath(inboxDir, id) {
|
|
3213
|
-
return
|
|
3427
|
+
return path4.join(inboxDir, entryFileName(id));
|
|
3214
3428
|
}
|
|
3215
3429
|
function enforceOwnerOnlyDirectory(inboxDir) {
|
|
3216
3430
|
fs3.mkdirSync(inboxDir, { recursive: true, mode: 448 });
|
|
@@ -3320,7 +3534,7 @@ function parseCapacityState(value) {
|
|
|
3320
3534
|
};
|
|
3321
3535
|
}
|
|
3322
3536
|
function capacityStatePath(inboxDir) {
|
|
3323
|
-
return
|
|
3537
|
+
return path4.join(inboxDir, CAPACITY_STATE_FILE);
|
|
3324
3538
|
}
|
|
3325
3539
|
function readCapacityStateFile(inboxDir) {
|
|
3326
3540
|
try {
|
|
@@ -3348,7 +3562,7 @@ function writeCapacityState(inboxDir, state, capacityLock) {
|
|
|
3348
3562
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3349
3563
|
capacityLock.assertOwned();
|
|
3350
3564
|
const destination = capacityStatePath(inboxDir);
|
|
3351
|
-
const temporary =
|
|
3565
|
+
const temporary = path4.join(
|
|
3352
3566
|
inboxDir,
|
|
3353
3567
|
`${CAPACITY_STATE_FILE}.${process.pid}.${randomUUID2()}.tmp`
|
|
3354
3568
|
);
|
|
@@ -3457,7 +3671,7 @@ function recoverStaleCapacityLock(lockPath, nowMs) {
|
|
|
3457
3671
|
return;
|
|
3458
3672
|
}
|
|
3459
3673
|
fs3.rmSync(abandoned, { force: true });
|
|
3460
|
-
fsyncDirectory(
|
|
3674
|
+
fsyncDirectory(path4.dirname(lockPath));
|
|
3461
3675
|
} catch (error) {
|
|
3462
3676
|
if (!capacityLockRaceError(error)) throw error;
|
|
3463
3677
|
}
|
|
@@ -3468,7 +3682,7 @@ function sleepSync(milliseconds) {
|
|
|
3468
3682
|
}
|
|
3469
3683
|
function acquireCapacityLock(inboxDir, waitMs = CAPACITY_LOCK_WAIT_MS) {
|
|
3470
3684
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3471
|
-
const lockPath =
|
|
3685
|
+
const lockPath = path4.join(inboxDir, CAPACITY_LOCK_FILE);
|
|
3472
3686
|
const deadlineMs = Date.now() + Math.max(0, waitMs);
|
|
3473
3687
|
while (true) {
|
|
3474
3688
|
let descriptor;
|
|
@@ -3541,19 +3755,19 @@ function capacityMutationEvidence(inboxDir, pending, includeTemporary) {
|
|
|
3541
3755
|
const prefix = `.${id}.`;
|
|
3542
3756
|
const candidates = [];
|
|
3543
3757
|
if (includeTemporary && pending.temporaryFileName) {
|
|
3544
|
-
candidates.push(
|
|
3758
|
+
candidates.push(path4.join(inboxDir, pending.temporaryFileName));
|
|
3545
3759
|
} else if (includeTemporary) {
|
|
3546
3760
|
try {
|
|
3547
3761
|
for (const name of fs3.readdirSync(inboxDir)) {
|
|
3548
3762
|
if (name.startsWith(prefix) && name.endsWith(".tmp")) {
|
|
3549
|
-
candidates.push(
|
|
3763
|
+
candidates.push(path4.join(inboxDir, name));
|
|
3550
3764
|
}
|
|
3551
3765
|
}
|
|
3552
3766
|
} catch (error) {
|
|
3553
3767
|
return error.code === "ENOENT" ? { kind: "missing" } : { kind: "unavailable" };
|
|
3554
3768
|
}
|
|
3555
3769
|
}
|
|
3556
|
-
candidates.push(
|
|
3770
|
+
candidates.push(path4.join(inboxDir, fileName));
|
|
3557
3771
|
let presentBytes;
|
|
3558
3772
|
for (const candidate of candidates) {
|
|
3559
3773
|
try {
|
|
@@ -3665,7 +3879,7 @@ function removeCapacityTrackedEntry(filePath, inboxDir, expectedBytes, missingIs
|
|
|
3665
3879
|
}
|
|
3666
3880
|
if (bytes === void 0) return false;
|
|
3667
3881
|
const pendingRemoval = {
|
|
3668
|
-
fileName:
|
|
3882
|
+
fileName: path4.basename(filePath),
|
|
3669
3883
|
bytes,
|
|
3670
3884
|
startedAtMs: Date.now()
|
|
3671
3885
|
};
|
|
@@ -3746,7 +3960,7 @@ function readRetirementState(inboxDir) {
|
|
|
3746
3960
|
try {
|
|
3747
3961
|
return parseRetirementState(
|
|
3748
3962
|
JSON.parse(
|
|
3749
|
-
fs3.readFileSync(
|
|
3963
|
+
fs3.readFileSync(path4.join(inboxDir, RETIREMENT_STATE_FILE), "utf8")
|
|
3750
3964
|
)
|
|
3751
3965
|
) ?? defaultRetirementState();
|
|
3752
3966
|
} catch {
|
|
@@ -3755,8 +3969,8 @@ function readRetirementState(inboxDir) {
|
|
|
3755
3969
|
}
|
|
3756
3970
|
function writeRetirementState(inboxDir, state) {
|
|
3757
3971
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3758
|
-
const destination =
|
|
3759
|
-
const temporary =
|
|
3972
|
+
const destination = path4.join(inboxDir, RETIREMENT_STATE_FILE);
|
|
3973
|
+
const temporary = path4.join(
|
|
3760
3974
|
inboxDir,
|
|
3761
3975
|
`${RETIREMENT_STATE_FILE}.${process.pid}.${randomUUID2()}.tmp`
|
|
3762
3976
|
);
|
|
@@ -3783,7 +3997,7 @@ function completePendingRetirement(inboxDir, state) {
|
|
|
3783
3997
|
const pending = state.pending;
|
|
3784
3998
|
if (!pending) return state;
|
|
3785
3999
|
if (!removeCapacityTrackedEntry(
|
|
3786
|
-
|
|
4000
|
+
path4.join(inboxDir, pending.fileName),
|
|
3787
4001
|
inboxDir,
|
|
3788
4002
|
pending.bytes,
|
|
3789
4003
|
true
|
|
@@ -3819,7 +4033,7 @@ function retireEntry(filePath, reason, inboxDir) {
|
|
|
3819
4033
|
const pending = {
|
|
3820
4034
|
...current,
|
|
3821
4035
|
pending: {
|
|
3822
|
-
fileName:
|
|
4036
|
+
fileName: path4.basename(filePath),
|
|
3823
4037
|
bytes,
|
|
3824
4038
|
reason,
|
|
3825
4039
|
retiredAtMs: Date.now()
|
|
@@ -3870,7 +4084,7 @@ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = ho
|
|
|
3870
4084
|
throw new Error("FML hook inbox admission is busy");
|
|
3871
4085
|
}
|
|
3872
4086
|
const destination = entryPath(inboxDir, entry.id);
|
|
3873
|
-
const temporary =
|
|
4087
|
+
const temporary = path4.join(
|
|
3874
4088
|
inboxDir,
|
|
3875
4089
|
`.${entry.id}.${process.pid}.${randomUUID2()}.tmp`
|
|
3876
4090
|
);
|
|
@@ -3900,8 +4114,8 @@ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = ho
|
|
|
3900
4114
|
}
|
|
3901
4115
|
try {
|
|
3902
4116
|
const pendingPersist = {
|
|
3903
|
-
fileName:
|
|
3904
|
-
temporaryFileName:
|
|
4117
|
+
fileName: path4.basename(destination),
|
|
4118
|
+
temporaryFileName: path4.basename(temporary),
|
|
3905
4119
|
bytes: serializedBytes,
|
|
3906
4120
|
startedAtMs: Date.now()
|
|
3907
4121
|
};
|
|
@@ -4059,7 +4273,7 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4059
4273
|
const abandonedLockMatch = ABANDONED_CAPACITY_LOCK_PATTERN.exec(name);
|
|
4060
4274
|
if (INBOX_STATE_TEMP_PATTERN.test(name) || abandonedLockMatch !== null) {
|
|
4061
4275
|
try {
|
|
4062
|
-
const orphanPath =
|
|
4276
|
+
const orphanPath = path4.join(inboxDir, name);
|
|
4063
4277
|
const stat = await fs3.promises.lstat(orphanPath);
|
|
4064
4278
|
const abandonedAtMs = abandonedLockMatch ? Number(abandonedLockMatch[1]) : void 0;
|
|
4065
4279
|
const isStale = abandonedLockMatch ? validTimestamp(abandonedAtMs) && abandonedAtMs <= cutoffMs : stat.mtimeMs <= cutoffMs;
|
|
@@ -4082,8 +4296,8 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4082
4296
|
const id = match?.[1];
|
|
4083
4297
|
if (!id) continue;
|
|
4084
4298
|
const destination = entryPath(inboxDir, id);
|
|
4085
|
-
const destinationName =
|
|
4086
|
-
const temporary =
|
|
4299
|
+
const destinationName = path4.basename(destination);
|
|
4300
|
+
const temporary = path4.join(inboxDir, name);
|
|
4087
4301
|
try {
|
|
4088
4302
|
const stat = await fs3.promises.lstat(temporary);
|
|
4089
4303
|
if (!stat.isFile()) continue;
|
|
@@ -4136,7 +4350,7 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4136
4350
|
return { names, failed, resetCursor, blockCursorAdvance };
|
|
4137
4351
|
}
|
|
4138
4352
|
function drainCursor(inboxDir) {
|
|
4139
|
-
const key =
|
|
4353
|
+
const key = path4.resolve(inboxDir);
|
|
4140
4354
|
const existing = HOOK_INBOX_DRAIN_CURSORS.get(key);
|
|
4141
4355
|
if (existing) {
|
|
4142
4356
|
HOOK_INBOX_DRAIN_CURSORS.delete(key);
|
|
@@ -4173,7 +4387,7 @@ async function pendingInboxSizeAsync(inboxDir) {
|
|
|
4173
4387
|
for (let dirent = await directory.read(); dirent !== null; dirent = await directory.read()) {
|
|
4174
4388
|
if (!isPendingEntryName(dirent.name)) continue;
|
|
4175
4389
|
try {
|
|
4176
|
-
const stat = await fs3.promises.lstat(
|
|
4390
|
+
const stat = await fs3.promises.lstat(path4.join(inboxDir, dirent.name));
|
|
4177
4391
|
if (stat.isFile()) {
|
|
4178
4392
|
pendingEntries += 1;
|
|
4179
4393
|
pendingBytes += stat.size;
|
|
@@ -4197,7 +4411,7 @@ function pendingInboxSize(inboxDir, includeTemporary = false) {
|
|
|
4197
4411
|
continue;
|
|
4198
4412
|
}
|
|
4199
4413
|
try {
|
|
4200
|
-
const stat = fs3.lstatSync(
|
|
4414
|
+
const stat = fs3.lstatSync(path4.join(inboxDir, dirent.name));
|
|
4201
4415
|
if (stat.isFile()) {
|
|
4202
4416
|
pendingEntries += 1;
|
|
4203
4417
|
pendingBytes += stat.size;
|
|
@@ -4352,7 +4566,7 @@ async function drainHookInbox(capture, inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4352
4566
|
safeThrough = name;
|
|
4353
4567
|
};
|
|
4354
4568
|
for (const name of names) {
|
|
4355
|
-
const filePath =
|
|
4569
|
+
const filePath = path4.join(inboxDir, name);
|
|
4356
4570
|
const readResult = readEntryResult(filePath);
|
|
4357
4571
|
if (readResult.kind === "missing") {
|
|
4358
4572
|
markResolved(name, false);
|
|
@@ -4422,6 +4636,7 @@ export {
|
|
|
4422
4636
|
CODEX_HOOK_EVENTS,
|
|
4423
4637
|
GEMINI_HOOK_EVENTS,
|
|
4424
4638
|
GROK_HOOK_EVENTS,
|
|
4639
|
+
SHELL_TOOL_NAMES,
|
|
4425
4640
|
parseHookCaptureRequest,
|
|
4426
4641
|
normalizeObserverHookCapture,
|
|
4427
4642
|
ObserverHookSpoolEntryTooLargeError,
|
|
@@ -99,6 +99,9 @@ function resolveRepositorySlugFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
|
99
99
|
const remote = git(cwd, ["remote", "get-url", "origin"], timeoutMs);
|
|
100
100
|
return remote ? repositorySlug(remote) : null;
|
|
101
101
|
}
|
|
102
|
+
function resolveRepositoryRootFromCwd(cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
103
|
+
return git(cwd, ["rev-parse", "--show-toplevel"], timeoutMs);
|
|
104
|
+
}
|
|
102
105
|
async function resolveRepoFromCwdAsync(cwd, signal) {
|
|
103
106
|
if (signal?.aborted) {
|
|
104
107
|
throw signal.reason ?? new Error("Repository lookup aborted");
|
|
@@ -134,5 +137,6 @@ async function resolveRepoFromCwdAsync(cwd, signal) {
|
|
|
134
137
|
export {
|
|
135
138
|
resolveRepoFromCwd,
|
|
136
139
|
resolveRepositorySlugFromCwd,
|
|
140
|
+
resolveRepositoryRootFromCwd,
|
|
137
141
|
resolveRepoFromCwdAsync
|
|
138
142
|
};
|