@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fml",
3
- "version": "0.8.0-rc.1",
3
+ "version": "0.8.0-rc.2",
4
4
  "description": "FML agent tools for Claude Code",
5
5
  "mcpServers": {
6
6
  "fml": {
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 pnpm are required.
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
- pnpm add --global @fml-inc/fml
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
- Installing the package does not change agent configuration. Run `fml install`
24
- explicitly to register the FML Claude Code plugin, install the agent-native FML
25
- skill, configure the FML MCP server, and register capture-only lifecycle hooks
26
- for supported agents. Invoke the skill as `/fml review` in Claude, Gemini, or
27
- Grok, and as `$fml review` in Codex. Review runs inside the agent from the
28
- packaged skill; it is not an `fml review` CLI command. Start a new agent session
29
- after installing so it picks up the integration. FML pre-approves only the
30
- isolated command hooks it installs for Codex; use `/hooks` to review or disable
31
- them.
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 pnpm
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,7 +1,7 @@
1
1
  import {
2
2
  resolvePackageRoot,
3
3
  runRawSyncOnce
4
- } from "./chunk-JXSSPQ5B.js";
4
+ } from "./chunk-YSU5CS2V.js";
5
5
 
6
6
  // src/observer/sync-cycle.ts
7
7
  import { fork } from "child_process";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  FML_HOOK_INBOX_DIR
3
- } from "./chunk-7DR5X7XI.js";
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 path2 from "path";
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 = path2.dirname(databaseFile);
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 path3 from "path";
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 path3.join(inboxDir, entryFileName(id));
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 path3.join(inboxDir, CAPACITY_STATE_FILE);
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 = path3.join(
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(path3.dirname(lockPath));
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 = path3.join(inboxDir, CAPACITY_LOCK_FILE);
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(path3.join(inboxDir, pending.temporaryFileName));
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(path3.join(inboxDir, name));
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(path3.join(inboxDir, fileName));
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: path3.basename(filePath),
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(path3.join(inboxDir, RETIREMENT_STATE_FILE), "utf8")
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 = path3.join(inboxDir, RETIREMENT_STATE_FILE);
3759
- const temporary = path3.join(
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
- path3.join(inboxDir, pending.fileName),
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: path3.basename(filePath),
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 = path3.join(
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: path3.basename(destination),
3904
- temporaryFileName: path3.basename(temporary),
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 = path3.join(inboxDir, name);
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 = path3.basename(destination);
4086
- const temporary = path3.join(inboxDir, name);
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 = path3.resolve(inboxDir);
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(path3.join(inboxDir, dirent.name));
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(path3.join(inboxDir, dirent.name));
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 = path3.join(inboxDir, name);
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
  };